refactoring, reorganizing
Changed the folder structure for better maintenance and inserted Dockerfile for image building.
This commit is contained in:
parent
f26dee5489
commit
b978e0da56
22
Dockerfile
Normal file
22
Dockerfile
Normal 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"]
|
||||
@ -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
5
app/auth/__init__.py
Normal 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
27
app/auth/forms.py
Normal 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
44
app/auth/routes.py
Normal 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
5
app/errors/__init__.py
Normal 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
12
app/errors/handlers.py
Normal 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
|
||||
61
app/forms.py
61
app/forms.py
@ -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
5
app/main/__init__.py
Normal 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
37
app/main/forms.py
Normal 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"})
|
||||
@ -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:
|
||||
@ -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:
|
||||
|
||||
19
app/templates/auth/login.html
Normal file
19
app/templates/auth/login.html
Normal 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 %}
|
||||
19
app/templates/auth/register.html
Normal file
19
app/templates/auth/register.html
Normal 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 %}
|
||||
@ -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>
|
||||
</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>
|
||||
</html>
|
||||
<!-- <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> -->
|
||||
{% endblock %}
|
||||
6
app/templates/errors/404.html
Normal file
6
app/templates/errors/404.html
Normal 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 %}
|
||||
7
app/templates/errors/500.html
Normal file
7
app/templates/errors/500.html
Normal 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 %}
|
||||
@ -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 %}
|
||||
@ -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>
|
||||
@ -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) }}'">
|
||||
@ -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
1
app/utils/constants.py
Normal file
@ -0,0 +1 @@
|
||||
APPNAME = "scan2kasse"
|
||||
5
boot.sh
Normal file
5
boot.sh
Normal file
@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
source venv/bin/activate
|
||||
flask db upgrade
|
||||
flask translate compile
|
||||
python run.py
|
||||
@ -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 ###
|
||||
Loading…
x
Reference in New Issue
Block a user