From 51edd1bdc1f802fd09d37b727ce83d1726a597e6 Mon Sep 17 00:00:00 2001 From: Jan-Pieter Baert Date: Sat, 7 Sep 2019 15:05:24 +0200 Subject: [PATCH 1/6] Add typing to database files --- app/database/add_admins.py | 2 +- app/database/add_fitchen.py | 2 +- app/database/add_oceans_garden.py | 6 +++--- app/database/add_primadonna.py | 4 ++-- app/database/add_simpizza.py | 2 +- app/database/add_stefanos.py | 6 +++--- app/database/create_database.py | 24 ++++++++++++------------ 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/app/database/add_admins.py b/app/database/add_admins.py index c3dff1f..cb385c7 100644 --- a/app/database/add_admins.py +++ b/app/database/add_admins.py @@ -2,7 +2,7 @@ from app import db from models import User -def add(): +def add() -> None: feli = User() feli.configure("feliciaan", True, 0) db.session.add(feli) diff --git a/app/database/add_fitchen.py b/app/database/add_fitchen.py index 8e7ff9b..ab05012 100644 --- a/app/database/add_fitchen.py +++ b/app/database/add_fitchen.py @@ -23,7 +23,7 @@ menuitems = [ pricedict = {"Small": 799, "Medium": 999, "Large": 1199} -def add(): +def add() -> None: simpizza = Location() simpizza.configure("Fitchen", "?", "?", "https://www.fitchen.be/") db.session.add(simpizza) diff --git a/app/database/add_oceans_garden.py b/app/database/add_oceans_garden.py index 0954fc5..8c2b87a 100644 --- a/app/database/add_oceans_garden.py +++ b/app/database/add_oceans_garden.py @@ -29,7 +29,7 @@ specials = [ ] -def add(): +def add() -> None: chinees = Location() chinees.configure( "Oceans's Garden", @@ -39,12 +39,12 @@ def add(): ) db.session.add(chinees) - def chinees_create_entry(name): + def chinees_create_entry(name) -> None: entry = Product() entry.configure(chinees, name, 550) db.session.add(entry) - def chinees_create_regulat(zetmeel, vlees="", saus=""): + def chinees_create_regulat(zetmeel, vlees="", saus="") -> None: chinees_create_entry("{} {} {}".format(zetmeel, vlees, saus).rstrip()) for z, v, s in product(zetmelen, vlezen, sauzen): diff --git a/app/database/add_primadonna.py b/app/database/add_primadonna.py index 28aa564..3140742 100644 --- a/app/database/add_primadonna.py +++ b/app/database/add_primadonna.py @@ -46,7 +46,7 @@ pizzasTA = { } -def addTA(): +def addTA() -> None: primadonna_takeaway = Location() primadonna_takeaway.configure( "Primadonna (takeaway laten bezorgen)", @@ -101,7 +101,7 @@ pizzasAfhalen = { } -def addAfhalen(): +def addAfhalen() -> None: primadonna_afhalen = Location() primadonna_afhalen.configure( "Primadonna (bellen en afhalen)", diff --git a/app/database/add_simpizza.py b/app/database/add_simpizza.py index e653195..5f5f169 100644 --- a/app/database/add_simpizza.py +++ b/app/database/add_simpizza.py @@ -33,7 +33,7 @@ pizzas = [ ] -def add(): +def add() -> None: simpizza = Location() simpizza.configure( "Sim-pizza", diff --git a/app/database/add_stefanos.py b/app/database/add_stefanos.py index 35f7a38..b3b8f45 100644 --- a/app/database/add_stefanos.py +++ b/app/database/add_stefanos.py @@ -11,7 +11,7 @@ bickies = { "Bicky Veggie": 350, } -saus = { +sauskes = { "american": 70, "andalouse": 70, "bicky saus": 70, @@ -100,7 +100,7 @@ friet = {"Klein pak": 200, "Midden pak": 250, "Groot pak": 300} data = [special_bickies, specials, vlezekes, friet] -def add(): +def add() -> None: stefanos = Location() stefanos.configure( "Stefano's Place", @@ -127,7 +127,7 @@ def add(): db.session.add(item) # saus in een potteke bestellen is 10 cent extra - for name, price in saus.items(): + for name, price in sauskes.items(): saus = Product() saus.configure(stefanos, name, price) db.session.add(saus) diff --git a/app/database/create_database.py b/app/database/create_database.py index 244e212..59178cd 100644 --- a/app/database/create_database.py +++ b/app/database/create_database.py @@ -15,23 +15,23 @@ no = ["no", "n", "N"] # Commit all the things -def commit(): +def commit() -> None: db.session.commit() print("Committing successful") -def check_if_overwrite(): +def check_if_overwrite() -> bool: answer = input("Do you want to overwrite the previous database? (y/N) ") return answer in yes -def add_all(): +def add_all() -> None: for entry_set in entry_sets.keys(): print("Adding {}.".format(entry_set)) entry_sets[entry_set]() -def recreate_from_scratch(): +def recreate_from_scratch() -> None: confirmation = "Are you very very sure? (Will delete previous entries!) (y/N) " check = "I acknowledge any repercussions!" if input(confirmation) in yes and input("Type: '{}' ".format(check)) == check: @@ -41,10 +41,10 @@ def recreate_from_scratch(): add_to_current() -def add_to_current(): +def add_to_current() -> None: available = [entry_set for entry_set in entry_sets] - def add_numbers(): + def add_numbers() -> str: return " ".join( ["{}({}), ".format(loc, i) for i, loc in enumerate(available)] ).rstrip(", ") @@ -59,17 +59,17 @@ def add_to_current(): available = [] elif answer == "C": pass - elif answer in [str(x) for x in range(len(available))]: - answer = int(answer) - print("Adding {}.".format(available[answer])) - entry_sets[str(available[answer])]() - del available[answer] + elif answer.isnumeric() and answer in [str(x) for x in range(len(available))]: + answer_index = int(answer) + print("Adding {}.".format(available[answer_index])) + entry_sets[str(available[answer_index])]() + del available[answer_index] else: print("Not a valid answer.") print("Thank you for adding, come again!") -def init(): +def init() -> None: print("Database modification script!") print("=============================\n\n") if check_if_overwrite(): From 27cb10f74539bf676604a60d926ea289ca9782c1 Mon Sep 17 00:00:00 2001 From: Jan-Pieter Baert Date: Sun, 8 Sep 2019 00:41:50 +0200 Subject: [PATCH 2/6] Add typing to model files --- app/models/anonymous_user.py | 10 +++++----- app/models/location.py | 7 +++++-- app/models/order.py | 22 ++++++++++++---------- app/models/orderitem.py | 10 ++++++---- app/models/product.py | 9 ++++++--- app/models/user.py | 14 +++++++------- 6 files changed, 41 insertions(+), 31 deletions(-) diff --git a/app/models/anonymous_user.py b/app/models/anonymous_user.py index d82abd9..76f99f7 100644 --- a/app/models/anonymous_user.py +++ b/app/models/anonymous_user.py @@ -1,17 +1,17 @@ class AnonymouseUser: id = None - def is_active(self): + def is_active(self) -> bool: return False - def is_authenticated(self): + def is_authenticated(self) -> bool: return False - def is_anonymous(self): + def is_anonymous(self) -> bool: return True - def is_admin(self): + def is_admin(self) -> bool: return False - def get_id(self): + def get_id(self) -> None: return None diff --git a/app/models/location.py b/app/models/location.py index 73946e7..8fc3a92 100644 --- a/app/models/location.py +++ b/app/models/location.py @@ -1,3 +1,5 @@ +import typing + from models import db @@ -10,11 +12,12 @@ class Location(db.Model): products = db.relationship("Product", backref="location", lazy="dynamic") orders = db.relationship("Order", backref="location", lazy="dynamic") - def configure(self, name, address, telephone, website): + def configure(self, name: str, address: str, + telephone: typing.Optional[str], website: str) -> None: self.name = name self.address = address self.website = website self.telephone = telephone - def __repr__(self): + def __repr__(self) -> str: return "%s" % (self.name) diff --git a/app/models/order.py b/app/models/order.py index 0c804ec..b9c2b1a 100644 --- a/app/models/order.py +++ b/app/models/order.py @@ -1,6 +1,8 @@ +import typing from datetime import datetime from .database import db +from .location import Location from .user import User @@ -13,34 +15,34 @@ class Order(db.Model): public = db.Column(db.Boolean, default=True) items = db.relationship("OrderItem", backref="order", lazy="dynamic") - def configure(self, courrier, location, starttime, stoptime): + def configure(self, courrier: User, location: Location, + starttime: db.DateTime, stoptime: db.DateTime) -> None: self.courrier = courrier self.location = location self.starttime = starttime self.stoptime = stoptime - def __repr__(self): + def __repr__(self) -> str: 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() + def group_by_user(self) -> typing.Dict[str, typing.Any]: + group: typing.Dict[str, typing.Any] = 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["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() + def group_by_product(self) -> typing.Dict[str, typing.Any]: + group: typing.Dict[str, typing.Any] = dict() for item in self.items: product = group.get(item.product.name, dict()) product["count"] = product.get("count", 0) + 1 @@ -50,7 +52,7 @@ class Order(db.Model): return group - def can_close(self, user_id): + def can_close(self, user_id: int) -> bool: if self.stoptime and self.stoptime < datetime.now(): return False user = None diff --git a/app/models/orderitem.py b/app/models/orderitem.py index 0ea982e..d699a90 100644 --- a/app/models/orderitem.py +++ b/app/models/orderitem.py @@ -1,6 +1,8 @@ from datetime import datetime from .database import db +from .order import Order +from .product import Product from .user import User @@ -17,17 +19,17 @@ class OrderItem(db.Model): extra = db.Column(db.String(254), nullable=True) name = db.Column(db.String(120)) - def configure(self, user, order, product): + def configure(self, user: User, order: Order, product: Product) -> None: self.user = user self.order = order self.product = product - def get_name(self): + def get_name(self) -> str: if self.user_id is not None and self.user_id > 0: return self.user.username return self.name - def __repr__(self): + def __repr__(self) -> str: product_name = None if self.product: product_name = self.product.name @@ -37,7 +39,7 @@ class OrderItem(db.Model): product_name or "None", ) - def can_delete(self, order_id, user_id, name): + def can_delete(self, order_id: int, user_id: int, name: str) -> bool: if int(self.order_id) != int(order_id): return False if self.order.stoptime and self.order.stoptime < datetime.now(): diff --git a/app/models/product.py b/app/models/product.py index 21e4008..909aade 100644 --- a/app/models/product.py +++ b/app/models/product.py @@ -1,19 +1,22 @@ from models import db +from .location import Location + 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") + orderItems = db.relationship("OrderItem", + backref="product", lazy="dynamic") - def configure(self, location, name, price): + def configure(self, location: Location, name: str, price: int) -> None: self.location = location self.name = name self.price = price - def __repr__(self): + def __repr__(self) -> str: return "%s (€%d)from %s" % ( self.name, self.price / 100, diff --git a/app/models/user.py b/app/models/user.py index 93362d1..4ed2fe8 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -14,25 +14,25 @@ class User(db.Model): ) orderItems = db.relationship("OrderItem", backref="user", lazy="dynamic") - def configure(self, username, admin, bias): + def configure(self, username: str, admin: bool, bias: int) -> None: self.username = username self.admin = admin self.bias = bias - def is_authenticated(self): + def is_authenticated(self) -> bool: return True - def is_active(self): + def is_active(self) -> bool: return True - def is_admin(self): + def is_admin(self) -> bool: return self.admin - def is_anonymous(self): + def is_anonymous(self) -> bool: return False - def get_id(self): + def get_id(self) -> str: return str(self.id) - def __repr__(self): + def __repr__(self) -> str: return "%s" % self.username From b7fa92a22ab3c6374f34b152bc2f4f5d07136beb Mon Sep 17 00:00:00 2001 From: Jan-Pieter Baert Date: Sun, 8 Sep 2019 01:34:16 +0200 Subject: [PATCH 3/6] Add typing to view files --- app/views/debug.py | 2 +- app/views/general.py | 18 ++++++------ app/views/order.py | 67 +++++++++++++++++++++++++++----------------- app/views/stats.py | 2 +- 4 files changed, 53 insertions(+), 36 deletions(-) diff --git a/app/views/debug.py b/app/views/debug.py index 84dd545..96177e7 100644 --- a/app/views/debug.py +++ b/app/views/debug.py @@ -8,7 +8,7 @@ debug_bp = Blueprint("debug_bp", __name__) @debug_bp.route("/routes") @login_required -def list_routes(): +def list_routes() -> str: import urllib output = [] diff --git a/app/views/general.py b/app/views/general.py index d1ade12..6386109 100644 --- a/app/views/general.py +++ b/app/views/general.py @@ -15,7 +15,7 @@ general_bp = Blueprint("general_bp", __name__) @general_bp.route("/") -def home(): +def home() -> str: prev_day = datetime.now() - timedelta(days=1) recently_closed = get_orders( ((Order.stoptime > prev_day) & (Order.stoptime < datetime.now())) @@ -27,19 +27,19 @@ def home(): @general_bp.route("/map", defaults={"id": None}) @general_bp.route("/map/") -def map(id): +def map(id) -> str: locs = Location.query.order_by("name") return render_template("maps.html", locations=locs) @general_bp.route("/location") -def locations(): +def locations() -> str: locs = Location.query.order_by("name") return render_template("locations.html", locations=locs) @general_bp.route("/location/") -def location(id): +def location(id) -> str: loc = Location.query.filter(Location.id == id).first() if loc is None: abort(404) @@ -47,27 +47,27 @@ def location(id): @general_bp.route("/about/") -def about(): +def about() -> str: return render_template("about.html") @general_bp.route("/profile/") @login_required -def profile(): +def profile() -> str: return render_template("profile.html") @general_bp.route("/favicon.ico") -def favicon(): +def favicon() -> str: if len(get_orders((Order.stoptime > datetime.now()))) == 0: return send_from_directory( - os.path.join(app.root_path, "static"), + os.path.join(str(app.root_path), "static"), "favicon.ico", mimetype="image/x-icon", ) else: return send_from_directory( - os.path.join(app.root_path, "static"), + os.path.join(str(app.root_path), "static"), "favicon_orange.ico", mimetype="image/x-icon", ) diff --git a/app/views/order.py b/app/views/order.py index b424692..536ac14 100644 --- a/app/views/order.py +++ b/app/views/order.py @@ -1,6 +1,7 @@ +from werkzeug.wrappers import Response import random from datetime import datetime - +import typing # from flask import current_app as app from flask import ( Blueprint, @@ -11,7 +12,9 @@ from flask import ( request, session, url_for, + wrappers, ) +import werkzeug from flask_login import current_user, login_required from forms import AnonOrderItemForm, OrderForm, OrderItemForm @@ -22,7 +25,7 @@ order_bp = Blueprint("order_bp", "order") @order_bp.route("/") -def orders(form=None): +def orders(form: OrderForm = None) -> str: if form is None and not current_user.is_anonymous(): form = OrderForm() location_id = request.args.get("location_id") @@ -34,7 +37,7 @@ def orders(form=None): @order_bp.route("/create", methods=["POST"]) @login_required -def order_create(): +def order_create() -> typing.Union[str, Response]: orderForm = OrderForm() orderForm.populate() if orderForm.validate_on_submit(): @@ -48,7 +51,7 @@ def order_create(): @order_bp.route("/") -def order(id, form=None): +def order(id: int, form: OrderForm = None) -> str: order = Order.query.filter(Order.id == id).first() if order is None: abort(404) @@ -56,19 +59,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): +def items_showcase(id: int, form: OrderForm = None) -> str: order = Order.query.filter(Order.id == id).first() if order is None: abort(404) @@ -80,9 +83,10 @@ def items_showcase(id, form=None): @order_bp.route("//edit", methods=["GET", "POST"]) @login_required -def order_edit(id): +def order_edit(id: int) -> typing.Union[str, Response]: 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) @@ -96,7 +100,9 @@ def order_edit(id): @order_bp.route("//create", methods=["POST"]) -def order_item_create(id): +def order_item_create(id: int) -> typing.Any: + # type is 'typing.Union[str, Response]', but this errors due to + # https://github.com/python/mypy/issues/7187 current_order = Order.query.filter(Order.id == id).first() if current_order is None: abort(404) @@ -105,7 +111,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() @@ -124,22 +131,24 @@ def order_item_create(id): @order_bp.route("///paid") @login_required -def item_paid(order_id, item_id): +def item_paid(order_id: int, item_id: int) -> typing.Optional[Response]: item = OrderItem.query.filter(OrderItem.id == item_id).first() id = current_user.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") + flash("Paid %s by %s" % (item.product.name, item.get_name()), + "success") return redirect(url_for("order_bp.order", id=order_id)) abort(404) @order_bp.route("///user_paid") @login_required -def items_user_paid(order_id, user_name): +def items_user_paid(order_id: int, + user_name: str) -> typing.Optional[Response]: user = User.query.filter(User.username == user_name).first() - items = [] + items: typing.List[OrderItem] = [] if user: items = OrderItem.query.filter( (OrderItem.user_id == user.id) & (OrderItem.order_id == order_id) @@ -155,13 +164,16 @@ 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") + flash("Paid %d items for %s" % + (len(items), item.get_name()), "success") return redirect(url_for("order_bp.order", id=order_id)) abort(404) @order_bp.route("///delete") -def delete_item(order_id, item_id): +def delete_item(order_id: int, item_id: int) -> typing.Any: + # type is 'typing.Optional[Response]', but this errors due to + # https://github.com/python/mypy/issues/7187 item = OrderItem.query.filter(OrderItem.id == item_id).first() id = None if not current_user.is_anonymous(): @@ -178,7 +190,7 @@ def delete_item(order_id, item_id): @order_bp.route("//volunteer") @login_required -def volunteer(id): +def volunteer(id: int) -> Response: order = Order.query.filter(Order.id == id).first() if order is None: abort(404) @@ -193,7 +205,7 @@ def volunteer(id): @order_bp.route("//close") @login_required -def close_order(id): +def close_order(id: int) -> typing.Optional[Response]: order = Order.query.filter(Order.id == id).first() if order is None: abort(404) @@ -208,9 +220,13 @@ def close_order(id): order.courrier_id = courrier.id db.session.commit() return redirect(url_for("order_bp.order", id=id)) + # The line below is to make sure mypy doesn't say + # "Missing return statement" + # https://github.com/python/mypy/issues/4223 + return None -def select_user(items): +def select_user(items) -> typing.Optional[User]: user = None # remove non users items = [i for i in items if i.user_id] @@ -228,8 +244,8 @@ def select_user(items): return user -def get_orders(expression=None): - orders = [] +def get_orders(expression=None) -> typing.List[Order]: + orders: typing.List[OrderForm] = [] if expression is None: expression = (datetime.now() > Order.starttime) & ( Order.stoptime > datetime.now() @@ -237,5 +253,6 @@ def get_orders(expression=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 diff --git a/app/views/stats.py b/app/views/stats.py index 31a1dde..73ad1b1 100644 --- a/app/views/stats.py +++ b/app/views/stats.py @@ -8,7 +8,7 @@ stats_blueprint = Blueprint("stats_blueprint", __name__) @stats_blueprint.route("/") -def stats(): +def stats() -> str: data = { "amount": { "orders": FatOrder.amount(), From 15794b06b761ee4cf6287d127e111573e0b88ba6 Mon Sep 17 00:00:00 2001 From: Jan-Pieter Baert Date: Sun, 8 Sep 2019 01:58:21 +0200 Subject: [PATCH 4/6] Add typing to general app files --- app/admin.py | 6 ++++-- app/app.py | 21 +++++++++++---------- app/fatmodels.py | 4 +++- app/forms.py | 11 ++++++----- app/login.py | 12 ++++++------ app/notification.py | 10 +++++----- app/utils.py | 2 +- app/zeus.py | 16 +++++++++++----- 8 files changed, 47 insertions(+), 35 deletions(-) diff --git a/app/admin.py b/app/admin.py index 819b99d..7712358 100644 --- a/app/admin.py +++ b/app/admin.py @@ -1,12 +1,14 @@ import flask_login as login +from flask import Flask from flask_admin import Admin from flask_admin.contrib.sqla import ModelView +from flask_sqlalchemy import SQLAlchemy from models import Location, Order, OrderItem, Product, User class ModelBaseView(ModelView): - def is_accessible(self): + def is_accessible(self) -> bool: if login.current_user.is_anonymous(): return False @@ -29,7 +31,7 @@ class LocationAdminModel(ModelBaseView): form_columns = ("name", "address", "website", "telephone") -def init_admin(app, db): +def init_admin(app: Flask, db: SQLAlchemy) -> None: admin = Admin(app, name="Haldis", url="/admin", template_mode="bootstrap3") admin.add_view(UserAdminModel(User, db.session)) diff --git a/app/app.py b/app/app.py index e1674d5..8e6c063 100644 --- a/app/app.py +++ b/app/app.py @@ -1,4 +1,5 @@ import logging +import typing from datetime import datetime from logging.handlers import TimedRotatingFileHandler @@ -19,7 +20,7 @@ from utils import euro_string from zeus import init_oauth -def create_app(): +def create_app() -> Manager: app = Flask(__name__) # Load the config file @@ -34,7 +35,7 @@ def create_app(): return manager -def register_plugins(app, debug: bool): +def register_plugins(app: Flask, debug: bool) -> Manager: # Register Airbrake and enable the logrotation if not app.debug: timedFileHandler = TimedRotatingFileHandler( @@ -96,17 +97,17 @@ def register_plugins(app, debug: bool): return manager -def add_handlers(app): +def add_handlers(app: Flask) -> None: @app.errorhandler(404) - def handle404(e): + def handle404(e) -> typing.Tuple[str, int]: return render_template("errors/404.html"), 404 @app.errorhandler(401) - def handle401(e): + def handle401(e) -> typing.Tuple[str, int]: return render_template("errors/401.html"), 401 -def add_routes(application): +def add_routes(application: Flask) -> None: # import views # TODO convert to blueprint # import views.stats # TODO convert to blueprint @@ -127,9 +128,9 @@ def add_routes(application): application.register_blueprint(debug_bp, url_prefix="/debug") -def add_template_filters(app): +def add_template_filters(app: Flask) -> None: @app.template_filter("countdown") - def countdown(value, only_positive=True, show_text=True): + def countdown(value, only_positive: bool = True, show_text: bool = True) -> str: delta = value - datetime.now() if delta.total_seconds() < 0 and only_positive: return "closed" @@ -141,11 +142,11 @@ def add_template_filters(app): return time @app.template_filter("year") - def current_year(value): + def current_year(value: typing.Any) -> str: return str(datetime.now().year) @app.template_filter("euro") - def euro(value): + def euro(value: int) -> None: euro_string(value) diff --git a/app/fatmodels.py b/app/fatmodels.py index f4c4c53..79eac01 100644 --- a/app/fatmodels.py +++ b/app/fatmodels.py @@ -1,3 +1,5 @@ +import typing + from sqlalchemy.sql import desc, func from models import Location, Order, OrderItem, Product, User @@ -42,7 +44,7 @@ class FatOrderItem(OrderItem, FatModel): class FatProduct(Product, FatModel): @classmethod - def top4(cls): + def top4(cls) -> None: top4 = ( OrderItem.query.join(Product) .join(Location) diff --git a/app/forms.py b/app/forms.py index a41c188..4003944 100644 --- a/app/forms.py +++ b/app/forms.py @@ -3,7 +3,8 @@ 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 DateTimeField, SelectField, StringField, SubmitField, validators +from wtforms import (DateTimeField, SelectField, StringField, SubmitField, + validators) from models import Location, User from utils import euro_string @@ -20,7 +21,7 @@ class OrderForm(Form): stoptime = DateTimeField("Stoptime", format="%d-%m-%Y %H:%M") submit_button = SubmitField("Submit") - def populate(self): + def populate(self) -> None: if current_user.is_admin(): self.courrier_id.choices = [(0, None)] + [ (u.id, u.username) for u in User.query.order_by("username") @@ -42,7 +43,7 @@ class OrderItemForm(Form): extra = StringField("Extra") submit_button = SubmitField("Submit") - def populate(self, location): + def populate(self, location: Location) -> None: self.product_id.choices = [ (i.id, (i.name + ": " + euro_string(i.price))) for i in location.products ] @@ -51,12 +52,12 @@ class OrderItemForm(Form): class AnonOrderItemForm(OrderItemForm): name = StringField("Name", validators=[validators.required()]) - def populate(self, location): + def populate(self, location: Location) -> None: OrderItemForm.populate(self, location) if self.name.data is None: self.name.data = session.get("anon_name", None) - def validate(self): + def validate(self) -> bool: rv = OrderForm.validate(self) if not rv: return False diff --git a/app/login.py b/app/login.py index 27c2e8a..c50b245 100644 --- a/app/login.py +++ b/app/login.py @@ -1,6 +1,6 @@ -from flask import abort, Blueprint -from flask import redirect, session, url_for +from flask import Blueprint, abort, redirect, session, url_for from flask_login import current_user, logout_user +from werkzeug.wrappers import Response from models import User from zeus import zeus_login @@ -8,9 +8,9 @@ from zeus import zeus_login auth_bp = Blueprint("auth_bp", __name__) -def init_login(app): +def init_login(app) -> None: @app.login_manager.user_loader - def load_user(userid): + def load_user(userid) -> User: return User.query.filter_by(id=userid).first() @@ -20,13 +20,13 @@ def login(): @auth_bp.route("/logout") -def logout(): +def logout() -> Response: if "zeus_token" in session: session.pop("zeus_token", None) logout_user() return redirect(url_for("general_bp.home")) -def before_request(): +def before_request() -> None: if current_user.is_anonymous() or not current_user.is_allowed(): abort(401) diff --git a/app/notification.py b/app/notification.py index 2fc8cbc..be20c3c 100644 --- a/app/notification.py +++ b/app/notification.py @@ -7,7 +7,7 @@ from flask import current_app as app from flask import url_for -def post_order_to_webhook(order_item): +def post_order_to_webhook(order_item) -> None: message = "" if order_item.courrier is not None: message = " {3} is going to {1}, order <{0}|here>! Deadline in {2} minutes!".format( @@ -27,14 +27,14 @@ def post_order_to_webhook(order_item): class WebhookSenderThread(Thread): - def __init__(self, message): + def __init__(self, message: str) -> None: super(WebhookSenderThread, self).__init__() self.message = message - def run(self): + def run(self) -> None: self.slack_webhook() - def slack_webhook(self): + def slack_webhook(self) -> None: js = json.dumps({"text": self.message}) url = app.config["SLACK_WEBHOOK"] if len(url) > 0: @@ -43,7 +43,7 @@ class WebhookSenderThread(Thread): app.logger.info(str(js)) -def remaining_minutes(value): +def remaining_minutes(value) -> str: delta = value - datetime.now() if delta.total_seconds() < 0: return "0" diff --git a/app/utils.py b/app/utils.py index 0c456ce..1584117 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1,4 +1,4 @@ -def euro_string(value): +def euro_string(value: int) -> str: """ Convert cents to string formatted euro """ diff --git a/app/zeus.py b/app/zeus.py index b56f70a..8ca77d1 100644 --- a/app/zeus.py +++ b/app/zeus.py @@ -1,6 +1,10 @@ -from flask import current_app, flash, redirect, request, session, url_for, Blueprint +import typing + +from flask import (Blueprint, current_app, flash, redirect, request, session, + url_for) from flask_login import login_user -from flask_oauthlib.client import OAuthException, OAuth +from flask_oauthlib.client import OAuth, OAuthException +from werkzeug.wrappers import Response from models import User, db @@ -14,7 +18,9 @@ def zeus_login(): @oauth_bp.route("/login/zeus/authorized") -def authorized(): +def authorized() -> typing.Any: + # type is 'typing.Union[str, Response]', but this errors due to + # https://github.com/python/mypy/issues/7187 resp = current_app.zeus.authorized_response() if resp is None: return "Access denied: reason=%s error=%s" % ( @@ -60,12 +66,12 @@ def init_oauth(app): return zeus -def login_and_redirect_user(user): +def login_and_redirect_user(user) -> Response: login_user(user) return redirect(url_for("general_bp.home")) -def create_user(username): +def create_user(username) -> User: user = User() user.configure(username, False, 1) db.session.add(user) From c64e4bd998f94e441b6ce9b844088ca076d36863 Mon Sep 17 00:00:00 2001 From: Jan-Pieter Baert Date: Sun, 8 Sep 2019 02:02:16 +0200 Subject: [PATCH 5/6] Run black and isort on all code --- app/database/add_fitchen.py | 3 +- app/database/add_oceans_garden.py | 4 +-- app/database/add_primadonna.py | 2 +- app/database/add_simpizza.py | 3 +- app/database/add_stefanos.py | 2 +- app/database/create_database.py | 7 ++-- app/migrations/env.py | 16 +++++---- app/migrations/versions/150252c1cdb1_.py | 2 +- app/models/location.py | 5 +-- app/models/order.py | 14 +++++--- app/models/product.py | 3 +- app/views/general.py | 1 - app/views/order.py | 46 +++++++++--------------- 13 files changed, 51 insertions(+), 57 deletions(-) diff --git a/app/database/add_fitchen.py b/app/database/add_fitchen.py index ab05012..b9a885e 100644 --- a/app/database/add_fitchen.py +++ b/app/database/add_fitchen.py @@ -1,6 +1,5 @@ -from models import Location, Product from app import db - +from models import Location, Product menuitems = [ "Spicy Chicken", diff --git a/app/database/add_oceans_garden.py b/app/database/add_oceans_garden.py index 8c2b87a..fb293ef 100644 --- a/app/database/add_oceans_garden.py +++ b/app/database/add_oceans_garden.py @@ -1,7 +1,7 @@ -from models import Location, Product -from app import db from itertools import product +from app import db +from models import Location, Product zetmelen = ["Nasi", "Bami"] vlezen = ["Rundsvlees", "Varkensvlees", "Kippenstukkjes"] diff --git a/app/database/add_primadonna.py b/app/database/add_primadonna.py index 3140742..5bbb3c4 100644 --- a/app/database/add_primadonna.py +++ b/app/database/add_primadonna.py @@ -1,5 +1,5 @@ -from models import Location, Product from app import db +from models import Location, Product def add(): diff --git a/app/database/add_simpizza.py b/app/database/add_simpizza.py index 5f5f169..77a6c0f 100644 --- a/app/database/add_simpizza.py +++ b/app/database/add_simpizza.py @@ -1,6 +1,5 @@ -from models import Location, Product from app import db - +from models import Location, Product pizzas = [ "Bolognese de luxe", diff --git a/app/database/add_stefanos.py b/app/database/add_stefanos.py index b3b8f45..9868d29 100644 --- a/app/database/add_stefanos.py +++ b/app/database/add_stefanos.py @@ -1,5 +1,5 @@ -from models import Location, Product from app import db +from models import Location, Product bickies = { "Bicky Burger Original": 330, diff --git a/app/database/create_database.py b/app/database/create_database.py index 59178cd..cdadd2b 100644 --- a/app/database/create_database.py +++ b/app/database/create_database.py @@ -1,6 +1,9 @@ +import add_admins +import add_fitchen +import add_oceans_garden +import add_primadonna +import add_simpizza from app import db -import add_oceans_garden, add_admins, add_simpizza, add_primadonna, add_fitchen - entry_sets = { "Admins": add_admins.add, diff --git a/app/migrations/env.py b/app/migrations/env.py index 6b2907b..f416a43 100644 --- a/app/migrations/env.py +++ b/app/migrations/env.py @@ -1,8 +1,15 @@ from __future__ import with_statement -from alembic import context -from sqlalchemy import engine_from_config, pool + from logging.config import fileConfig +from alembic import context +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +from flask import current_app +from sqlalchemy import engine_from_config, pool + # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config @@ -11,11 +18,6 @@ config = context.config # This line sets up loggers basically. fileConfig(config.config_file_name) -# add your model's MetaData object here -# for 'autogenerate' support -# from myapp import mymodel -# target_metadata = mymodel.Base.metadata -from flask import current_app config.set_main_option( "sqlalchemy.url", current_app.config.get("SQLALCHEMY_DATABASE_URI") diff --git a/app/migrations/versions/150252c1cdb1_.py b/app/migrations/versions/150252c1cdb1_.py index 62a6099..753b70d 100644 --- a/app/migrations/versions/150252c1cdb1_.py +++ b/app/migrations/versions/150252c1cdb1_.py @@ -10,8 +10,8 @@ Create Date: 2019-04-02 18:00:12.618368 revision = "150252c1cdb1" down_revision = None -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): diff --git a/app/models/location.py b/app/models/location.py index 8fc3a92..539e21d 100644 --- a/app/models/location.py +++ b/app/models/location.py @@ -12,8 +12,9 @@ class Location(db.Model): products = db.relationship("Product", backref="location", lazy="dynamic") orders = db.relationship("Order", backref="location", lazy="dynamic") - def configure(self, name: str, address: str, - telephone: typing.Optional[str], website: str) -> None: + def configure( + self, name: str, address: str, telephone: typing.Optional[str], website: str + ) -> None: self.name = name self.address = address self.website = website diff --git a/app/models/order.py b/app/models/order.py index b9c2b1a..44718fd 100644 --- a/app/models/order.py +++ b/app/models/order.py @@ -15,8 +15,13 @@ class Order(db.Model): public = db.Column(db.Boolean, default=True) items = db.relationship("OrderItem", backref="order", lazy="dynamic") - def configure(self, courrier: User, location: Location, - starttime: db.DateTime, stoptime: db.DateTime) -> None: + def configure( + self, + courrier: User, + location: Location, + starttime: db.DateTime, + stoptime: db.DateTime, + ) -> None: self.courrier = courrier self.location = location self.starttime = starttime @@ -33,8 +38,9 @@ class Order(db.Model): 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["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 diff --git a/app/models/product.py b/app/models/product.py index 909aade..7e8e1a7 100644 --- a/app/models/product.py +++ b/app/models/product.py @@ -8,8 +8,7 @@ class Product(db.Model): 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") + orderItems = db.relationship("OrderItem", backref="product", lazy="dynamic") def configure(self, location: Location, name: str, price: int) -> None: self.location = location diff --git a/app/views/general.py b/app/views/general.py index 6386109..9617ec9 100644 --- a/app/views/general.py +++ b/app/views/general.py @@ -7,7 +7,6 @@ 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 diff --git a/app/views/order.py b/app/views/order.py index 536ac14..1d011b2 100644 --- a/app/views/order.py +++ b/app/views/order.py @@ -1,21 +1,13 @@ -from werkzeug.wrappers import Response import random -from datetime import datetime import typing -# from flask import current_app as app -from flask import ( - Blueprint, - abort, - flash, - redirect, - render_template, - request, - session, - url_for, - wrappers, -) +from datetime import datetime + import werkzeug +# from flask import current_app as app +from flask import (Blueprint, abort, flash, redirect, render_template, request, + session, url_for, wrappers) from flask_login import current_user, login_required +from werkzeug.wrappers import Response from forms import AnonOrderItemForm, OrderForm, OrderItemForm from models import Order, OrderItem, User, db @@ -59,15 +51,15 @@ def order(id: int, form: OrderForm = None) -> str: 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") @@ -85,8 +77,7 @@ def items_showcase(id: int, form: OrderForm = None) -> str: @login_required def order_edit(id: int) -> typing.Union[str, Response]: 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) @@ -111,8 +102,7 @@ def order_item_create(id: int) -> typing.Any: 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() @@ -137,16 +127,14 @@ def item_paid(order_id: int, item_id: int) -> typing.Optional[Response]: 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") + flash("Paid %s by %s" % (item.product.name, item.get_name()), "success") return redirect(url_for("order_bp.order", id=order_id)) abort(404) @order_bp.route("///user_paid") @login_required -def items_user_paid(order_id: int, - user_name: str) -> typing.Optional[Response]: +def items_user_paid(order_id: int, user_name: str) -> typing.Optional[Response]: user = User.query.filter(User.username == user_name).first() items: typing.List[OrderItem] = [] if user: @@ -164,8 +152,7 @@ def items_user_paid(order_id: int, for item in items: item.paid = True db.session.commit() - flash("Paid %d items for %s" % - (len(items), item.get_name()), "success") + flash("Paid %d items for %s" % (len(items), item.get_name()), "success") return redirect(url_for("order_bp.order", id=order_id)) abort(404) @@ -253,6 +240,5 @@ def get_orders(expression=None) -> typing.List[Order]: 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 From c14874e1c3be6d5cc28f60de9cff31922a0b14b5 Mon Sep 17 00:00:00 2001 From: Jan-Pieter Baert Date: Sun, 8 Sep 2019 20:59:05 +0200 Subject: [PATCH 6/6] Add file listing tests to be run --- tests.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 tests.md diff --git a/tests.md b/tests.md new file mode 100644 index 0000000..9f9f54a --- /dev/null +++ b/tests.md @@ -0,0 +1,4 @@ +# Tests +For this application we run a number of tests, at the moment the tests are: +- [mypy](http://mypy-lang.org/), type-checking, which you can run using `mypy` with the argument `--ignore-missing-imports` if it says modules cannot be found +