Compare commits

...

44 commits
slug ... master

Author SHA1 Message Date
redfast00 cdca5646ef
Merge pull request #204 from Happilands/patch-1
Add robots.txt file
2023-04-22 17:52:17 +02:00
Tibo e86fce0a7e
Merge branch 'master' into patch-1 2023-04-19 19:14:54 +02:00
Charlotte Van Petegem 02afba70a9
Merge pull request #212 from ZeusWPI/fix/admin-order-current-top
Order current user to the top for admins when choosing courier
2023-01-24 19:11:21 +01:00
Charlotte Van Petegem 1bc6a5931e
Order current user to the top for admins 2023-01-24 19:06:10 +01:00
Jasper c991cd7882
Merge pull request #207 from JasperJanin/master
Add user group reference to orders
2022-10-27 22:15:37 +02:00
Jasper Janin a29ade4773 Remove redundant class attributes 2022-10-27 22:12:03 +02:00
Jasper 6f7aff15cc
Merge branch 'ZeusWPI:master' into master 2022-10-27 21:35:28 +02:00
Jasper Janin 7b12c266b3 Add user group reference to orders 2022-10-27 21:32:22 +02:00
Tibo 7d122cf6e9
Merge pull request #203 from ZeusWPI/addToolversions
.tool-versions toegevoegd
2022-10-27 21:32:06 +02:00
Charlotte Van Petegem 202d5d3e7a
Revert "Merge branch 'master' of github.com:ZeusWPI/Haldis"
This reverts commit 28fa1b7592, reversing
changes made to b14671413c.
2022-10-27 21:23:13 +02:00
Jasper Janin 28fa1b7592 Merge branch 'master' of github.com:ZeusWPI/Haldis 2022-10-27 20:28:11 +02:00
Jasper Janin bf8eb94117 Add user group reference to orders 2022-10-27 20:24:35 +02:00
Maxime b14671413c
Merge pull request #206 from ZeusWPI/sentry-local-dev
Make sure local dev still works with sentry
2022-10-27 19:49:57 +02:00
Charlotte Van Petegem 29afc8db7a
Make sure local dev still works with sentry 2022-10-27 19:47:12 +02:00
Charlotte Van Petegem 1dcd723bd4
Merge pull request #205 from ZeusWPI/add-sentry
Add glitchtip
2022-10-27 19:41:26 +02:00
Charlotte Van Petegem c0f44ab037
Add glitchtip 2022-10-27 19:38:46 +02:00
Happilands 4e8799eca5
Add robots.txt file
From
https://www.pythonanywhere.com/forums/topic/2899/
2022-10-27 19:29:50 +02:00
AlexVDP8 e302da0335 .tool-versions toegevoegd
Co-authored-by: Francis <francisklinck@gmail.com>
2022-10-27 19:15:29 +02:00
redfast00 c839fce270
Merge pull request #199 from ZeusWPI/fix/delete-bottom-list
Fix not being able to delete an item from the list at the bottom of an order
2022-06-10 18:41:14 +02:00
Charlotte Van Petegem 687d389fa2
Fix not being able to delete an item from the list at the bottom of an order 2022-06-10 18:28:31 +02:00
redfast00 9c4361ab1b
Merge pull request #197 from ZeusWPI/remove-dot-title-from-username
Don't title username
2022-06-03 19:36:54 +02:00
redfast00 754eae4a50
Don't title username 2022-06-03 19:27:11 +02:00
Charlotte Van Petegem f3911b377d
Merge pull request #196 from ZeusWPI/eight-character-slug
Make slug eight characters
2022-06-03 19:16:11 +02:00
Charlotte Van Petegem 3bc2ad83ea
Remove some more letters from the alphabet 2022-06-03 19:12:00 +02:00
Charlotte Van Petegem 0661016236
Make slug eight characters 2022-06-03 19:05:33 +02:00
Maxime 10327941d2
Merge pull request #195 from ZeusWPI/base58-slugs
Use base58 for slugs
2022-06-01 21:26:49 +02:00
mcbloch 5d204a4012 use a string, not bytes 2022-06-01 17:36:52 +02:00
Maxime 2bdd07c9af
Update app/models/order.py
Co-authored-by: Charlotte Van Petegem <charlotte.vanpetegem@ugent.be>
2022-06-01 17:24:27 +02:00
mcbloch 978b432d7e use base58 for slugs to remove doubt 2022-06-01 17:18:47 +02:00
Charlotte Van Petegem 426357f00d
Move filtering by association to get_orders
Fixes duplication and restores old `/orders` behaviour
2022-05-30 20:24:07 +02:00
Midgard 5306561ddd
Correct word on home page 2022-05-30 20:20:51 +02:00
Charlotte Van Petegem 01b5c72e7b
Generate slug in app 2022-05-30 19:48:23 +02:00
Midgard 4a353ec17e
Create a slug for old orders in the migration 2022-05-30 19:47:48 +02:00
Charlotte Van Petegem 8f3750060b
VARCHAR requires a length in mysql 2022-05-30 18:50:46 +02:00
Charlotte Van Petegem bb49fb2795
Add merge migration 2022-05-30 18:44:18 +02:00
Charlotte Van Petegem 28a6dc5422
Merge pull request #193 from ZeusWPI/feature/association-management
Integrate associations
2022-05-30 18:29:05 +02:00
Charlotte Van Petegem c04d9bbd44
Fix typing of associations in user model 2022-05-25 10:07:30 +02:00
Charlotte Van Petegem 4d9d43b0f0
Don't limit length (and fix migration) 2022-05-24 21:26:56 +02:00
Charlotte Van Petegem da1a708e28
List order association in admin view 2022-05-20 23:29:05 +02:00
Charlotte Van Petegem d6d9d61f27
Remove some unused code 2022-05-20 23:21:11 +02:00
Charlotte Van Petegem a077a8038a
Only list orders to users of its association 2022-05-20 23:15:45 +02:00
Charlotte Van Petegem 1c0d78f2ee
Make sure only users with at least one association can create an order 2022-05-20 22:46:56 +02:00
Charlotte Van Petegem c43efa4b10
Migration 2022-05-20 21:34:18 +02:00
mcbloch 8a2b9247e1
initial work, model works, layout doenst 2022-05-20 21:02:24 +02:00
21 changed files with 228 additions and 101 deletions

1
.tool-versions Normal file
View file

@ -0,0 +1 @@
python 3.9.2

View file

@ -10,5 +10,5 @@ def add() -> None:
"""Add users as admin."""
for username in Configuration.HALDIS_ADMINS:
user = User()
user.configure(username, True, 0)
user.configure(username, True, 0, associations=["zeus"])
db.session.add(user)

View file

@ -28,11 +28,12 @@ class OrderAdminModel(ModelBaseView):
"Class for the model of a OrderAdmin"
# pylint: disable=too-few-public-methods
column_default_sort = ("starttime", True)
column_list = ["starttime", "stoptime", "location_name", "location_id", "courier"]
column_list = ["starttime", "stoptime", "location_name", "location_id", "courier", "association"]
column_labels = {
"starttime": "Start Time",
"stoptime": "Closing Time",
"location_id": "HLDS Location ID",
"association": "Association",
}
form_excluded_columns = ["items", "courier_id"]
can_delete = False

View file

@ -3,12 +3,14 @@
"""Main Haldis script"""
import logging
import sentry_sdk
import typing
from datetime import datetime
from logging.handlers import TimedRotatingFileHandler
from admin import init_admin
from flask import Flask, render_template
from config import Configuration
from flask import Flask, render_template, Response
from flask_bootstrap import Bootstrap, StaticCDN
from flask_debugtoolbar import DebugToolbarExtension
from flask_login import LoginManager
@ -19,6 +21,7 @@ from login import init_login
from markupsafe import Markup
from models import db
from models.anonymous_user import AnonymouseUser
from sentry_sdk.integrations.flask import FlaskIntegration
from utils import euro_string, price_range_string, ignore_none
from zeus import init_oauth
@ -158,6 +161,12 @@ def create_app():
"""Initializer for the Flask app object"""
app = Flask(__name__)
@app.route('/robots.txt')
def noindex():
r = Response(response="User-Agent: *\nDisallow: /\n", status=200, mimetype="text/plain")
r.headers["Content-Type"] = "text/plain; charset=utf-8"
return r
# Load the config file
app.config.from_object("config.Configuration")
@ -171,5 +180,11 @@ def create_app():
# For usage when you directly call the script with python
if __name__ == "__main__":
if Configuration.SENTRY_DSN:
sentry_sdk.init(
dsn=Configuration.SENTRY_DSN,
integrations=[FlaskIntegration()]
)
app, app_mgr = create_app()
app_mgr.run()

View file

@ -12,5 +12,6 @@ class Configuration:
SECRET_KEY = "<change>"
SLACK_WEBHOOK = None
LOGFILE = "haldis.log"
SENTRY_DSN = None
ZEUS_KEY = "tomtest"
ZEUS_SECRET = "blargh"

View file

@ -24,13 +24,17 @@ class OrderForm(Form):
"Starttime", default=datetime.now, format="%d-%m-%Y %H:%M"
)
stoptime = DateTimeField("Stoptime", format="%d-%m-%Y %H:%M")
association = SelectField("Association", coerce=str, validators=[validators.required()])
submit_button = SubmitField("Submit")
def populate(self) -> None:
"Fill in the options for courier for an Order"
if current_user.is_admin():
self.courier_id.choices = [(0, None)] + [
(u.id, u.username) for u in User.query.order_by("username")
self.courier_id.choices = [
(0, None),
(current_user.id, current_user.username),
] + [
(u.id, u.username) for u in User.query.order_by("username") if u.id != current_user.id
]
else:
self.courier_id.choices = [
@ -38,6 +42,7 @@ class OrderForm(Form):
(current_user.id, current_user.username),
]
self.location_id.choices = [(l.id, l.name) for l in location_definitions]
self.association.choices = current_user.association_list()
if self.stoptime.data is None:
self.stoptime.data = datetime.now() + timedelta(hours=1)

View file

@ -12,12 +12,19 @@ down_revision = '55013fe95bea'
from alembic import op
import sqlalchemy as sa
from sqlalchemy.sql import text
def upgrade():
op.add_column('order', sa.Column('slug', sa.String(length=7), nullable=True))
op.create_unique_constraint(None, 'order', ['slug'])
op.add_column('order', sa.Column(
'slug',
sa.String(length=8),
nullable=False,
# Default: random alphanumerical string
server_default=text('SUBSTRING(MD5(RAND()) FROM 1 FOR 7)')
))
op.create_unique_constraint('order_slug_unique', 'order', ['slug'])
def downgrade():
op.drop_constraint(None, 'order', type_='unique')
op.drop_constraint('order_slug_unique', 'order', type_='unique')
op.drop_column('order', 'slug')

View file

@ -0,0 +1,22 @@
"""empty message
Revision ID: 9eac0f3d7b1e
Revises: ('f6a6004bf4b9', '29ccbe077c57')
Create Date: 2022-05-30 18:35:43.918797
"""
# revision identifiers, used by Alembic.
revision = '9eac0f3d7b1e'
down_revision = ('f6a6004bf4b9', '29ccbe077c57')
from alembic import op
import sqlalchemy as sa
def upgrade():
pass
def downgrade():
pass

View file

@ -0,0 +1,28 @@
"""Add user associations
Revision ID: f6a6004bf4b9
Revises: 55013fe95bea
Create Date: 2022-05-24 21:23:27.770365
"""
# revision identifiers, used by Alembic.
revision = 'f6a6004bf4b9'
down_revision = '55013fe95bea'
from alembic import op
import sqlalchemy as sa
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('order', sa.Column('association', sa.String(length=120), server_default='', nullable=False))
op.add_column('user', sa.Column('associations', sa.String(length=255), server_default='', nullable=False))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('user', 'associations')
op.drop_column('order', 'association')
# ### end Alembic commands ###

View file

@ -1,10 +1,14 @@
"AnonymouseUser for people who are not logged in the normal way"
from typing import List
# pylint: disable=R0201,C0111
class AnonymouseUser:
id = None
def association_list(self) -> List[str]:
return []
def is_active(self) -> bool:
return False

View file

@ -11,11 +11,13 @@ from utils import first
from .database import db
from .user import User
BASE31_ALPHABET = '23456789abcdefghjkmnpqrstuvwxyz'
def generate_slug():
alphabet = string.ascii_letters + string.digits
return ''.join(secrets.choice(alphabet) for i in range(7))
secret = ''.join(secrets.choice(BASE31_ALPHABET) for i in range(8))
while Order.query.filter(Order.slug == secret).first() is not None:
secret = ''.join(secrets.choice(BASE31_ALPHABET) for i in range(8))
return secret
class Order(db.Model):
"""Class used for configuring the Order model in the database"""
@ -26,7 +28,8 @@ class Order(db.Model):
starttime = db.Column(db.DateTime)
stoptime = db.Column(db.DateTime)
public = db.Column(db.Boolean, default=True)
slug = db.Column(db.String(7), default=generate_slug, unique=True)
slug = db.Column(db.String(8), default=generate_slug, unique=True)
association = db.Column(db.String(120), nullable=False, server_default="")
items = db.relationship("OrderItem", backref="order", lazy="dynamic")

View file

@ -1,4 +1,6 @@
"Script for everything User related in the database"
from typing import List, Optional
from models import db
@ -8,6 +10,10 @@ class User(db.Model):
username = db.Column(db.String(80), unique=True, nullable=False)
admin = db.Column(db.Boolean)
bias = db.Column(db.Integer)
# Assocation logic
associations = db.Column(db.String(255), nullable=False, server_default="")
# Relations
runs = db.relation(
"Order",
backref="courier",
@ -16,11 +22,17 @@ class User(db.Model):
)
orderItems = db.relationship("OrderItem", backref="user", lazy="dynamic")
def configure(self, username: str, admin: bool, bias: int) -> None:
"Configure the User"
def association_list(self) -> List[str]:
return self.associations.split(",")
def configure(self, username: str, admin: bool, bias: int, associations: Optional[List[str]] = None) -> None:
"""Configure the User"""
if associations is None:
associations = []
self.username = username
self.admin = admin
self.bias = bias
self.associations = ",".join(associations)
# pylint: disable=C0111, R0201
def is_authenticated(self) -> bool:

View file

@ -21,7 +21,7 @@ def webhook_text(order: Order) -> typing.Optional[str]:
url_for("order_bp.order_from_slug", order_slug=order.slug, _external=True),
order.location_name,
remaining_minutes(order.stoptime),
order.courier.username.title(),
order.courier.username,
)
# pylint: disable=C0209

View file

@ -155,60 +155,66 @@
<div class="box" id="order_info">
<h3>Order information</h3>
<dl>
<div>
<dt>Order opens</dt>
<dd>{{ order.starttime.strftime("%Y-%m-%d, %H:%M") }}</dd>
<div class="row">
<dl class="col-md-10 col-lg-8">
<div>
<dt>Order opens</dt>
<dd>{{ order.starttime.strftime("%Y-%m-%d, %H:%M") }}</dd>
<dt>Order closes</dt>
<dd>
{% if order.stoptime %}
{% set stoptimefmt = (
"%H:%M" if order.stoptime.date() == order.starttime.date()
else "%Y-%m-%d, %H:%M"
) %}
{{ order.stoptime.strftime(stoptimefmt) }} ({{ order.stoptime|countdown }})
{% else %}
Never
{% endif %}
</dd>
<dt>Order closes</dt>
<dd>
{% if order.stoptime %}
{% set stoptimefmt = (
"%H:%M" if order.stoptime.date() == order.starttime.date()
else "%Y-%m-%d, %H:%M"
) %}
{{ order.stoptime.strftime(stoptimefmt) }} ({{ order.stoptime|countdown }})
{% else %}
Never
{% endif %}
</dd>
</div>
<div>
<dt>Location</dt>
<dd>
{% if order.location %}
<a href="{{ url_for('general_bp.location', location_id=order.location_id) }}">{{ order.location_name }}</a>
{% else %}
{{ order.location_name }}
{% endif %}
</dd>
<dt>Courier</dt>
<dd>
{% if order.courier == None %}
{% if not current_user.is_anonymous() %}
<form action="{{ url_for('order_bp.volunteer', order_slug=order.slug) }}" method="post" style="display:inline">
<input type="submit" class="btn btn-primary btn-sm" value="Volunteer"></input>
</form>
{% else %}No-one yet{% endif %}
{% else %}
{{ order.courier.username }}
{% endif %}
</dd>
</div>
</dl>
<div class="col-md-2 col-lg-4">
<img src="https://dsa.ugent.be/api/verenigingen/{{ order.association }}/logo" class="img-responsive align-top" style="max-width:200px;width:100%">
</div>
<div>
<dt>Location</dt>
<dd>
{% if order.location %}
<a href="{{ url_for('general_bp.location', location_id=order.location_id) }}">{{ order.location_name }}</a>
{% else %}
{{ order.location_name }}
{% endif %}
</dd>
<dt>Courier</dt>
<dd>
{% if order.courier == None %}
{% if not current_user.is_anonymous() %}
<form action="{{ url_for('order_bp.volunteer', order_slug=order.slug) }}" method="post" style="display:inline">
<input type="submit" class="btn btn-primary btn-sm" value="Volunteer"></input>
</form>
{% else %}No-one yet{% endif %}
{% else %}
{{ order.courier.username }}
{% endif %}
</dd>
</div>
</dl>
<div>
{% if order.can_close(current_user.id) -%}
<form action="{{ url_for('order_bp.close_order', order_slug=order.slug) }}" method="post" style="display:inline">
<input type="submit" class="btn btn-danger" value="Close"></input>
</form>
{% endif %}
{% if courier_or_admin %}
<a class="btn" href="{{ url_for('order_bp.order_edit', order_slug=order.slug) }}">Edit</a>
{%- endif %}
</div>
{% if order.can_close(current_user.id) -%}
<form action="{{ url_for('order_bp.close_order', order_slug=order.slug) }}" method="post" style="display:inline">
<input type="submit" class="btn btn-danger" value="Close"></input>
</form>
{% endif %}
{% if courier_or_admin %}
<a class="btn" href="{{ url_for('order_bp.order_edit', order_slug=order.slug) }}">Edit</a>
{%- endif %}
</div>
<div class="box" id="how_to_order">
@ -314,9 +320,7 @@
<li class="{{ 'paid' if item.paid }}">
<div class="actions">
{% if item.can_delete(order.id, current_user.id, session.get('anon_name', '')) -%}
<form action="{{ url_for('order_bp.delete_item', order_slug=order.slug, item_id=item.id) }}" method="post" style="display:inline">
<button class="btn btn-link btn-sm" type="submit" style="padding: 0 0.5em;"><span class="glyphicon glyphicon-remove"></span></button>
</form>
<button class="btn btn-link btn-sm" type="submit" name="delete_item" value="{{ item.id }}" style="padding: 0 0.5em;"><span class="glyphicon glyphicon-remove"></span></button>
{% else %}
<span class="glyphicon glyphicon-remove" style="color: var(--gray3); padding: 0 0.5em; cursor: not-allowed"></span>
{%- endif %}

View file

@ -38,6 +38,11 @@
{{ form.location_id(class='form-control select') }}
{{ util.render_form_field_errors(form.location_id) }}
</div>
<div class="form-group select2 {{ 'has-errors' if form.association.errors else ''}}{{ ' required' if form.association.flags.required }}">
{{ form.association.label(class='control-label') }}
{{ form.association(class='form-control select') }}
{{ util.render_form_field_errors(form.association) }}
</div>
{% if current_user.is_admin() %}
<div class="form-group{{ ' has-error' if form.starttime.errors }}{{ ' required' if form.starttime.flags.required }}{{ ' hidden' if not current_user.is_admin() }}">
{{ form.starttime.label(class='control-label') }}

View file

@ -1,14 +1,17 @@
{% macro render_order(order) -%}
<div class="row order_row">
<div class="col-md-8 col-lg-9 order_data">
<div class="col-md-6 order_data">
<h5>{{ order.location_name }}</h5>
<b class="amount_of_orders">{{ order.items.count() }} orders</b></p>
<b class="amount_of_orders">{{ order.items.count() }} items ordered for {{ order.association }}</b></p>
<p class="time_data">
{% if order.stoptime %}
<span><b>Closes </b>{{ order.stoptime.strftime("%H:%M") }}</span>{{ order.stoptime|countdown }}
{% else %}open{% endif %}<br/>
</div>
<div class="col-md-4 col-lg-3 expand_button_wrapper">
<div class="col-md-3">
<img src="https://dsa.ugent.be/api/verenigingen/{{ order.association }}/logo" class="img-responsive align-bottom" style="max-width:200px;width:100%">
</div>
<div class="col-md-3 expand_button_wrapper">
<a class="btn btn-primary btn-block align-bottom expand_button" href="{{ url_for('order_bp.order_from_slug', order_slug=order.slug) }}">Expand</a>
</div>
</div>

View file

@ -9,7 +9,7 @@ from flask import Blueprint, Flask, abort
from flask import current_app as app
from flask import (jsonify, make_response, render_template, request,
send_from_directory, url_for)
from flask_login import login_required
from flask_login import current_user, login_required
from hlds.definitions import location_definitions
from hlds.models import Location
from models import Order
@ -34,7 +34,9 @@ def home() -> str:
(Order.stoptime > prev_day) & (Order.stoptime < datetime.now())
)
return render_template(
"home.html", orders=get_orders(), recently_closed=recently_closed
"home.html", orders=get_orders(
((datetime.now() > Order.starttime) & (Order.stoptime > datetime.now()) | (Order.stoptime == None))
), recently_closed=recently_closed
)

View file

@ -21,7 +21,7 @@ order_bp = Blueprint("order_bp", "order")
@order_bp.route("/")
def orders(form: OrderForm = None) -> str:
"""Generate general order view"""
if form is None and not current_user.is_anonymous():
if form is None and current_user.association_list():
form = OrderForm()
location_id = request.args.get("location_id")
form.location_id.default = location_id
@ -34,6 +34,9 @@ def orders(form: OrderForm = None) -> str:
@login_required
def order_create() -> typing.Union[str, Response]:
"""Generate order create view"""
if not current_user.association_list():
flash("Not allowed to create an order.", "info")
abort(401)
orderForm = OrderForm()
orderForm.populate()
if orderForm.validate_on_submit():
@ -393,16 +396,17 @@ def get_orders(expression=None) -> typing.List[Order]:
"""Give the list of all currently open and public Orders"""
order_list: typing.List[OrderForm] = []
if expression is None:
expression = (datetime.now() > Order.starttime) & (
Order.stoptime
> datetime.now()
# pylint: disable=C0121
) | (Order.stoptime == None)
expression = ((datetime.now() > Order.starttime) & (
Order.stoptime
> datetime.now()
# pylint: disable=C0121
) | (Order.stoptime == None)
) & (Order.association.in_(current_user.association_list()))
if not current_user.is_anonymous():
order_list = Order.query.filter(expression).all()
else:
order_list = Order.query.filter(
# pylint: disable=C0121
expression & (Order.public == True)
expression & (Order.public == True) & (Order.association.in_(current_user.association_list()))
).all()
return order_list

View file

@ -77,7 +77,7 @@ def login_and_redirect_user(user) -> Response:
def create_user(username) -> User:
"Create a temporary user if it is needed"
user = User()
user.configure(username, False, 1)
user.configure(username, False, 1, associations=["zeus"])
db.session.add(user)
db.session.commit()
return user

View file

@ -12,3 +12,4 @@ black
pymysql
pyyaml
tatsu<5.6 # >=5.6 needs Python >=3.8
sentry-sdk[flask]

View file

@ -1,5 +1,5 @@
#
# This file is autogenerated by pip-compile
# This file is autogenerated by pip-compile with python 3.9
# To update, run:
#
# pip-compile
@ -11,11 +11,15 @@ appdirs==1.4.4
black==21.6b0
# via -r requirements.in
blinker==1.4
# via flask-debugtoolbar
# via
# flask-debugtoolbar
# sentry-sdk
cachelib==0.1.1
# via flask-oauthlib
certifi==2021.5.30
# via requests
# via
# requests
# sentry-sdk
chardet==4.0.0
# via requests
click==7.1.2
@ -24,6 +28,19 @@ click==7.1.2
# flask
dominate==2.6.0
# via flask-bootstrap
flask==1.1.4
# via
# -r requirements.in
# flask-admin
# flask-bootstrap
# flask-debugtoolbar
# flask-login
# flask-migrate
# flask-oauthlib
# flask-script
# flask-sqlalchemy
# flask-wtf
# sentry-sdk
flask-admin==1.5.8
# via -r requirements.in
flask-bootstrap==3.3.7.1
@ -44,18 +61,6 @@ flask-sqlalchemy==2.5.1
# flask-migrate
flask-wtf==0.15.1
# via -r requirements.in
flask==1.1.4
# via
# -r requirements.in
# flask-admin
# flask-bootstrap
# flask-debugtoolbar
# flask-login
# flask-migrate
# flask-oauthlib
# flask-script
# flask-sqlalchemy
# flask-wtf
greenlet==1.1.0
# via sqlalchemy
idna==2.10
@ -92,10 +97,12 @@ pyyaml==5.4.1
# via -r requirements.in
regex==2021.4.4
# via black
requests-oauthlib==1.1.0
# via flask-oauthlib
requests==2.25.1
# via requests-oauthlib
requests-oauthlib==1.1.0
# via flask-oauthlib
sentry-sdk[flask]==1.10.1
# via -r requirements.in
six==1.16.0
# via python-dateutil
sqlalchemy==1.4.18
@ -106,8 +113,10 @@ tatsu==4.4.0
# via -r requirements.in
toml==0.10.2
# via black
urllib3==1.26.5
# via requests
urllib3==1.26.12
# via
# requests
# sentry-sdk
visitor==0.1.3
# via flask-bootstrap
werkzeug==1.0.1