Merge pull request #134 from ZeusWPI/refactoring

Refactoring
This commit is contained in:
Maxime 2019-09-05 16:13:21 +02:00 committed by GitHub
commit b90b8d571b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 661 additions and 767 deletions

1
.python-version Normal file
View file

@ -0,0 +1 @@
3.5.3

View file

@ -58,3 +58,7 @@ Finally run the webserver with
Run `pip-compile --upgrade` Run `pip-compile --upgrade`
For more information about managing the dependencies see [jazzband/pip-tools: A set of tools to keep your pinned Python dependencies fresh.](https://github.com/jazzband/pip-tools) For more information about managing the dependencies see [jazzband/pip-tools: A set of tools to keep your pinned Python dependencies fresh.](https://github.com/jazzband/pip-tools)
## Authors
* **Feliciaan De Palmenaer** - *Initial work* - [Github](https://github.com/feliciaan)

View file

@ -1,2 +0,0 @@
# Bind app.db to db
from app.app import db as db

View file

@ -1,14 +1,11 @@
import flask_login as login
from flask_admin import Admin from flask_admin import Admin
from flask_admin.contrib.sqla import ModelView from flask_admin.contrib.sqla import ModelView
import flask_login as login
from models import Location, Order, OrderItem, Product, User
from app import app, db
from models import User, Location, Product, Order, OrderItem
class ModelBaseView(ModelView): class ModelBaseView(ModelView):
def is_accessible(self): def is_accessible(self):
if login.current_user.is_anonymous(): if login.current_user.is_anonymous():
return False return False
@ -17,12 +14,12 @@ class ModelBaseView(ModelView):
class UserAdminModel(ModelBaseView): class UserAdminModel(ModelBaseView):
column_searchable_list = ('username',) column_searchable_list = ('username', )
inline_models = None inline_models = None
class ProductAdminModel(ModelBaseView): class ProductAdminModel(ModelBaseView):
column_searchable_list = ('name',) column_searchable_list = ('name', )
inline_models = None inline_models = None
@ -32,11 +29,11 @@ class LocationAdminModel(ModelBaseView):
form_columns = ('name', 'address', 'website', 'telephone') form_columns = ('name', 'address', 'website', 'telephone')
admin = Admin(app, name='Haldis', url='/admin', template_mode='bootstrap3') def init_admin(app, db):
admin = Admin(app, name='Haldis', url='/admin', template_mode='bootstrap3')
admin.add_view(UserAdminModel(User, db.session))
admin.add_view(UserAdminModel(User, db.session)) admin.add_view(LocationAdminModel(Location, db.session))
admin.add_view(LocationAdminModel(Location, db.session)) admin.add_view(ProductAdminModel(Product, db.session))
admin.add_view(ProductAdminModel(Product, db.session)) admin.add_view(ModelBaseView(Order, db.session))
admin.add_view(ModelBaseView(Order, db.session)) admin.add_view(ModelBaseView(OrderItem, db.session))
admin.add_view(ModelBaseView(OrderItem, db.session))

View file

@ -1,48 +1,153 @@
import logging import logging
from datetime import datetime
from logging.handlers import TimedRotatingFileHandler from logging.handlers import TimedRotatingFileHandler
from flask import Flask
from flask_bootstrap import Bootstrap, StaticCDN
from flask_sqlalchemy import SQLAlchemy
from flask_debugtoolbar import DebugToolbarExtension
from airbrake import Airbrake, AirbrakeHandler from airbrake import Airbrake, AirbrakeHandler
from flask import Flask, render_template
from flask_bootstrap import Bootstrap, StaticCDN
from flask_debugtoolbar import DebugToolbarExtension
from flask_login import LoginManager
from flask_migrate import Migrate, MigrateCommand
from flask_oauthlib.client import OAuth, OAuthException
from flask_script import Manager, Server
from admin import init_admin
from login import init_login
from models import db
from models.anonymous_user import AnonymouseUser
from utils import euro_string
from zeus import init_oauth
app = Flask(__name__) def create_app():
app.config.from_object('config.Configuration') app = Flask(__name__)
Bootstrap(app)
app.config['BOOTSTRAP_SERVE_LOCAL'] = True
# use our own bootstrap theme # Load the config file
app.extensions['bootstrap']['cdns']['bootstrap'] = StaticCDN() app.config.from_object('config.Configuration')
db = SQLAlchemy(app) manager = register_plugins(app, debug=app.debug)
add_handlers(app)
add_routes(app)
add_template_filters(app)
toolbar = DebugToolbarExtension(app) # TODO do we need to return and then run the manager?
return manager
if not app.debug: def register_plugins(app, debug: bool):
timedFileHandler = TimedRotatingFileHandler(app.config['LOGFILE'], when='midnight', backupCount=100) # Register Airbrake and enable the logrotation
timedFileHandler.setLevel(logging.DEBUG) if not app.debug:
timedFileHandler = TimedRotatingFileHandler(app.config['LOGFILE'],
when='midnight',
backupCount=100)
timedFileHandler.setLevel(logging.DEBUG)
loglogger = logging.getLogger('werkzeug') loglogger = logging.getLogger('werkzeug')
loglogger.setLevel(logging.DEBUG) loglogger.setLevel(logging.DEBUG)
loglogger.addHandler(timedFileHandler) loglogger.addHandler(timedFileHandler)
app.logger.addHandler(timedFileHandler) app.logger.addHandler(timedFileHandler)
airbrakelogger = logging.getLogger('airbrake') airbrakelogger = logging.getLogger('airbrake')
# Airbrake # Airbrake
airbrake = Airbrake( airbrake = Airbrake(project_id=app.config['AIRBRAKE_ID'],
project_id=app.config['AIRBRAKE_ID'], api_key=app.config['AIRBRAKE_KEY'])
api_key=app.config['AIRBRAKE_KEY'] # ugly hack to make this work for out errbit
) airbrake._api_url = "http://errbit.awesomepeople.tv/api/v3/projects/{}/notices".format(
# ugly hack to make this work for out errbit airbrake.project_id)
airbrake._api_url = "http://errbit.awesomepeople.tv/api/v3/projects/{}/notices".format(airbrake.project_id)
airbrakelogger.addHandler( airbrakelogger.addHandler(AirbrakeHandler(airbrake=airbrake))
AirbrakeHandler(airbrake=airbrake) app.logger.addHandler(AirbrakeHandler(airbrake=airbrake))
)
app.logger.addHandler( # Initialize SQLAlchemy
AirbrakeHandler(airbrake=airbrake) db.init_app(app)
)
# Initialize Flask-Migrate
migrate = Migrate(app, db)
manager = Manager(app)
manager.add_command('db', MigrateCommand)
manager.add_command('runserver', Server(port=8000))
# Add admin interface
init_admin(app, db)
# Init login manager
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.anonymous_user = AnonymouseUser
init_login(app)
# Add oauth
zeus = init_oauth(app)
app.zeus = zeus
# Load the bootstrap local cdn
Bootstrap(app)
app.config['BOOTSTRAP_SERVE_LOCAL'] = True
# use our own bootstrap theme
app.extensions['bootstrap']['cdns']['bootstrap'] = StaticCDN()
# Load the flask debug toolbar
toolbar = DebugToolbarExtension(app)
return manager
def add_handlers(app):
@app.errorhandler(404)
def handle404(e):
return render_template('errors/404.html'), 404
@app.errorhandler(401)
def handle401(e):
return render_template('errors/401.html'), 401
def add_routes(application):
# import views # TODO convert to blueprint
# import views.stats # TODO convert to blueprint
from views.order import order_bp
from views.general import general_bp
from views.stats import stats_blueprint
from views.debug import debug_bp
from login import auth_bp
from zeus import oauth_bp
application.register_blueprint(general_bp, url_prefix='/')
application.register_blueprint(order_bp, url_prefix='/order')
application.register_blueprint(stats_blueprint, url_prefix='/stats')
application.register_blueprint(auth_bp, url_prefix='/')
application.register_blueprint(oauth_bp, url_prefix='/')
if application.debug:
application.register_blueprint(debug_bp, url_prefix='/debug')
def add_template_filters(app):
@app.template_filter('countdown')
def countdown(value, only_positive=True, show_text=True):
delta = value - datetime.now()
if delta.total_seconds() < 0 and only_positive:
return "closed"
hours, remainder = divmod(delta.seconds, 3600)
minutes, seconds = divmod(remainder, 60)
time = '%02d:%02d:%02d' % (hours, minutes, seconds)
if show_text:
return 'closes in ' + time
return time
@app.template_filter('year')
def current_year(value):
return str(datetime.now().year)
@app.template_filter('euro')
def euro(value):
euro_string(value)
# For usage when you directly call the script with python
if __name__ == '__main__':
manager = create_app()
manager.run()

View file

@ -1,20 +1,23 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from flask import session from flask import session
from flask_login import current_user from flask_login import current_user
from flask_wtf import FlaskForm as Form from flask_wtf import FlaskForm as Form
from wtforms import SelectField, DateTimeField, validators, SubmitField, StringField from wtforms import (DateTimeField, SelectField, StringField, SubmitField,
validators)
from models import Location, User
from models import User, Location from utils import euro_string
from utils import euro
__author__ = 'feliciaan'
class OrderForm(Form): class OrderForm(Form):
courrier_id = SelectField('Courrier', coerce=int) courrier_id = SelectField('Courrier', coerce=int)
location_id = SelectField('Location', coerce=int, validators=[validators.required()]) location_id = SelectField('Location',
starttime = DateTimeField('Starttime', default=datetime.now, format='%d-%m-%Y %H:%M') coerce=int,
validators=[validators.required()])
starttime = DateTimeField('Starttime',
default=datetime.now,
format='%d-%m-%Y %H:%M')
stoptime = DateTimeField('Stoptime', format='%d-%m-%Y %H:%M') stoptime = DateTimeField('Stoptime', format='%d-%m-%Y %H:%M')
submit_button = SubmitField('Submit') submit_button = SubmitField('Submit')
@ -23,7 +26,9 @@ class OrderForm(Form):
self.courrier_id.choices = [(0, None)] + \ self.courrier_id.choices = [(0, None)] + \
[(u.id, u.username) for u in User.query.order_by('username')] [(u.id, u.username) for u in User.query.order_by('username')]
else: else:
self.courrier_id.choices = [(0, None), (current_user.id, current_user.username)] self.courrier_id.choices = [(0, None),
(current_user.id,
current_user.username)]
self.location_id.choices = [(l.id, l.name) self.location_id.choices = [(l.id, l.name)
for l in Location.query.order_by('name')] for l in Location.query.order_by('name')]
if self.stoptime.data is None: if self.stoptime.data is None:
@ -36,7 +41,9 @@ class OrderItemForm(Form):
submit_button = SubmitField('Submit') submit_button = SubmitField('Submit')
def populate(self, location): def populate(self, location):
self.product_id.choices = [(i.id, (i.name + ": " + euro(i.price))) for i in location.products] self.product_id.choices = [(i.id,
(i.name + ": " + euro_string(i.price)))
for i in location.products]
class AnonOrderItemForm(OrderItemForm): class AnonOrderItemForm(OrderItemForm):

View file

@ -1,19 +0,0 @@
from views import *
from app import app, db
from admin import admin
from login import login_manager
from models import *
from forms import *
from utils import *
from views import *
from flask_migrate import Migrate, MigrateCommand
from flask_script import Manager
if __name__ == '__main__':
# do it here, because make accessing db changes only possible when executing the program directly
migrate = Migrate(app, db)
manager = Manager(app)
manager.add_command('db', MigrateCommand)
manager.run()

View file

@ -1,31 +1,30 @@
from flask import redirect, abort, session, url_for from flask import abort, Blueprint
from flask_login import LoginManager, current_user, logout_user from flask import redirect, session, url_for
from flask_login import current_user, logout_user
from app import app
from models import User from models import User
from zeus import zeus_login from zeus import zeus_login
login_manager = LoginManager() auth_bp = Blueprint('auth_bp', __name__)
login_manager.init_app(app)
@login_manager.user_loader def init_login(app):
def load_user(userid): @app.login_manager.user_loader
return User.query.filter_by(id=userid).first() def load_user(userid):
return User.query.filter_by(id=userid).first()
@app.route('/login') @auth_bp.route('/login')
def login(): def login():
return zeus_login() return zeus_login()
@app.route('/logout') @auth_bp.route('/logout')
def logout(): def logout():
if 'zeus_token' in session: if 'zeus_token' in session:
session.pop('zeus_token', None) session.pop('zeus_token', None)
logout_user() logout_user()
return redirect(url_for('home')) return redirect(url_for('general_bp.home'))
def before_request(): def before_request():

View file

@ -1,54 +0,0 @@
"""Make it possible for some columns to be null
Revision ID: 2d696203e56
Revises: 354676f60be
Create Date: 2015-06-04 17:25:39.119678
"""
# revision identifiers, used by Alembic.
revision = '2d696203e56'
down_revision = '354676f60be'
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.alter_column('location', 'name',
existing_type=sa.VARCHAR(length=120),
nullable=False)
op.alter_column('order_item', 'order_id',
existing_type=sa.INTEGER(),
nullable=False)
op.alter_column('product', 'name',
existing_type=sa.VARCHAR(length=120),
nullable=False)
op.alter_column('product', 'price',
existing_type=sa.INTEGER(),
nullable=False)
op.alter_column('user', 'username',
existing_type=sa.VARCHAR(length=80),
nullable=False)
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.alter_column('user', 'username',
existing_type=sa.VARCHAR(length=80),
nullable=True)
op.alter_column('product', 'price',
existing_type=sa.INTEGER(),
nullable=True)
op.alter_column('product', 'name',
existing_type=sa.VARCHAR(length=120),
nullable=True)
op.alter_column('order_item', 'order_id',
existing_type=sa.INTEGER(),
nullable=True)
op.alter_column('location', 'name',
existing_type=sa.VARCHAR(length=120),
nullable=True)
### end Alembic commands ###

View file

@ -1,26 +0,0 @@
"""Add telephone number
Revision ID: 3243c3538fc
Revises: 2d696203e56
Create Date: 2015-06-04 18:39:54.895177
"""
# revision identifiers, used by Alembic.
revision = '3243c3538fc'
down_revision = '2d696203e56'
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.add_column('location', sa.Column('telephone', sa.String(length=20), nullable=True))
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_column('location', 'telephone')
### end Alembic commands ###

View file

@ -1,74 +0,0 @@
"""Initial configuration
Revision ID: 354676f60be
Revises: None
Create Date: 2015-06-04 17:17:28.933840
"""
# revision identifiers, used by Alembic.
revision = '354676f60be'
down_revision = None
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.create_table('location',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=120), nullable=True),
sa.Column('address', sa.String(length=254), nullable=True),
sa.Column('website', sa.String(length=120), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('username', sa.String(length=80), nullable=True),
sa.Column('admin', sa.Boolean(), nullable=True),
sa.Column('bias', sa.Integer(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('username')
)
op.create_table('product',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('location_id', sa.Integer(), nullable=True),
sa.Column('name', sa.String(length=120), nullable=True),
sa.Column('price', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['location_id'], ['location.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('order',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('courrier_id', sa.Integer(), nullable=True),
sa.Column('location_id', sa.Integer(), nullable=True),
sa.Column('starttime', sa.DateTime(), nullable=True),
sa.Column('stoptime', sa.DateTime(), nullable=True),
sa.Column('public', sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(['courrier_id'], ['user.id'], ),
sa.ForeignKeyConstraint(['location_id'], ['location.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('order_item',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('order_id', sa.Integer(), nullable=True),
sa.Column('product_id', sa.Integer(), nullable=True),
sa.Column('name', sa.String(length=120), nullable=True),
sa.ForeignKeyConstraint(['order_id'], ['order.id'], ),
sa.ForeignKeyConstraint(['product_id'], ['product.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_table('order_item')
op.drop_table('order')
op.drop_table('product')
op.drop_table('user')
op.drop_table('location')
### end Alembic commands ###

View file

@ -1,26 +0,0 @@
"""Removed foreign key constraint
Revision ID: 42709384216
Revises: 57a00d0b7bc
Create Date: 2015-06-04 22:34:05.237943
"""
# revision identifiers, used by Alembic.
revision = '42709384216'
down_revision = '57a00d0b7bc'
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('order_ibfk_1', 'order', type_='foreignkey')
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.create_foreign_key('order_ibfk_1', 'order', 'user', ['courrier_id'], ['id'])
### end Alembic commands ###

View file

@ -1,26 +0,0 @@
"""Add extra message
Revision ID: 4e94c0b08ed
Revises: 42709384216
Create Date: 2015-06-24 21:42:41.466973
"""
# revision identifiers, used by Alembic.
revision = '4e94c0b08ed'
down_revision = '42709384216'
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.add_column('order_item', sa.Column('extra', sa.String(length=254), nullable=True))
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_column('order_item', 'extra')
### end Alembic commands ###

View file

@ -1,26 +0,0 @@
"""Added order paid column
Revision ID: 57a00d0b7bc
Revises: 3243c3538fc
Create Date: 2015-06-04 19:16:47.888372
"""
# revision identifiers, used by Alembic.
revision = '57a00d0b7bc'
down_revision = '3243c3538fc'
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.add_column('order_item', sa.Column('paid', sa.Boolean(), nullable=True))
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_column('order_item', 'paid')
### end Alembic commands ###

View file

@ -1,173 +0,0 @@
from datetime import datetime
from collections import defaultdict
from app import db
# Create database models
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
admin = db.Column(db.Boolean)
bias = db.Column(db.Integer)
runs = db.relation('Order', backref='courrier', primaryjoin='Order.courrier_id==User.id',
foreign_keys='Order.courrier_id')
orderItems = db.relationship('OrderItem', backref='user', lazy='dynamic')
def configure(self, username, admin, bias):
self.username = username
self.admin = admin
self.bias = bias
def is_authenticated(self):
return True
def is_active(self):
return True
def is_admin(self):
return self.admin
def is_anonymous(self):
return False
def get_id(self):
try:
return unicode(self.id) # python 2
except NameError:
return str(self.id) # python 3
def __repr__(self):
return '%s' % self.username
class Location(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(120), nullable=False)
address = db.Column(db.String(254))
website = db.Column(db.String(120))
telephone = db.Column(db.String(20), nullable=True)
products = db.relationship('Product', backref='location', lazy='dynamic')
orders = db.relationship('Order', backref='location', lazy='dynamic')
def configure(self, name, address, telephone, website):
self.name = name
self.address = address
self.website = website
self.telephone = telephone
def __repr__(self):
return '%s' % (self.name)
class Product(db.Model):
id = db.Column(db.Integer, primary_key=True)
location_id = db.Column(db.Integer, db.ForeignKey('location.id'))
name = db.Column(db.String(120), nullable=False)
price = db.Column(db.Integer, nullable=False)
orderItems = db.relationship('OrderItem', backref='product', lazy='dynamic')
def configure(self, location, name, price):
self.location = location
self.name = name
self.price = price
def __repr__(self):
return '%s (€%d)from %s' % (self.name, self.price/100, self.location or 'None')
class Order(db.Model):
id = db.Column(db.Integer, primary_key=True)
courrier_id = db.Column(db.Integer, nullable=True)
location_id = db.Column(db.Integer, db.ForeignKey('location.id'))
starttime = db.Column(db.DateTime)
stoptime = db.Column(db.DateTime)
public = db.Column(db.Boolean, default=True)
items = db.relationship('OrderItem', backref='order', lazy='dynamic')
def configure(self, courrier, location, starttime, stoptime):
self.courrier = courrier
self.location = location
self.starttime = starttime
self.stoptime = stoptime
def __repr__(self):
if self.location:
return 'Order %d @ %s' % (self.id, self.location.name or 'None')
else:
return 'Order %d' % (self.id)
def group_by_user(self):
group = dict()
for item in self.items:
user = group.get(item.get_name(), dict())
user["total"] = user.get("total", 0) + item.product.price
user["to_pay"] = user.get("to_pay", 0) + item.product.price if not item.paid else 0
user["paid"] = user.get("paid", True) and item.paid
user["products"] = user.get("products", []) + [item.product]
group[item.get_name()] = user
return group
def group_by_product(self):
group = dict()
for item in self.items:
product = group.get(item.product.name, dict())
product['count'] = product.get("count", 0) + 1
if item.extra:
product["extras"] = product.get("extras", []) + [item.extra]
group[item.product.name] = product
return group
def can_close(self, user_id):
if self.stoptime and self.stoptime < datetime.now():
return False
user = None
if user_id:
user = User.query.filter_by(id=user_id).first()
print(user)
if self.courrier_id == user_id or (user and user.is_admin()):
return True
return False
class OrderItem(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
order_id = db.Column(db.Integer, db.ForeignKey('order.id'), nullable=False)
product_id = db.Column(db.Integer, db.ForeignKey('product.id'),
nullable=True) # TODO make false after init migration
paid = db.Column(db.Boolean, default=False, nullable=True) # TODO make false after init migration
extra = db.Column(db.String(254), nullable=True)
name = db.Column(db.String(120))
def configure(self, user, order, product):
self.user = user
self.order = order
self.product = product
def get_name(self):
if self.user_id is not None and self.user_id > 0:
return self.user.username
return self.name
def __repr__(self):
product_name = None
if self.product:
product_name = self.product.name
return 'Order %d: %s wants %s' % (self.order_id or 0, self.get_name(), product_name or 'None')
def can_delete(self, order_id, user_id, name):
if int(self.order_id) != int(order_id):
return False
if self.order.stoptime and self.order.stoptime < datetime.now():
return False
if self.user is not None and self.user_id == user_id:
return True
if user_id is None:
return False
user = User.query.filter(User.id == user_id).first()
if user and user.is_admin():
return True
return False

16
app/models/__init__.py Normal file
View file

@ -0,0 +1,16 @@
# This file will expose what we want from the models module
# This will probably be everything. But putting the imports here makes it possible to import all models in one line like this:
#
# from models import User, Item, ...
#
# Instead of this:
# from models.user import User
# from models.item import Item
# ...
from .database import db
from .location import Location
from .order import Order
from .orderitem import OrderItem
from .product import Product
from .user import User

View file

@ -0,0 +1,17 @@
class AnonymouseUser:
id = None
def is_active(self):
return False
def is_authenticated(self):
return False
def is_anonymous(self):
return True
def is_admin(self):
return False
def get_id(self):
return None

3
app/models/database.py Normal file
View file

@ -0,0 +1,3 @@
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()

20
app/models/location.py Normal file
View file

@ -0,0 +1,20 @@
from models import db
class Location(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(120), nullable=False)
address = db.Column(db.String(254))
website = db.Column(db.String(120))
telephone = db.Column(db.String(20), nullable=True)
products = db.relationship('Product', backref='location', lazy='dynamic')
orders = db.relationship('Order', backref='location', lazy='dynamic')
def configure(self, name, address, telephone, website):
self.name = name
self.address = address
self.website = website
self.telephone = telephone
def __repr__(self):
return '%s' % (self.name)

61
app/models/order.py Normal file
View file

@ -0,0 +1,61 @@
from datetime import datetime
from .database import db
from .user import User
class Order(db.Model):
id = db.Column(db.Integer, primary_key=True)
courrier_id = db.Column(db.Integer, nullable=True)
location_id = db.Column(db.Integer, db.ForeignKey('location.id'))
starttime = db.Column(db.DateTime)
stoptime = db.Column(db.DateTime)
public = db.Column(db.Boolean, default=True)
items = db.relationship('OrderItem', backref='order', lazy='dynamic')
def configure(self, courrier, location, starttime, stoptime):
self.courrier = courrier
self.location = location
self.starttime = starttime
self.stoptime = stoptime
def __repr__(self):
if self.location:
return 'Order %d @ %s' % (self.id, self.location.name or 'None')
else:
return 'Order %d' % (self.id)
def group_by_user(self):
group = dict()
for item in self.items:
user = group.get(item.get_name(), dict())
user["total"] = user.get("total", 0) + item.product.price
user["to_pay"] = user.get(
"to_pay", 0) + item.product.price if not item.paid else 0
user["paid"] = user.get("paid", True) and item.paid
user["products"] = user.get("products", []) + [item.product]
group[item.get_name()] = user
return group
def group_by_product(self):
group = dict()
for item in self.items:
product = group.get(item.product.name, dict())
product['count'] = product.get("count", 0) + 1
if item.extra:
product["extras"] = product.get("extras", []) + [item.extra]
group[item.product.name] = product
return group
def can_close(self, user_id):
if self.stoptime and self.stoptime < datetime.now():
return False
user = None
if user_id:
user = User.query.filter_by(id=user_id).first()
print(user)
if self.courrier_id == user_id or (user and user.is_admin()):
return True
return False

48
app/models/orderitem.py Normal file
View file

@ -0,0 +1,48 @@
from datetime import datetime
from .database import db
from .user import User
class OrderItem(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
order_id = db.Column(db.Integer, db.ForeignKey('order.id'), nullable=False)
product_id = db.Column(
db.Integer, db.ForeignKey('product.id'),
nullable=True) # TODO make false after init migration
paid = db.Column(db.Boolean, default=False,
nullable=True) # TODO make false after init migration
extra = db.Column(db.String(254), nullable=True)
name = db.Column(db.String(120))
def configure(self, user, order, product):
self.user = user
self.order = order
self.product = product
def get_name(self):
if self.user_id is not None and self.user_id > 0:
return self.user.username
return self.name
def __repr__(self):
product_name = None
if self.product:
product_name = self.product.name
return 'Order %d: %s wants %s' % (self.order_id or 0, self.get_name(),
product_name or 'None')
def can_delete(self, order_id, user_id, name):
if int(self.order_id) != int(order_id):
return False
if self.order.stoptime and self.order.stoptime < datetime.now():
return False
if self.user is not None and self.user_id == user_id:
return True
if user_id is None:
return False
user = User.query.filter(User.id == user_id).first()
if user and user.is_admin():
return True
return False

20
app/models/product.py Normal file
View file

@ -0,0 +1,20 @@
from models import db
class Product(db.Model):
id = db.Column(db.Integer, primary_key=True)
location_id = db.Column(db.Integer, db.ForeignKey('location.id'))
name = db.Column(db.String(120), nullable=False)
price = db.Column(db.Integer, nullable=False)
orderItems = db.relationship('OrderItem',
backref='product',
lazy='dynamic')
def configure(self, location, name, price):
self.location = location
self.name = name
self.price = price
def __repr__(self):
return '%s (€%d)from %s' % (self.name, self.price / 100, self.location
or 'None')

36
app/models/user.py Normal file
View file

@ -0,0 +1,36 @@
from models import db
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
admin = db.Column(db.Boolean)
bias = db.Column(db.Integer)
runs = db.relation('Order',
backref='courrier',
primaryjoin='Order.courrier_id==User.id',
foreign_keys='Order.courrier_id')
orderItems = db.relationship('OrderItem', backref='user', lazy='dynamic')
def configure(self, username, admin, bias):
self.username = username
self.admin = admin
self.bias = bias
def is_authenticated(self):
return True
def is_active(self):
return True
def is_admin(self):
return self.admin
def is_anonymous(self):
return False
def get_id(self):
return str(self.id)
def __repr__(self):
return '%s' % self.username

47
app/notification.py Normal file
View file

@ -0,0 +1,47 @@
import json
from datetime import datetime
from threading import Thread
import requests
from flask import current_app as app
from flask import url_for
def post_order_to_webhook(order_item):
message = ''
if order_item.courrier is not None:
message = '<!channel|@channel> {3} is going to {1}, order <{0}|here>! Deadline in {2} minutes!'.format(
url_for('order_bp.order', id=order_item.id, _external=True),
order_item.location.name, remaining_minutes(order_item.stoptime),
order_item.courrier.username.title())
else:
message = '<!channel|@channel> New order for {}. Deadline in {} minutes. <{}|Open here.>'.format(
order_item.location.name, remaining_minutes(order_item.stoptime),
url_for('order_bp.order', id=order_item.id, _external=True))
webhookthread = WebhookSenderThread(message)
webhookthread.start()
class WebhookSenderThread(Thread):
def __init__(self, message):
super(WebhookSenderThread, self).__init__()
self.message = message
def run(self):
self.slack_webhook()
def slack_webhook(self):
js = json.dumps({'text': self.message})
url = app.config['SLACK_WEBHOOK']
if len(url) > 0:
requests.post(url, data=js)
else:
app.logger.info(str(js))
def remaining_minutes(value):
delta = value - datetime.now()
if delta.total_seconds() < 0:
return "0"
minutes, _ = divmod(delta.total_seconds(), 60)
return "%02d" % minutes

View file

@ -1,7 +1,9 @@
#!/usr/bin/env python #!/usr/bin/env python
import sys
import os import os
import sys
from app import create_app
INTERP = os.path.expanduser("~/env/bin/python") INTERP = os.path.expanduser("~/env/bin/python")
if sys.executable != INTERP: if sys.executable != INTERP:
@ -9,4 +11,8 @@ if sys.executable != INTERP:
sys.path.append(os.getcwd()) sys.path.append(os.getcwd())
from haldis import app as application application = create_app()
# For running on the server with passenger etc
if __name__ == "__main__":
application.run(port=8000)

View file

@ -4,6 +4,6 @@
<div class="jumbotron"> <div class="jumbotron">
<h1>Unauthorized</h1> <h1>Unauthorized</h1>
<p>You're not authorized to look to this page!</p> <p>You're not authorized to look to this page!</p>
<p><a href="{{ url_for('home') }}">Go somewhere nice</a></p> <p><a href="{{ url_for('general_bp.home') }}">Go somewhere nice</a></p>
</div> </div>
{% endblock %} {% endblock %}

View file

@ -4,6 +4,6 @@
<div class="jumbotron"> <div class="jumbotron">
<h1>Page Not Found</h1> <h1>Page Not Found</h1>
<p>What you were looking for is just not there.</p> <p>What you were looking for is just not there.</p>
<p><a href="{{ url_for('home') }}">Go somewhere nice</a></p> <p><a href="{{ url_for('general_bp.home') }}">Go somewhere nice</a></p>
</div> </div>
{% endblock %} {% endblock %}

View file

@ -2,12 +2,12 @@
{% import "bootstrap/utils.html" as utils %} {% import "bootstrap/utils.html" as utils %}
{% set navbar = [ {% set navbar = [
('home', 'Home'), ('general_bp.home', 'Home'),
('order_bp.orders', 'Orders'), ('order_bp.orders', 'Orders'),
('locations', 'Locations'), ('general_bp.locations', 'Locations'),
('map', 'Map'), ('general_bp.map', 'Map'),
('about', 'About'), ('general_bp.about', 'About'),
('stats', 'Stats'), ('stats_blueprint.stats', 'Stats'),
] -%} ] -%}
{% if not current_user.is_anonymous() and current_user.is_admin() -%} {% if not current_user.is_anonymous() and current_user.is_admin() -%}
{% set navbar = navbar + [('admin.index', 'Admin')] -%} {% set navbar = navbar + [('admin.index', 'Admin')] -%}
@ -43,7 +43,7 @@ Haldis - {{ active_page|capitalize }}
<span class="icon-bar"></span> <span class="icon-bar"></span>
<span class="icon-bar"></span> <span class="icon-bar"></span>
</button> </button>
<a class="navbar-brand" href="{{ url_for('home') }}">HALDIS</a> <a class="navbar-brand" href="{{ url_for('general_bp.home') }}">HALDIS</a>
</div> </div>
<div id="navbar" class="navbar-collapse collapse"> <div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav"> <ul class="nav navbar-nav">
@ -55,10 +55,10 @@ Haldis - {{ active_page|capitalize }}
</ul> </ul>
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
{% if current_user.is_anonymous() %} {% if current_user.is_anonymous() %}
<li><a href="{{ url_for('login') }}">Login</a></li> <li><a href="{{ url_for('auth_bp.login') }}">Login</a></li>
{% else %} {% else %}
<li><a href="{{ url_for('profile') }}">{{ current_user.username }}</a></li> <li><a href="{{ url_for('general_bp.profile') }}">{{ current_user.username }}</a></li>
<li><a href="{{ url_for('logout') }}">Logout</a></li> <li><a href="{{ url_for('auth_bp.logout') }}">Logout</a></li>
{% endif %} {% endif %}
</ul> </ul>
</div><!--/.nav-collapse --> </div><!--/.nav-collapse -->

View file

@ -14,7 +14,7 @@
<tbody> <tbody>
{% for loc in locations -%} {% for loc in locations -%}
<tr> <tr>
<td><a href="{{ url_for('location', id=loc.id) }}">{{ loc.name }}</a></td> <td><a href="{{ url_for('general_bp.location', id=loc.id) }}">{{ loc.name }}</a></td>
<td>{{ loc.address }}<td> <td>{{ loc.address }}<td>
<td><a href="{{ loc.website}}"><span class="glyphicon glyphicon-link"></span></a></td> <td><a href="{{ loc.website}}"><span class="glyphicon glyphicon-link"></span></a></td>
<td> <td>

View file

@ -28,7 +28,7 @@
loc = { loc = {
"address": "{{loc.address}}", "address": "{{loc.address}}",
"name": "{{loc.name}}", "name": "{{loc.name}}",
"url": "{{ url_for('location', id=loc.id) }}" "url": "{{ url_for('general_bp.location', id=loc.id) }}"
}; };
locations.push(loc); locations.push(loc);

View file

@ -11,14 +11,14 @@
<h3 id="order-title">Order {{ order.id }} <h3 id="order-title">Order {{ order.id }}
<div class="pull-right"> <div class="pull-right">
{% if order.can_close(current_user.id) -%} {% if order.can_close(current_user.id) -%}
<a class="btn btn-danger" href="{{ url_for('.close_order', id=order.id) }}">Close</a> <a class="btn btn-danger" href="{{ url_for('order_bp.close_order', id=order.id) }}">Close</a>
{% endif %}{% if courier_or_admin %} {% endif %}{% if courier_or_admin %}
<a class="btn btn-warning" href="{{ url_for('.order_edit', id=order.id) }}">Edit</a> <a class="btn btn-warning" href="{{ url_for('order_bp.order_edit', id=order.id) }}">Edit</a>
{%- endif %} {%- endif %}
</div></h3> </div></h3>
courier: {{ order.courrier.username }} courier: {{ order.courrier.username }}
{% if order.courrier == None and not current_user.is_anonymous() %} {% if order.courrier == None and not current_user.is_anonymous() %}
<a href="{{ url_for('.volunteer', id=order.id) }}" class="btn btn-primary btn-sm">Volunteer</a> <a href="{{ url_for('order_bp.volunteer', id=order.id) }}" class="btn btn-primary btn-sm">Volunteer</a>
{% endif %} {% endif %}
<br/> <br/>
location: <a href="{{ order.location.website }}">{{ order.location.name }}</a><br/> location: <a href="{{ order.location.website }}">{{ order.location.name }}</a><br/>
@ -34,7 +34,7 @@
{% if form -%} {% if form -%}
<div class="col-md-push-1 col-md-10 darker" id="form"> <div class="col-md-push-1 col-md-10 darker" id="form">
<h4>Order:</h4> <h4>Order:</h4>
<form method="post" action="{{ url_for('.order_item_create', id=order.id) }}"> <form method="post" action="{{ url_for('order_bp.order_item_create', id=order.id) }}">
<span class="pull-right"> <span class="pull-right">
<a class="btn btn-primary" onclick="chooseRandom()">Choose for me</a> <a class="btn btn-primary" onclick="chooseRandom()">Choose for me</a>
</span> </span>
@ -76,8 +76,8 @@
<td>{{ item.get_name() }}</td> <td>{{ item.get_name() }}</td>
<td><span title="{{ item.extra if item.extra }}">{{ item.product.name }}{{ "*" if item.extra }}</span></td> <td><span title="{{ item.extra if item.extra }}">{{ item.product.name }}{{ "*" if item.extra }}</span></td>
<td>{{ item.product.price|euro }}</td> <td>{{ item.product.price|euro }}</td>
{% if courier_or_admin %}<td>{% if not item.paid %} <a class="btn btn-xs btn-primary" href="{{ url_for('.item_paid', order_id=order.id, item_id=item.id) }}">Pay</a> {% else %} <span class="glyphicon glyphicon-chevron-down"></span> {% endif %}</td>{% endif %} {% if courier_or_admin %}<td>{% if not item.paid %} <a class="btn btn-xs btn-primary" href="{{ url_for('order_bp.item_paid', order_id=order.id, item_id=item.id) }}">Pay</a> {% else %} <span class="glyphicon glyphicon-chevron-down"></span> {% endif %}</td>{% endif %}
<td>{% if item.can_delete(order.id, current_user.id, session.get('anon_name', '')) -%}<a href="{{ url_for('.delete_item', order_id=order.id, item_id=item.id) }}"><span class="glyphicon glyphicon-remove"></span></a>{%- endif %}<br/></td> <td>{% if item.can_delete(order.id, current_user.id, session.get('anon_name', '')) -%}<a href="{{ url_for('order_bp.delete_item', order_id=order.id, item_id=item.id) }}"><span class="glyphicon glyphicon-remove"></span></a>{%- endif %}<br/></td>
</tr> </tr>
{%- endfor %} {%- endfor %}
</tbody> </tbody>
@ -85,7 +85,7 @@
</div> </div>
<div class="col-md-push-2 col-md-4 darker box" id="items-ordered"> <div class="col-md-push-2 col-md-4 darker box" id="items-ordered">
<h3>Ordered products: {{ order.items.count() }}</h3> <h3>Ordered products: {{ order.items.count() }}</h3>
<a class="divLink" href="{{ url_for('.items_showcase', id=order.id) }}"></a> <a class="divLink" href="{{ url_for('order_bp.items_showcase', id=order.id) }}"></a>
{% for key, value in order.group_by_product().items() -%} {% for key, value in order.group_by_product().items() -%}
<div class="product"> <div class="product">
{{ key }}: {{ value["count"] }} {{ key }}: {{ value["count"] }}
@ -113,7 +113,7 @@
<td>{{ key }}</td> <td>{{ key }}</td>
<td>{{ value["total"]|euro }}</td> <td>{{ value["total"]|euro }}</td>
<td>{{ value["to_pay"]|euro }}</td> <td>{{ value["to_pay"]|euro }}</td>
{% if courier_or_admin %}<td>{% if not value["to_pay"] == 0 %} <a class="btn btn-xs btn-primary" href="{{ url_for('.items_user_paid', order_id=order.id, user_name=key) }}">Pay</a> {% else %} <span class="glyphicon glyphicon-chevron-down"></span> {% endif %}</td>{% endif %} {% if courier_or_admin %}<td>{% if not value["to_pay"] == 0 %} <a class="btn btn-xs btn-primary" href="{{ url_for('order_bp.items_user_paid', order_id=order.id, user_name=key) }}">Pay</a> {% else %} <span class="glyphicon glyphicon-chevron-down"></span> {% endif %}</td>{% endif %}
</tr> </tr>
{%- endfor %} {%- endfor %}
</tbody> </tbody>

View file

@ -26,7 +26,7 @@
<h3>Create new order:</h3> <h3>Create new order:</h3>
<div class="row darker"> <div class="row darker">
<div class="col-sm-12"> <div class="col-sm-12">
<form method="post" action="{{ url_for('.order_create') }}"> <form method="post" action="{{ url_for('order_bp.order_create') }}">
{{ form.csrf_token }} {{ form.csrf_token }}
<div class="form-group select2 {{ 'has-errors' if form.courrier_id.errors else ''}}"> <div class="form-group select2 {{ 'has-errors' if form.courrier_id.errors else ''}}">
{{ form.courrier_id.label(class='control-label') }}<br> {{ form.courrier_id.label(class='control-label') }}<br>

View file

@ -1,57 +1,6 @@
from datetime import datetime def euro_string(value):
from flask import render_template """
Convert cents to string formatted euro
from app import app """
from login import login_manager result = '{:.2f}'.format(round(value / 100, 2))
__author__ = 'feliciaan'
@app.template_filter('euro')
def euro(value):
result = '{:.2f}'.format(round(value/100,2))
return result return result
@app.template_filter('countdown')
def countdown(value, only_positive=True, show_text=True):
delta = value - datetime.now()
if delta.total_seconds() < 0 and only_positive:
return "closed"
hours, remainder = divmod(delta.seconds, 3600)
minutes, seconds = divmod(remainder, 60)
time = '%02d:%02d:%02d' % (hours, minutes, seconds)
if show_text:
return 'closes in ' + time
return time
@app.template_filter('year')
def current_year(value):
return str(datetime.now().year)
@app.errorhandler(404)
def handle404(e):
return render_template('errors/404.html'), 404
@app.errorhandler(401)
def handle401(e):
return render_template('errors/401.html'), 401
class AnonymouseUser:
id = None
def is_active(self):
return False
def is_authenticated(self):
return False
def is_anonymous(self):
return True
def is_admin(self):
return False
def get_id(self):
return None
login_manager.anonymous_user = AnonymouseUser

View file

@ -1,85 +0,0 @@
from datetime import datetime, timedelta
from flask import url_for, render_template, abort, send_from_directory
from flask_login import login_required
import os
from app import app
from models import Order, Location
# import views
from views.order import get_orders
from views import stats
@app.route('/')
def home():
prev_day = datetime.now() - timedelta(days=1)
recently_closed = get_orders(
((Order.stoptime > prev_day) & (Order.stoptime < datetime.now())))
return render_template('home.html', orders=get_orders(),
recently_closed=recently_closed)
@app.route('/map', defaults= {'id': None})
@app.route('/map/<int:id>')
def map(id):
locs = Location.query.order_by('name')
return render_template('maps.html', locations= locs)
@app.route('/location')
def locations():
locs = Location.query.order_by('name')
return render_template('locations.html', locations=locs)
@app.route('/location/<int:id>')
def location(id):
loc = Location.query.filter(Location.id == id).first()
if loc is None:
abort(404)
return render_template('location.html', location=loc, title=loc.name)
@app.route('/about/')
def about():
return render_template('about.html')
@app.route('/profile/')
@login_required
def profile():
return render_template('profile.html')
@app.route('/favicon.ico')
def favicon():
if len(get_orders((Order.stoptime > datetime.now()))) == 0:
return send_from_directory(os.path.join(app.root_path, 'static'), 'favicon.ico', mimetype='image/x-icon')
else:
return send_from_directory(os.path.join(app.root_path, 'static'), 'favicon_orange.ico', mimetype='image/x-icon')
if app.debug: # add route information
@app.route('/routes')
@login_required
def list_routes():
import urllib
output = []
for rule in app.url_map.iter_rules():
options = {}
for arg in rule.arguments:
options[arg] = "[{0}]".format(arg)
print(rule.endpoint)
methods = ','.join(rule.methods)
url = url_for(rule.endpoint, **options)
line = urllib.parse.unquote(
"{:50s} {:20s} {}".format(rule.endpoint, methods, url))
output.append(line)
string = ''
for line in sorted(output):
string += line + "<br/>"
return string

29
app/views/debug.py Normal file
View file

@ -0,0 +1,29 @@
from flask import Blueprint
from flask import current_app as app
from flask import url_for
from flask_login import login_required
debug_bp = Blueprint('debug_bp', __name__)
@debug_bp.route('/routes')
@login_required
def list_routes():
import urllib
output = []
for rule in app.url_map.iter_rules():
options = {}
for arg in rule.arguments:
options[arg] = "[{0}]".format(arg)
print(rule.endpoint)
methods = ','.join(rule.methods)
url = url_for(rule.endpoint, **options)
line = urllib.parse.unquote("{:50s} {:20s} {}".format(
rule.endpoint, methods, url))
output.append(line)
string = ''
for line in sorted(output):
string += line + "<br/>"
return string

67
app/views/general.py Normal file
View file

@ -0,0 +1,67 @@
import os
from datetime import datetime, timedelta
from flask import Blueprint, abort
from flask import current_app as app
from flask import render_template, send_from_directory, url_for
from flask_login import login_required
from models import Location, Order
# import views
from views.order import get_orders
general_bp = Blueprint('general_bp', __name__)
@general_bp.route('/')
def home():
prev_day = datetime.now() - timedelta(days=1)
recently_closed = get_orders(
((Order.stoptime > prev_day) & (Order.stoptime < datetime.now())))
return render_template('home.html',
orders=get_orders(),
recently_closed=recently_closed)
@general_bp.route('/map', defaults={'id': None})
@general_bp.route('/map/<int:id>')
def map(id):
locs = Location.query.order_by('name')
return render_template('maps.html', locations=locs)
@general_bp.route('/location')
def locations():
locs = Location.query.order_by('name')
return render_template('locations.html', locations=locs)
@general_bp.route('/location/<int:id>')
def location(id):
loc = Location.query.filter(Location.id == id).first()
if loc is None:
abort(404)
return render_template('location.html', location=loc, title=loc.name)
@general_bp.route('/about/')
def about():
return render_template('about.html')
@general_bp.route('/profile/')
@login_required
def profile():
return render_template('profile.html')
@general_bp.route('/favicon.ico')
def favicon():
if len(get_orders((Order.stoptime > datetime.now()))) == 0:
return send_from_directory(os.path.join(app.root_path, 'static'),
'favicon.ico',
mimetype='image/x-icon')
else:
return send_from_directory(os.path.join(app.root_path, 'static'),
'favicon_orange.ico',
mimetype='image/x-icon')

View file

@ -1,18 +1,18 @@
__author__ = 'feliciaan'
import json
from threading import Thread
import requests
from flask import url_for, render_template, abort, redirect, Blueprint, flash, session, request
from flask_login import current_user, login_required
import random import random
from datetime import datetime from datetime import datetime
from app import app, db # from flask import current_app as app
from models import Order, OrderItem, User from flask import (Blueprint, abort, flash, redirect, render_template, request,
from forms import OrderItemForm, OrderForm, AnonOrderItemForm session, url_for)
from flask_login import current_user, login_required
from forms import AnonOrderItemForm, OrderForm, OrderItemForm
from models import Order, OrderItem, User, db
from notification import post_order_to_webhook
order_bp = Blueprint('order_bp', 'order') order_bp = Blueprint('order_bp', 'order')
@order_bp.route('/') @order_bp.route('/')
def orders(form=None): def orders(form=None):
if form is None and not current_user.is_anonymous(): if form is None and not current_user.is_anonymous():
@ -35,7 +35,7 @@ def order_create():
db.session.add(order) db.session.add(order)
db.session.commit() db.session.commit()
post_order_to_webhook(order) post_order_to_webhook(order)
return redirect(url_for('.order', id=order.id)) return redirect(url_for('order_bp.order', id=order.id))
return orders(form=orderForm) return orders(form=orderForm)
@ -48,13 +48,19 @@ def order(id, form=None):
flash('Please login to see this order.', 'info') flash('Please login to see this order.', 'info')
abort(401) abort(401)
if form is None: if form is None:
form = AnonOrderItemForm() if current_user.is_anonymous() else OrderItemForm() form = AnonOrderItemForm() if current_user.is_anonymous(
) else OrderItemForm()
form.populate(order.location) form.populate(order.location)
if order.stoptime and order.stoptime < datetime.now(): if order.stoptime and order.stoptime < datetime.now():
form = None form = None
total_price = sum([o.product.price for o in order.items]) total_price = sum([o.product.price for o in order.items])
debts = sum([o.product.price for o in order.items if not o.paid]) debts = sum([o.product.price for o in order.items if not o.paid])
return render_template('order.html', order=order, form=form, total_price=total_price, debts=debts) return render_template('order.html',
order=order,
form=form,
total_price=total_price,
debts=debts)
@order_bp.route('/<id>/items') @order_bp.route('/<id>/items')
def items_showcase(id, form=None): def items_showcase(id, form=None):
@ -66,11 +72,13 @@ def items_showcase(id, form=None):
abort(401) abort(401)
return render_template('order_items.html', order=order) return render_template('order_items.html', order=order)
@order_bp.route('/<id>/edit', methods=['GET', 'POST']) @order_bp.route('/<id>/edit', methods=['GET', 'POST'])
@login_required @login_required
def order_edit(id): def order_edit(id):
order = Order.query.filter(Order.id == id).first() order = Order.query.filter(Order.id == id).first()
if current_user.id is not order.courrier_id and not current_user.is_admin(): if current_user.id is not order.courrier_id and not current_user.is_admin(
):
abort(401) abort(401)
if order is None: if order is None:
abort(404) abort(404)
@ -79,7 +87,7 @@ def order_edit(id):
if orderForm.validate_on_submit(): if orderForm.validate_on_submit():
orderForm.populate_obj(order) orderForm.populate_obj(order)
db.session.commit() db.session.commit()
return redirect(url_for('.order', id=order.id)) return redirect(url_for('order_bp.order', id=order.id))
return render_template('order_edit.html', form=orderForm, order_id=id) return render_template('order_edit.html', form=orderForm, order_id=id)
@ -93,7 +101,8 @@ def order_item_create(id):
if current_user.is_anonymous() and not current_order.public: if current_user.is_anonymous() and not current_order.public:
flash('Please login to see this order.', 'info') flash('Please login to see this order.', 'info')
abort(401) abort(401)
form = AnonOrderItemForm() if current_user.is_anonymous() else OrderItemForm() form = AnonOrderItemForm() if current_user.is_anonymous(
) else OrderItemForm()
form.populate(current_order.location) form.populate(current_order.location)
if form.validate_on_submit(): if form.validate_on_submit():
item = OrderItem() item = OrderItem()
@ -106,7 +115,7 @@ def order_item_create(id):
db.session.add(item) db.session.add(item)
db.session.commit() db.session.commit()
flash('Ordered %s' % (item.product.name), 'success') flash('Ordered %s' % (item.product.name), 'success')
return redirect(url_for('.order', id=id)) return redirect(url_for('order_bp.order', id=id))
return order(id, form=form) return order(id, form=form)
@ -118,8 +127,9 @@ def item_paid(order_id, item_id):
if item.order.courrier_id == id or current_user.admin: if item.order.courrier_id == id or current_user.admin:
item.paid = True item.paid = True
db.session.commit() db.session.commit()
flash('Paid %s by %s' % (item.product.name, item.get_name()), 'success') flash('Paid %s by %s' % (item.product.name, item.get_name()),
return redirect(url_for('.order', id=order_id)) 'success')
return redirect(url_for('order_bp.order', id=order_id))
abort(404) abort(404)
@ -129,9 +139,11 @@ def items_user_paid(order_id, user_name):
user = User.query.filter(User.username == user_name).first() user = User.query.filter(User.username == user_name).first()
items = [] items = []
if user: if user:
items = OrderItem.query.filter((OrderItem.user_id == user.id) & (OrderItem.order_id == order_id)) items = OrderItem.query.filter((OrderItem.user_id == user.id)
& (OrderItem.order_id == order_id))
else: else:
items = OrderItem.query.filter((OrderItem.name == user_name) & (OrderItem.order_id == order_id)) items = OrderItem.query.filter((OrderItem.name == user_name)
& (OrderItem.order_id == order_id))
current_order = Order.query.filter(Order.id == order_id).first() current_order = Order.query.filter(Order.id == order_id).first()
for item in items: for item in items:
print(item) print(item)
@ -139,8 +151,9 @@ def items_user_paid(order_id, user_name):
for item in items: for item in items:
item.paid = True item.paid = True
db.session.commit() db.session.commit()
flash('Paid %d items for %s' % (items.count(), item.get_name()), 'success') flash('Paid %d items for %s' % (items.count(), item.get_name()),
return redirect(url_for('.order', id=order_id)) 'success')
return redirect(url_for('order_bp.order', id=order_id))
abort(404) abort(404)
@ -156,7 +169,7 @@ def delete_item(order_id, item_id):
db.session.delete(item) db.session.delete(item)
db.session.commit() db.session.commit()
flash('Deleted %s' % (product_name), 'success') flash('Deleted %s' % (product_name), 'success')
return redirect(url_for('.order', id=order_id)) return redirect(url_for('order_bp.order', id=order_id))
abort(404) abort(404)
@ -172,7 +185,7 @@ def volunteer(id):
flash("Thank you for volunteering!") flash("Thank you for volunteering!")
else: else:
flash("Volunteering not possible!") flash("Volunteering not possible!")
return redirect(url_for('.order', id=id)) return redirect(url_for('order_bp.order', id=id))
@order_bp.route('/<id>/close') @order_bp.route('/<id>/close')
@ -190,9 +203,7 @@ def close_order(id):
if courrier is not None: if courrier is not None:
order.courrier_id = courrier.id order.courrier_id = courrier.id
db.session.commit() db.session.commit()
return redirect(url_for('.order', id=id)) return redirect(url_for('order_bp.order', id=id))
app.register_blueprint(order_bp, url_prefix='/order')
def select_user(items): def select_user(items):
@ -216,52 +227,12 @@ def select_user(items):
def get_orders(expression=None): def get_orders(expression=None):
orders = [] orders = []
if expression is None: if expression is None:
expression = ((datetime.now() > Order.starttime) & (Order.stoptime > datetime.now()) | (Order.stoptime == None)) expression = ((datetime.now() > Order.starttime) &
(Order.stoptime > datetime.now()) |
(Order.stoptime == None))
if not current_user.is_anonymous(): if not current_user.is_anonymous():
orders = Order.query.filter(expression).all() orders = Order.query.filter(expression).all()
else: else:
orders = Order.query.filter((expression & (Order.public == True))).all() orders = Order.query.filter(
(expression & (Order.public == True))).all()
return orders return orders
def post_order_to_webhook(order_item):
message = ''
if order_item.courrier is not None:
message = '<!channel|@channel> {3} is going to {1}, order <{0}|here>! Deadline in {2} minutes!'.format(
url_for('.order', id=order_item.id, _external=True),
order_item.location.name,
remaining_minutes(order_item.stoptime),
order_item.courrier.username.title())
else:
message = '<!channel|@channel> New order for {}. Deadline in {} minutes. <{}|Open here.>'.format(
order_item.location.name,
remaining_minutes(order_item.stoptime),
url_for('.order', id=order_item.id, _external=True))
webhookthread = WebhookSenderThread(message)
webhookthread.start()
class WebhookSenderThread(Thread):
def __init__(self, message):
super(WebhookSenderThread, self).__init__()
self.message = message
def run(self):
self.slack_webhook()
def slack_webhook(self):
js = json.dumps({'text': self.message})
url = app.config['SLACK_WEBHOOK']
if len(url) > 0:
requests.post(url, data=js)
else:
app.logger.info(str(js))
def remaining_minutes(value):
delta = value - datetime.now()
if delta.total_seconds() < 0:
return "0"
minutes, _ = divmod(delta.total_seconds(), 60)
return "%02d" % minutes

View file

@ -1,10 +1,13 @@
from flask import Blueprint
from flask import current_app as app
from flask import render_template from flask import render_template
from fatmodels import FatLocation, FatOrder, FatOrderItem, FatUser, FatProduct from fatmodels import FatLocation, FatOrder, FatOrderItem, FatProduct, FatUser
from app import app
stats_blueprint = Blueprint('stats_blueprint', __name__)
@app.route('/stats/') @stats_blueprint.route('/')
def stats(): def stats():
data = { data = {
'amount': { 'amount': {

View file

@ -1,44 +1,29 @@
from flask import redirect, url_for, session, jsonify, flash, request from flask import (current_app, flash, redirect, request, session,
url_for, Blueprint)
from flask_login import login_user from flask_login import login_user
from flask_oauthlib.client import OAuth, OAuthException from flask_oauthlib.client import OAuthException, OAuth
import json
import requests
from models import User, db
from app import app, db oauth_bp = Blueprint("oauth_bp", __name__)
from models import User
oauth = OAuth(app)
zeus = oauth.remote_app(
'zeus',
consumer_key=app.config['ZEUS_KEY'],
consumer_secret=app.config['ZEUS_SECRET'],
request_token_params={},
base_url='https://adams.ugent.be/oauth/api/',
access_token_method='POST',
access_token_url='https://adams.ugent.be/oauth/oauth2/token/',
authorize_url='https://adams.ugent.be/oauth/oauth2/authorize/'
)
def zeus_login(): def zeus_login():
return zeus.authorize(callback=url_for('authorized', _external=True)) return current_app.zeus.authorize(
callback=url_for('oauth_bp.authorized', _external=True))
@app.route('/login/zeus/authorized') @oauth_bp.route('/login/zeus/authorized')
def authorized(): def authorized():
resp = zeus.authorized_response() resp = current_app.zeus.authorized_response()
if resp is None: if resp is None:
return 'Access denied: reason=%s error=%s' % ( return 'Access denied: reason=%s error=%s' % (
request.args['error'], request.args['error'], request.args['error_description'])
request.args['error_description']
)
if isinstance(resp, OAuthException): if isinstance(resp, OAuthException):
return 'Access denied: %s' % resp.message + '<br>' + str(resp.data) return 'Access denied: %s' % resp.message + '<br>' + str(resp.data)
session['zeus_token'] = (resp['access_token'], '') session['zeus_token'] = (resp['access_token'], '')
me = zeus.get('current_user/') me = current_app.zeus.get('current_user/')
username = me.data.get('username', '').lower() username = me.data.get('username', '').lower()
user = User.query.filter_by(username=username).first() user = User.query.filter_by(username=username).first()
@ -49,17 +34,31 @@ def authorized():
return login_and_redirect_user(user) return login_and_redirect_user(user)
flash("You're not allowed to enter, please contact a system administrator") flash("You're not allowed to enter, please contact a system administrator")
return redirect(url_for("home")) return redirect(url_for("general_bp.home"))
def init_oauth(app):
oauth = OAuth(app)
@zeus.tokengetter zeus = oauth.remote_app(
def get_zeus_oauth_token(): 'zeus',
return session.get('zeus_token') consumer_key=app.config['ZEUS_KEY'],
consumer_secret=app.config['ZEUS_SECRET'],
request_token_params={},
base_url='https://adams.ugent.be/oauth/api/',
access_token_method='POST',
access_token_url='https://adams.ugent.be/oauth/oauth2/token/',
authorize_url='https://adams.ugent.be/oauth/oauth2/authorize/')
@zeus.tokengetter
def get_zeus_oauth_token():
return session.get('zeus_token')
return zeus
def login_and_redirect_user(user): def login_and_redirect_user(user):
login_user(user) login_user(user)
return redirect(url_for("home")) return redirect(url_for("general_bp.home"))
def create_user(username): def create_user(username):

View file

@ -29,4 +29,4 @@ cd ..
echo -e "${B} Seeding database ${E}" echo -e "${B} Seeding database ${E}"
./populate-db.sh ./populate-db.sh
echo -e "${B} Activate your venv using 'source venv/bin/activate'.\nThen run the server with 'python app/haldis.py runserver' ${E}" echo -e "${B} Activate your venv using 'source venv/bin/activate'.\nThen run the server with 'python app/app.py runserver' ${E}"