refactoring, reorganizing

Changed the folder structure for better maintenance and inserted
Dockerfile for image building.
This commit is contained in:
Lunaresk 2022-07-13 09:00:26 +02:00
parent f26dee5489
commit b978e0da56
25 changed files with 447 additions and 358 deletions

22
Dockerfile Normal file
View File

@ -0,0 +1,22 @@
FROM python:3.10-slim
RUN useradd scan2kasse
WORKDIR /home/scan2kasse
COPY requirements.txt requirements.txt
RUN python -m venv venv
RUN venv/bin/pip install -r requirements.txt
COPY app app
COPY migrations migrations
COPY run.py boot.sh ./
RUN chmod +x boot.sh
ENV FLASK_APP run.py
RUN chown -R scan2kasse:scan2kasse ./
USER scan2kasse
EXPOSE 5000
ENTRYPOINT ["./boot.sh"]

View File

@ -1,12 +1,13 @@
from flask import Flask
from flask_bootstrap import Bootstrap
from flask_login import LoginManager
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from yaml import safe_load
from logging import getLogger
from logging.config import fileConfig
from os import makedirs
from os.path import dirname, exists
from yaml import safe_load
try:
dir_name = dirname(__file__)
@ -23,11 +24,28 @@ if not exists(DIR + "logs"):
fileConfig(DIR + "configs/log.conf")
LOGGER = getLogger("root")
app = Flask(__name__)
app.config.from_file("configs/config.yaml", safe_load)
db = SQLAlchemy(app)
migrate = Migrate(app, db, render_as_batch=True)
login = LoginManager(app)
bootstrap = Bootstrap()
db = SQLAlchemy()
login = LoginManager()
login.login_view = 'web_login'
migrate = Migrate()
from app import routes, models
def create_app(config_file="configs/config.yaml"):
app = Flask(__name__)
app.config.from_file(config_file, safe_load)
bootstrap.init_app(app)
db.init_app(app)
login.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)
from app.main import bp as main_bp
app.register_blueprint(main_bp)
return app
from app import models

5
app/auth/__init__.py Normal file
View File

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

27
app/auth/forms.py Normal file
View File

@ -0,0 +1,27 @@
from app.models import User
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Email, EqualTo, ValidationError
class LoginForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired()])
remember_me = BooleanField('Remember Me')
submit = SubmitField('Sign In', render_kw={"class": "btn btn-primary mt-3"})
class RegistrationForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired()])
password2 = PasswordField(
'Repeat Password', validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('Register', render_kw={"class": "btn btn-primary mt-3"})
def validate_username(self, username):
user = User.query.filter_by(username=username.data).first()
if user is not None:
raise ValidationError('Please use a different username.')
def validate_email(self, email):
user = User.query.filter_by(email=email.data).first()
if user is not None:
raise ValidationError('Please use a different email address.')

44
app/auth/routes.py Normal file
View File

@ -0,0 +1,44 @@
from app import db
from app.auth import bp
from app.auth.forms import LoginForm, RegistrationForm
from app.models import User
from app.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
@bp.route(f'/register', methods=['GET', 'POST'])
def web_register():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = RegistrationForm()
if form.validate_on_submit():
user = User(username=form.username.data, email=form.email.data)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash('Congratulations, you are now a registered user!')
return redirect(url_for('auth.login'))
return render_template('auth/register.html', title='Register', form=form)
@bp.route(f'/login', methods=['GET', 'POST'])
def web_login():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user is None or not user.check_password(form.password.data):
flash('Invalid username or password')
return redirect(url_for('auth.web_login'))
login_user(user, remember=form.remember_me.data)
next_page = request.args.get('next')
if not next_page or url_parse(next_page).netloc != '':
next_page = url_for('main.index')
return redirect(next_page)
return render_template('auth/login.html', title='Sign In', form=form)
@bp.route(f'/logout')
def web_logout():
logout_user()
return redirect(url_for('main.index'))

5
app/errors/__init__.py Normal file
View File

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

12
app/errors/handlers.py Normal file
View File

@ -0,0 +1,12 @@
from app import db
from app.errors import bp
from flask import render_template
@bp.app_errorhandler(404)
def not_found_error(error):
return render_template('errors/404.html'), 404
@bp.app_errorhandler(500)
def internal_error(error):
db.session.rollback()
return render_template('errors/500.html'), 500

View File

@ -1,61 +0,0 @@
from app.models import Brand, Category, User
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField, SelectMultipleField, DateField, IntegerField, SelectField, FloatField
from wtforms.validators import DataRequired, Email, EqualTo, Optional, ValidationError
class LoginForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
password = PasswordField('Password', validators=[DataRequired()])
remember_me = BooleanField('Remember Me')
submit = SubmitField('Sign In')
class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired()])
password2 = PasswordField(
'Repeat Password', validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('Register')
def validate_username(self, username):
user = User.query.filter_by(username=username.data).first()
if user is not None:
raise ValidationError('Please use a different username.')
def validate_email(self, email):
user = User.query.filter_by(email=email.data).first()
if user is not None:
raise ValidationError('Please use a different email address.')
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()])
submit = SubmitField("Submit")
@classmethod
def new(cls):
form = cls()
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")
class NewBrandForm(FlaskForm):
name = StringField("Name", validators=[DataRequired()])
submit = SubmitField("Submit")
class NeueWGForm(FlaskForm):
wg_name = StringField("Name", validators=[DataRequired()])
submit = SubmitField("Submit")
class WGBeitretenForm(FlaskForm):
submit = SubmitField("Submit")

5
app/main/__init__.py Normal file
View File

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

37
app/main/forms.py Normal file
View File

@ -0,0 +1,37 @@
from app.models import Brand, 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()])
submit = SubmitField("Submit")
@classmethod
def new(cls):
form = cls()
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"})

View File

@ -1,106 +1,24 @@
from app import app, db, LOGGER
from app.forms import NewItemForm, LoginForm, RegistrationForm
from app.models import Establishment, LoginToken, User, Item, Brand, PriceChange, AmountChange
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, flash, redirect, request, url_for
from flask import abort, redirect, request, url_for
from flask.json import jsonify
from flask_login import current_user, login_required, login_user, logout_user
from werkzeug.urls import url_parse
from flask_login import current_user, login_required
APPNAME = "scan2kasse"
@app.route(f'/{APPNAME}')
@bp.route('/')
@bp.route('/index')
def index():
return render_template("base.html")
@app.route(f'/{APPNAME}/token_authorization')
def token_authorization():
LOGGER.debug("Token Login")
if not request.json or 'login' not in request.json:
abort(400)
if not LoginToken.query.filter_by(token=request.json['login']).first():
abort(403)
return jsonify({}), 200
# @bp.route('/')
# def test():
# return "Hello World"
@app.route(f'/{APPNAME}/token_insert', methods=['POST'])
def insert():
match request.json:
case {'user': user, 'items': items, 'date': date}:
failed = database_utils.insert_bought_items(user, items, date)
case {'user': user, 'items': items}:
failed = database_utils.insert_bought_items(user, items)
case _:
abort(400)
if failed:
return jsonify(failed), 400
return jsonify({'inserted': True}), 201
@app.route('/register', methods=['GET', 'POST'])
def web_register():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = RegistrationForm()
if form.validate_on_submit():
user = User(username=form.username.data, email=form.email.data)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash('Congratulations, you are now a registered user!')
return redirect(url_for('login'))
return render_template('register.html', title='Register', form=form)
@app.route(f'/{APPNAME}/login', methods=['GET', 'POST'])
def web_login():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user is None or not user.check_password(form.password.data):
flash('Invalid username or password')
return redirect(url_for('web_login'))
login_user(user, remember=form.remember_me.data)
next_page = request.args.get('next')
if not next_page or url_parse(next_page).netloc != '':
next_page = url_for('index')
return redirect(next_page)
return render_template('login.html', title='Sign In', form=form)
@app.route(f'/{APPNAME}/logout')
def web_logout():
logout_user()
return redirect(url_for('index'))
@app.route(f'/{APPNAME}/newitem', methods=['GET', 'POST'])
@login_required
def new_item():
if current_user.is_anonymous:
abort(403)
form=NewItemForm.new()
if form.is_submitted():
LOGGER.debug("submitted")
if form.validate():
LOGGER.debug("valid")
else:
LOGGER.debug(form.errors)
if form.validate_on_submit():
LOGGER.debug("valid form")
brand = Brand.query.get(form.brand.data)
new_item = Item(id = form.id.data, name = form.name.data, brand = brand.id, description = form.description.data)
# if form.category.data:
# category = Category.query.get(id = form.category.data)
# new_item.Category = category
new_item.PriceChange = [PriceChange(Item = new_item, date = date(2021, 12, 1), price = form.price_change.data)]
if form.amount_change.data:
new_item.AmountChange = [AmountChange(Item = new_item, date = date(2021, 12, 1), amount = form.amount_change.data)]
db.session.add(new_item)
db.session.commit()
return redirect(url_for('index'))
return render_template('admin/new_item.html', form=form)
@app.route(f'/{APPNAME}/overview', methods=['GET'])
@bp.route('/overview', methods=['GET'])
@login_required
def get_report_from_user():
if current_user.is_anonymous:
@ -130,7 +48,56 @@ def get_report_from_user():
else:
return render_template("overview.html", results=result_list)
@app.route(f'/{APPNAME}/overview/register_boughts', methods=['GET'])
@bp.route('/token_authorization')
def token_authorization():
LOGGER.debug("Token Login")
if not request.json or 'login' not in request.json:
abort(400)
if not LoginToken.query.filter_by(token=request.json['login']).first():
abort(403)
return jsonify({}), 200
@bp.route('/token_insert', methods=['POST'])
def insert():
match request.json:
case {'user': user, 'items': items, 'date': date}:
failed = database_utils.insert_bought_items(user, items, date)
case {'user': user, 'items': items}:
failed = database_utils.insert_bought_items(user, items)
case _:
abort(400)
if failed:
return jsonify(failed), 400
return jsonify({'inserted': True}), 201
@bp.route('/new_item', methods=['GET', 'POST'])
@login_required
def new_item():
if current_user.is_anonymous:
abort(403)
form=NewItemForm.new()
if form.is_submitted():
LOGGER.debug("submitted")
if form.validate():
LOGGER.debug("valid")
else:
LOGGER.debug(form.errors)
if form.validate_on_submit():
LOGGER.debug("valid form")
brand = Brand.query.get(form.brand.data)
new_item = Item(id = form.id.data, name = form.name.data, brand = brand.id, description = form.description.data)
# if form.category.data:
# category = Category.query.get(id = form.category.data)
# new_item.Category = category
new_item.PriceChange = [PriceChange(Item = new_item, date = date(2021, 12, 1), price = form.price_change.data)]
if form.amount_change.data:
new_item.AmountChange = [AmountChange(Item = new_item, date = date(2021, 12, 1), amount = form.amount_change.data)]
db.session.add(new_item)
db.session.commit()
return redirect(url_for('index'))
return render_template('admin/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:

View File

@ -4,14 +4,13 @@ from flask_login import UserMixin
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),
db.Column("category", db.ForeignKey("category.id"), primary_key=True)
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(64), nullable=False, unique=True)
username = db.Column(db.String(64), nullable=False, unique=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')
@ -40,9 +39,10 @@ class Establishment(db.Model):
return f"<Establishment {self.id} ({self.name})>"
class LoginToken(db.Model):
user = db.Column(db.ForeignKey('user.id'), primary_key=True)
establishment = db.Column(db.ForeignKey('establishment.id'), primary_key=True)
user = db.Column(db.ForeignKey('user.id'), primary_key=True, server_onupdate=db.FetchedValue())
establishment = db.Column(db.ForeignKey('establishment.id'), primary_key=True, server_onupdate=db.FetchedValue())
token = db.Column(db.String(15), nullable=True, unique=True)
paid = db.Column(db.BigInteger, nullable=False, server_default=str(0))
def __repr__(self) -> str:
return f"LoginToken {self.token}"
@ -66,7 +66,7 @@ class Category(db.Model):
class Item(db.Model):
id = db.Column(db.BigInteger, primary_key=True)
name = db.Column(db.String(64), nullable=False)
brand = db.Column(db.ForeignKey('brand.id'), 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")
@ -78,18 +78,17 @@ class Item(db.Model):
return f"<Item {self.id} ({self.name})>"
class Bought(db.Model):
token = db.Column(db.ForeignKey('login_token.token'), primary_key=True)
item = db.Column(db.ForeignKey('item.id'), primary_key=True)
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))
paid = db.Column(db.SmallInteger, nullable=False, server_default=str(0))
def __repr__(self) -> str:
return f"<Bought Object>"
class PriceChange(db.Model):
item = db.Column(db.ForeignKey('item.id'), primary_key=True)
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)
@ -97,7 +96,7 @@ class PriceChange(db.Model):
return f"<Price_Change {self.item} ({self.date})>"
class AmountChange(db.Model):
item = db.Column(db.ForeignKey('item.id'), primary_key=True)
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))
@ -107,15 +106,15 @@ class AmountChange(db.Model):
class Receipt(db.Model):
id = db.Column(db.Numeric(precision=22, scale=0), primary_key=True)
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))
paid = db.Column(db.SmallInteger, nullable=False, server_default=str(0))
def __repr__(self) -> str:
return f"<Receipt {self.id}>"
class ItemReceipt(db.Model):
receipt = db.Column(db.ForeignKey("receipt.id"), primary_key=True)
item = db.Column(db.ForeignKey("item.id"), primary_key=True)
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:

View File

@ -0,0 +1,19 @@
{% extends "base.html" %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<h1>Sign In</h1>
<form action="" method="post" novalidate>
{{ form.hidden_tag() }}
<p>
{{ wtf.form_field(form.email) }}
</p>
<p>
{{ wtf.form_field(form.password) }}
</p>
<p>
{{ wtf.form_field(form.remember_me) }}
</p>
{{ wtf.form_field(form.submit, class=form.submit.render_kw["class"]) }}
</form>
{% endblock %}

View File

@ -0,0 +1,19 @@
{% extends "base.html" %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<h1>Register</h1>
<form action="" method="post">
{{ form.hidden_tag() }}
<p>
{{ wtf.form_field(form.email) }}
</p>
<p>
{{ wtf.form_field(form.password) }}
</p>
<p>
{{ wtf.form_field(form.password2) }}
</p>
{{ wtf.form_field(form.submit, class=form.submit.render_kw["class"]) }}
</form>
{% endblock %}

View File

@ -1,18 +1,24 @@
<!doctype html>
<html>
<head>
{% 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")}}>
{% if title %}
<title>{{ title }}</title>
{% else %}
<title>Scan2Kasse</title>
{% endif %}
<!-- <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> -->
</head>
<body>
{% 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">
@ -41,7 +47,7 @@
{% if establishments %}
<li><a href="#" class="link-dark rounded">Allgemein</a></li>
{% for establishment in establishments %}
<li><a href="{{ url_for('get_report_from_user', establishment=establishment.id) }}" class="link-dark rounded">{{ establishment.name }}</a></li>
<li><a href="{{ url_for('main.get_report_from_user', establishment=establishment.id) }}" class="link-dark rounded">{{ establishment.name }}</a></li>
{% endfor %}
{% endif %}
</ul>
@ -71,16 +77,19 @@
<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('web_logout') }} class="link-dark rounded">Sign out</a></li>
<li><a href={{ url_for('auth.web_logout') }} class="link-dark rounded">Sign out</a></li>
{% else %}
<li><a href={{ url_for('web_register') }} class="link-dark rounded">Register</a></li>
<li><a href={{ url_for('web_login') }} class="link-dark rounded">Sign in</a></li>
<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>
@ -94,11 +103,14 @@
<div class="container">
<div class="row my-3"></div>
<div class="row-md-3">
{% block content %}{% endblock %}
{% block app_content %}{% endblock %}
</div>
<div class="row my-3"></div>
</div>
</main>
</body>
{% 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>
</html>
<!-- <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> -->
{% endblock %}

View File

@ -0,0 +1,6 @@
{% extends "base.html" %}
{% block app_content %}
<h1>File Not Found</h1>
<p><a href="{{ url_for('main.index') }}">Back</a></p>
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends "base.html" %}
{% block app_content %}
<h1>An unexpected error has occurred</h1>
<p>The administrator has been notified. Sorry for the inconvenience!</p>
<p><a href="{{ url_for('main.index') }}">Back</a></p>
{% endblock %}

View File

@ -1,24 +0,0 @@
{% extends "base.html" %}
{% block content %}
<h1>Sign In</h1>
<form action="" method="post" novalidate>
{{ form.hidden_tag() }}
<p>
{{ form.username.label }}<br>
{{ form.username(size=32) }}
{% for error in form.username.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.password.label }}<br>
{{ form.password(size=32) }}
{% for error in form.password.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
<p>{{ form.submit() }}</p>
</form>
{% endblock %}

View File

@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block content %}
{% block app_content %}
<form action="" method="post">
{{ form.hidden_tag() }}
<div class="row">{{form.id.label}}</div>

View File

@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block content %}
{% 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('check_unregistered_items', establishment=establishment.id) }}'">

View File

@ -1,37 +0,0 @@
{% extends "base.html" %}
{% block content %}
<h1>Register</h1>
<form action="" method="post">
{{ form.hidden_tag() }}
<p>
{{ form.username.label }}<br>
{{ form.username(size=32) }}<br>
{% for error in form.username.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.email.label }}<br>
{{ form.email(size=64) }}<br>
{% for error in form.email.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.password.label }}<br>
{{ form.password(size=32) }}<br>
{% for error in form.password.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.password2.label }}<br>
{{ form.password2(size=32) }}<br>
{% for error in form.password2.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.submit() }}</p>
</form>
{% endblock %}

1
app/utils/constants.py Normal file
View File

@ -0,0 +1 @@
APPNAME = "scan2kasse"

5
boot.sh Normal file
View File

@ -0,0 +1,5 @@
#!/bin/bash
source venv/bin/activate
flask db upgrade
flask translate compile
python run.py

View File

@ -31,12 +31,10 @@ def upgrade():
)
op.create_table('user',
sa.Column('id', sa.BigInteger(), nullable=False),
sa.Column('email', sa.String(length=64), nullable=False),
sa.Column('username', sa.String(length=64), nullable=False),
sa.Column('email', sa.String(length=255), nullable=False),
sa.Column('password_hash', sa.String(length=128), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email'),
sa.UniqueConstraint('username')
sa.UniqueConstraint('email')
)
op.create_table('establishment',
sa.Column('id', sa.BigInteger(), nullable=False),
@ -45,11 +43,22 @@ def upgrade():
sa.ForeignKeyConstraint(['owner'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('login_token',
sa.Column('user', sa.BigInteger(), nullable=False),
sa.Column('establishment', sa.BigInteger(), nullable=False),
sa.Column('token', sa.String(length=15), nullable=True),
sa.Column('paid', sa.BigInteger(), server_default='0', nullable=False),
sa.ForeignKeyConstraint(['establishment'], ['establishment.id'], ),
sa.ForeignKeyConstraint(['user'], ['user.id'], ),
sa.PrimaryKeyConstraint('user', 'establishment'),
sa.UniqueConstraint('token')
)
op.create_table('receipt',
sa.Column('id', sa.Numeric(precision=22, scale=0), nullable=False),
sa.Column('date', sa.Date(), nullable=False),
sa.Column('from_user', sa.String(length=15), nullable=True),
sa.Column('registered', sa.Boolean(), server_default='False', nullable=False),
sa.Column('paid', sa.SmallInteger(), server_default='0', nullable=False),
sa.ForeignKeyConstraint(['from_user'], ['login_token.token'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('item',
@ -60,15 +69,6 @@ def upgrade():
sa.ForeignKeyConstraint(['brand'], ['brand.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('login_token',
sa.Column('user', sa.BigInteger(), nullable=False),
sa.Column('establishment', sa.BigInteger(), nullable=False),
sa.Column('token', sa.String(length=15), nullable=True),
sa.ForeignKeyConstraint(['establishment'], ['establishment.id'], ),
sa.ForeignKeyConstraint(['user'], ['user.id'], ),
sa.PrimaryKeyConstraint('user', 'establishment'),
sa.UniqueConstraint('token')
)
op.create_table('amount_change',
sa.Column('item', sa.BigInteger(), nullable=False),
sa.Column('date', sa.Date(), server_default='2021-12-01', nullable=False),
@ -82,7 +82,6 @@ def upgrade():
sa.Column('date', sa.Date(), nullable=False),
sa.Column('amount', sa.SmallInteger(), nullable=False),
sa.Column('registered', sa.Boolean(), server_default='False', nullable=False),
sa.Column('paid', sa.SmallInteger(), server_default='0', nullable=False),
sa.ForeignKeyConstraint(['item'], ['item.id'], ),
sa.ForeignKeyConstraint(['token'], ['login_token.token'], ),
sa.PrimaryKeyConstraint('token', 'item', 'date')
@ -119,11 +118,11 @@ def downgrade():
op.drop_table('item_category')
op.drop_table('bought')
op.drop_table('amount_change')
op.drop_table('login_token')
op.drop_table('item')
op.drop_table('user')
op.drop_table('receipt')
op.drop_table('login_token')
op.drop_table('establishment')
op.drop_table('user')
op.drop_table('category')
op.drop_table('brand')
# ### end Alembic commands ###

4
run.py
View File

@ -1,7 +1,9 @@
from app import app, db
from app import create_app, db
from app.models import *
from gevent.pywsgi import WSGIServer
app = create_app()
@app.shell_context_processor
def make_shell_context():
return {'db': db, 'User': User, 'Bought': Bought, 'Item': Item,