diff --git a/app/__init__.py b/app/__init__.py deleted file mode 100644 index a811a0d..0000000 --- a/app/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Bind app.db to db -from app.app import db as db diff --git a/app/admin.py b/app/admin.py index a18e048..c2ddb34 100644 --- a/app/admin.py +++ b/app/admin.py @@ -1,14 +1,11 @@ +import flask_login as login from flask_admin import Admin from flask_admin.contrib.sqla import ModelView -import flask_login as login - -from app import app, db -from models import User, Location, Product, Order, OrderItem +from models import Location, Order, OrderItem, Product, User class ModelBaseView(ModelView): - def is_accessible(self): if login.current_user.is_anonymous(): return False @@ -17,12 +14,12 @@ class ModelBaseView(ModelView): class UserAdminModel(ModelBaseView): - column_searchable_list = ('username',) + column_searchable_list = ('username', ) inline_models = None class ProductAdminModel(ModelBaseView): - column_searchable_list = ('name',) + column_searchable_list = ('name', ) inline_models = None @@ -32,11 +29,11 @@ class LocationAdminModel(ModelBaseView): 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(LocationAdminModel(Location, db.session)) -admin.add_view(ProductAdminModel(Product, db.session)) -admin.add_view(ModelBaseView(Order, db.session)) -admin.add_view(ModelBaseView(OrderItem, db.session)) + admin.add_view(UserAdminModel(User, db.session)) + admin.add_view(LocationAdminModel(Location, db.session)) + admin.add_view(ProductAdminModel(Product, db.session)) + admin.add_view(ModelBaseView(Order, db.session)) + admin.add_view(ModelBaseView(OrderItem, db.session)) diff --git a/app/app.py b/app/app.py index bbeb0af..8844f8e 100644 --- a/app/app.py +++ b/app/app.py @@ -1,48 +1,148 @@ import logging +from datetime import datetime 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 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 + +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__) -app.config.from_object('config.Configuration') -Bootstrap(app) -app.config['BOOTSTRAP_SERVE_LOCAL'] = True +def create_app(): + app = Flask(__name__) -# use our own bootstrap theme -app.extensions['bootstrap']['cdns']['bootstrap'] = StaticCDN() + # Load the config file + 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 app -if not app.debug: - timedFileHandler = TimedRotatingFileHandler(app.config['LOGFILE'], when='midnight', backupCount=100) - timedFileHandler.setLevel(logging.DEBUG) +def register_plugins(app, debug: bool): + # Register Airbrake and enable the logrotation + if not app.debug: + timedFileHandler = TimedRotatingFileHandler(app.config['LOGFILE'], + when='midnight', + backupCount=100) + timedFileHandler.setLevel(logging.DEBUG) - loglogger = logging.getLogger('werkzeug') - loglogger.setLevel(logging.DEBUG) - loglogger.addHandler(timedFileHandler) - app.logger.addHandler(timedFileHandler) + loglogger = logging.getLogger('werkzeug') + loglogger.setLevel(logging.DEBUG) + loglogger.addHandler(timedFileHandler) + app.logger.addHandler(timedFileHandler) - airbrakelogger = logging.getLogger('airbrake') + airbrakelogger = logging.getLogger('airbrake') - # Airbrake - airbrake = Airbrake( - project_id=app.config['AIRBRAKE_ID'], - 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(airbrake.project_id) + # Airbrake + airbrake = Airbrake(project_id=app.config['AIRBRAKE_ID'], + 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( + airbrake.project_id) - airbrakelogger.addHandler( - AirbrakeHandler(airbrake=airbrake) - ) - app.logger.addHandler( - AirbrakeHandler(airbrake=airbrake) - ) + airbrakelogger.addHandler(AirbrakeHandler(airbrake=airbrake)) + app.logger.addHandler(AirbrakeHandler(airbrake=airbrake)) + + # Initialize SQLAlchemy + db.init_app(app) + + # Initialize Flask-Migrate + migrate = Migrate(app, db) + manager = Manager(app) + manager.add_command('db', MigrateCommand) + + # 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 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='/') + + +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__': + app = create_app() + app.run(threaded=True, host='0.0.0.0') diff --git a/app/forms.py b/app/forms.py index a10ba66..d326ea0 100644 --- a/app/forms.py +++ b/app/forms.py @@ -1,20 +1,25 @@ from datetime import datetime, timedelta + from flask import session from flask_login import current_user 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 User, Location -from utils import euro +from models import Location, User +from utils import euro_string __author__ = 'feliciaan' class OrderForm(Form): courrier_id = SelectField('Courrier', coerce=int) - location_id = SelectField('Location', coerce=int, validators=[validators.required()]) - starttime = DateTimeField('Starttime', default=datetime.now, format='%d-%m-%Y %H:%M') + location_id = SelectField('Location', + 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') submit_button = SubmitField('Submit') @@ -23,7 +28,9 @@ class OrderForm(Form): self.courrier_id.choices = [(0, None)] + \ [(u.id, u.username) for u in User.query.order_by('username')] 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) for l in Location.query.order_by('name')] if self.stoptime.data is None: @@ -36,7 +43,9 @@ class OrderItemForm(Form): submit_button = SubmitField('Submit') 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): diff --git a/app/haldis.py b/app/haldis.py deleted file mode 100644 index 272d516..0000000 --- a/app/haldis.py +++ /dev/null @@ -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() diff --git a/app/login.py b/app/login.py index 0c96825..a14cd2b 100644 --- a/app/login.py +++ b/app/login.py @@ -1,31 +1,30 @@ -from flask import redirect, abort, session, url_for -from flask_login import LoginManager, current_user, logout_user +from flask import abort, Blueprint +from flask import redirect, session, url_for +from flask_login import current_user, logout_user - -from app import app from models import User from zeus import zeus_login -login_manager = LoginManager() -login_manager.init_app(app) +auth_bp = Blueprint('auth_bp', __name__) -@login_manager.user_loader -def load_user(userid): - return User.query.filter_by(id=userid).first() +def init_login(app): + @app.login_manager.user_loader + def load_user(userid): + return User.query.filter_by(id=userid).first() -@app.route('/login') +@auth_bp.route('/login') def login(): return zeus_login() -@app.route('/logout') +@auth_bp.route('/logout') def logout(): if 'zeus_token' in session: session.pop('zeus_token', None) logout_user() - return redirect(url_for('home')) + return redirect(url_for('general_bp.home')) def before_request(): diff --git a/app/models.py b/app/models.py deleted file mode 100644 index 81388df..0000000 --- a/app/models.py +++ /dev/null @@ -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 diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..60576f2 --- /dev/null +++ b/app/models/__init__.py @@ -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 diff --git a/app/models/anonymous_user.py b/app/models/anonymous_user.py new file mode 100644 index 0000000..d82abd9 --- /dev/null +++ b/app/models/anonymous_user.py @@ -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 diff --git a/app/models/database.py b/app/models/database.py new file mode 100644 index 0000000..f0b13d6 --- /dev/null +++ b/app/models/database.py @@ -0,0 +1,3 @@ +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() diff --git a/app/models/location.py b/app/models/location.py new file mode 100644 index 0000000..aedd96f --- /dev/null +++ b/app/models/location.py @@ -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) diff --git a/app/models/order.py b/app/models/order.py new file mode 100644 index 0000000..d2c4560 --- /dev/null +++ b/app/models/order.py @@ -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 diff --git a/app/models/orderitem.py b/app/models/orderitem.py new file mode 100644 index 0000000..1631c2d --- /dev/null +++ b/app/models/orderitem.py @@ -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 diff --git a/app/models/product.py b/app/models/product.py new file mode 100644 index 0000000..1ffa122 --- /dev/null +++ b/app/models/product.py @@ -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') diff --git a/app/models/user.py b/app/models/user.py new file mode 100644 index 0000000..fa2c02d --- /dev/null +++ b/app/models/user.py @@ -0,0 +1,39 @@ +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): + try: + return unicode(self.id) # python 2 + except NameError: + return str(self.id) # python 3 + + def __repr__(self): + return '%s' % self.username diff --git a/app/notification.py b/app/notification.py new file mode 100644 index 0000000..3d2b821 --- /dev/null +++ b/app/notification.py @@ -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 = ' {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 = ' 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 diff --git a/app/passenger_wsgi.py b/app/passenger_wsgi.py index e8520f7..611b2e4 100644 --- a/app/passenger_wsgi.py +++ b/app/passenger_wsgi.py @@ -1,7 +1,9 @@ #!/usr/bin/env python -import sys import os +import sys + +from app import create_app INTERP = os.path.expanduser("~/env/bin/python") if sys.executable != INTERP: @@ -9,4 +11,8 @@ if sys.executable != INTERP: 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) diff --git a/app/templates/errors/401.html b/app/templates/errors/401.html index 59d7815..ba80436 100644 --- a/app/templates/errors/401.html +++ b/app/templates/errors/401.html @@ -4,6 +4,6 @@

Unauthorized

You're not authorized to look to this page!

-

Go somewhere nice

+

Go somewhere nice

{% endblock %} \ No newline at end of file diff --git a/app/templates/errors/404.html b/app/templates/errors/404.html index 534a1e1..76437e2 100644 --- a/app/templates/errors/404.html +++ b/app/templates/errors/404.html @@ -4,6 +4,6 @@

Page Not Found

What you were looking for is just not there.

-

Go somewhere nice

+

Go somewhere nice

{% endblock %} \ No newline at end of file diff --git a/app/templates/layout.html b/app/templates/layout.html index 15d5212..e2af745 100644 --- a/app/templates/layout.html +++ b/app/templates/layout.html @@ -2,12 +2,12 @@ {% import "bootstrap/utils.html" as utils %} {% set navbar = [ - ('home', 'Home'), + ('general_bp.home', 'Home'), ('order_bp.orders', 'Orders'), - ('locations', 'Locations'), - ('map', 'Map'), - ('about', 'About'), - ('stats', 'Stats'), + ('general_bp.locations', 'Locations'), + ('general_bp.map', 'Map'), + ('general_bp.about', 'About'), + ('stats_blueprint.stats', 'Stats'), ] -%} {% if not current_user.is_anonymous() and current_user.is_admin() -%} {% set navbar = navbar + [('admin.index', 'Admin')] -%} @@ -43,7 +43,7 @@ Haldis - {{ active_page|capitalize }} - HALDIS + HALDIS diff --git a/app/templates/locations.html b/app/templates/locations.html index 7dbc072..b174404 100644 --- a/app/templates/locations.html +++ b/app/templates/locations.html @@ -14,7 +14,7 @@ {% for loc in locations -%} - {{ loc.name }} + {{ loc.name }} {{ loc.address }} diff --git a/app/templates/maps.html b/app/templates/maps.html index 0c43dd5..1aced1d 100644 --- a/app/templates/maps.html +++ b/app/templates/maps.html @@ -28,7 +28,7 @@ loc = { "address": "{{loc.address}}", "name": "{{loc.name}}", - "url": "{{ url_for('location', id=loc.id) }}" + "url": "{{ url_for('general_bp.location', id=loc.id) }}" }; locations.push(loc); diff --git a/app/templates/order.html b/app/templates/order.html index f217afe..8104e26 100644 --- a/app/templates/order.html +++ b/app/templates/order.html @@ -11,14 +11,14 @@

Order {{ order.id }}
{% if order.can_close(current_user.id) -%} - Close + Close {% endif %}{% if courier_or_admin %} - Edit + Edit {%- endif %}

courier: {{ order.courrier.username }} {% if order.courrier == None and not current_user.is_anonymous() %} - Volunteer + Volunteer {% endif %}
location: {{ order.location.name }}
@@ -34,7 +34,7 @@ {% if form -%}

Order:

-
+ Choose for me @@ -76,8 +76,8 @@ {{ item.get_name() }} {{ item.product.name }}{{ "*" if item.extra }} {{ item.product.price|euro }} - {% if courier_or_admin %}{% if not item.paid %} Pay {% else %} {% endif %}{% endif %} - {% if item.can_delete(order.id, current_user.id, session.get('anon_name', '')) -%}{%- endif %}
+ {% if courier_or_admin %}{% if not item.paid %} Pay {% else %} {% endif %}{% endif %} + {% if item.can_delete(order.id, current_user.id, session.get('anon_name', '')) -%}{%- endif %}
{%- endfor %} @@ -85,7 +85,7 @@

Ordered products: {{ order.items.count() }}

- + {% for key, value in order.group_by_product().items() -%}
{{ key }}: {{ value["count"] }} @@ -113,7 +113,7 @@ {{ key }} {{ value["total"]|euro }} {{ value["to_pay"]|euro }} - {% if courier_or_admin %}{% if not value["to_pay"] == 0 %} Pay {% else %} {% endif %}{% endif %} + {% if courier_or_admin %}{% if not value["to_pay"] == 0 %} Pay {% else %} {% endif %}{% endif %} {%- endfor %} diff --git a/app/templates/orders.html b/app/templates/orders.html index 573198a..9733b68 100644 --- a/app/templates/orders.html +++ b/app/templates/orders.html @@ -26,7 +26,7 @@

Create new order:

- + {{ form.csrf_token }}
{{ form.courrier_id.label(class='control-label') }}
diff --git a/app/utils.py b/app/utils.py index cd694e8..5a12464 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1,57 +1,6 @@ -from datetime import datetime -from flask import render_template - -from app import app -from login import login_manager - -__author__ = 'feliciaan' - -@app.template_filter('euro') -def euro(value): - result = '€ {:.2f}'.format(round(value/100,2)) +def euro_string(value): + """ + Convert cents to string formatted euro + """ + result = '€ {:.2f}'.format(round(value / 100, 2)) 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 \ No newline at end of file diff --git a/app/views/__init__.py b/app/views/__init__.py deleted file mode 100644 index dd8c04f..0000000 --- a/app/views/__init__.py +++ /dev/null @@ -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/') -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/') -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 + "
" - - return string diff --git a/app/views/general.py b/app/views/general.py new file mode 100644 index 0000000..1565ab0 --- /dev/null +++ b/app/views/general.py @@ -0,0 +1,90 @@ +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/') +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/') +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') + + +@general_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 + "
" + + return string diff --git a/app/views/order.py b/app/views/order.py index 580ab77..c1ab67e 100644 --- a/app/views/order.py +++ b/app/views/order.py @@ -1,18 +1,19 @@ __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 from datetime import datetime -from app import app, db -from models import Order, OrderItem, User -from forms import OrderItemForm, OrderForm, AnonOrderItemForm +# from flask import current_app as app +from flask import (Blueprint, abort, flash, redirect, render_template, request, + 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.route('/') def orders(form=None): if form is None and not current_user.is_anonymous(): @@ -35,7 +36,7 @@ def order_create(): db.session.add(order) db.session.commit() 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) @@ -48,13 +49,19 @@ def order(id, form=None): flash('Please login to see this order.', 'info') abort(401) 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) if order.stoptime and order.stoptime < datetime.now(): form = None 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]) - 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('//items') def items_showcase(id, form=None): @@ -66,11 +73,13 @@ def items_showcase(id, form=None): abort(401) return render_template('order_items.html', order=order) + @order_bp.route('//edit', methods=['GET', 'POST']) @login_required def order_edit(id): 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) if order is None: abort(404) @@ -79,7 +88,7 @@ def order_edit(id): if orderForm.validate_on_submit(): orderForm.populate_obj(order) 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) @@ -93,7 +102,8 @@ def order_item_create(id): if current_user.is_anonymous() and not current_order.public: flash('Please login to see this order.', 'info') 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) if form.validate_on_submit(): item = OrderItem() @@ -106,7 +116,7 @@ def order_item_create(id): db.session.add(item) db.session.commit() 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) @@ -118,8 +128,9 @@ def item_paid(order_id, item_id): if item.order.courrier_id == id or current_user.admin: item.paid = True db.session.commit() - flash('Paid %s by %s' % (item.product.name, item.get_name()), 'success') - return redirect(url_for('.order', id=order_id)) + flash('Paid %s by %s' % (item.product.name, item.get_name()), + 'success') + return redirect(url_for('order_bp.order', id=order_id)) abort(404) @@ -129,9 +140,11 @@ def items_user_paid(order_id, user_name): user = User.query.filter(User.username == user_name).first() items = [] 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: - 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() for item in items: print(item) @@ -139,8 +152,9 @@ def items_user_paid(order_id, user_name): for item in items: item.paid = True db.session.commit() - flash('Paid %d items for %s' % (items.count(), item.get_name()), 'success') - return redirect(url_for('.order', id=order_id)) + flash('Paid %d items for %s' % (items.count(), item.get_name()), + 'success') + return redirect(url_for('order_bp.order', id=order_id)) abort(404) @@ -156,7 +170,7 @@ def delete_item(order_id, item_id): db.session.delete(item) db.session.commit() 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) @@ -172,7 +186,7 @@ def volunteer(id): flash("Thank you for volunteering!") else: flash("Volunteering not possible!") - return redirect(url_for('.order', id=id)) + return redirect(url_for('order_bp.order', id=id)) @order_bp.route('//close') @@ -190,9 +204,7 @@ def close_order(id): if courrier is not None: order.courrier_id = courrier.id db.session.commit() - return redirect(url_for('.order', id=id)) - -app.register_blueprint(order_bp, url_prefix='/order') + return redirect(url_for('order_bp.order', id=id)) def select_user(items): @@ -216,52 +228,12 @@ def select_user(items): def get_orders(expression=None): orders = [] 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(): orders = Order.query.filter(expression).all() else: - orders = Order.query.filter((expression & (Order.public == True))).all() + orders = Order.query.filter( + (expression & (Order.public == True))).all() return orders - - -def post_order_to_webhook(order_item): - message = '' - if order_item.courrier is not None: - message = ' {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 = ' 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 diff --git a/app/views/stats.py b/app/views/stats.py index 80a90fb..6b97096 100644 --- a/app/views/stats.py +++ b/app/views/stats.py @@ -1,10 +1,13 @@ +from flask import Blueprint +from flask import current_app as app from flask import render_template -from fatmodels import FatLocation, FatOrder, FatOrderItem, FatUser, FatProduct -from app import app +from fatmodels import FatLocation, FatOrder, FatOrderItem, FatProduct, FatUser + +stats_blueprint = Blueprint('stats_blueprint', __name__) -@app.route('/stats/') +@stats_blueprint.route('/') def stats(): data = { 'amount': { diff --git a/app/zeus.py b/app/zeus.py index 5376da7..4086f06 100644 --- a/app/zeus.py +++ b/app/zeus.py @@ -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_oauthlib.client import OAuth, OAuthException -import json -import requests +from flask_oauthlib.client import OAuthException, OAuth +from models import User, db -from app import app, db -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/' -) +oauth_bp = Blueprint("oauth_bp", __name__) 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(): - resp = zeus.authorized_response() + resp = current_app.zeus.authorized_response() if resp is None: return 'Access denied: reason=%s error=%s' % ( - request.args['error'], - request.args['error_description'] - ) + request.args['error'], request.args['error_description']) if isinstance(resp, OAuthException): return 'Access denied: %s' % resp.message + '
' + str(resp.data) session['zeus_token'] = (resp['access_token'], '') - me = zeus.get('current_user/') + me = current_app.zeus.get('current_user/') username = me.data.get('username', '').lower() user = User.query.filter_by(username=username).first() @@ -49,17 +34,31 @@ def authorized(): return login_and_redirect_user(user) 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 -def get_zeus_oauth_token(): - return session.get('zeus_token') + 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/') + + @zeus.tokengetter + def get_zeus_oauth_token(): + return session.get('zeus_token') + + return zeus def login_and_redirect_user(user): login_user(user) - return redirect(url_for("home")) + return redirect(url_for("general_bp.home")) def create_user(username):