admin: Sending bulk emails
This commit is contained in:
parent
24e45af377
commit
89513af939
14 changed files with 178 additions and 53 deletions
|
@ -4,6 +4,7 @@ from flask_migrate import Migrate
|
||||||
from flask_login import LoginManager
|
from flask_login import LoginManager
|
||||||
from flask_assets import Bundle, Environment
|
from flask_assets import Bundle, Environment
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
|
from flask_mail import Mail
|
||||||
|
|
||||||
|
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
|
@ -11,6 +12,7 @@ migrate = Migrate()
|
||||||
login = LoginManager()
|
login = LoginManager()
|
||||||
environment = Environment()
|
environment = Environment()
|
||||||
cors = CORS()
|
cors = CORS()
|
||||||
|
mail = Mail()
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
@ -22,6 +24,7 @@ def create_app():
|
||||||
login.init_app(app)
|
login.init_app(app)
|
||||||
environment.init_app(app)
|
environment.init_app(app)
|
||||||
cors.init_app(app)
|
cors.init_app(app)
|
||||||
|
mail.init_app(app)
|
||||||
|
|
||||||
scss = Bundle('css/style.scss', filters='scss',
|
scss = Bundle('css/style.scss', filters='scss',
|
||||||
output='css/style.css')
|
output='css/style.css')
|
||||||
|
@ -38,3 +41,5 @@ def create_app():
|
||||||
app.register_blueprint(admin.bp)
|
app.register_blueprint(admin.bp)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
from flask import Blueprint, jsonify, redirect, render_template, url_for
|
from flask import Blueprint, jsonify, redirect, render_template, request, url_for
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
|
from flask_mail import Message
|
||||||
|
|
||||||
from goathacks.models import User
|
from goathacks.models import User
|
||||||
|
|
||||||
bp = Blueprint("admin", __name__, url_prefix="/admin")
|
bp = Blueprint("admin", __name__, url_prefix="/admin")
|
||||||
|
|
||||||
from goathacks import db
|
from goathacks import db,mail
|
||||||
|
|
||||||
@bp.route("/")
|
@bp.route("/")
|
||||||
@login_required
|
@login_required
|
||||||
|
@ -53,6 +54,45 @@ def home():
|
||||||
female_count=female_count, nb_count=nb_count,
|
female_count=female_count, nb_count=nb_count,
|
||||||
check_in_count=check_in_count, schools=schools)
|
check_in_count=check_in_count, schools=schools)
|
||||||
|
|
||||||
|
@bp.route("/mail")
|
||||||
|
@login_required
|
||||||
|
def mail():
|
||||||
|
if not current_user.is_admin:
|
||||||
|
return redirect(url_for("dashboard.home"))
|
||||||
|
|
||||||
|
total_count = len(db.session.execute(db.select(User)).scalars().all())
|
||||||
|
|
||||||
|
return render_template("mail.html", NUM_HACKERS=total_count)
|
||||||
|
|
||||||
|
@bp.route("/send", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def send():
|
||||||
|
if not current_user.is_admin:
|
||||||
|
return {"status": "error"}
|
||||||
|
|
||||||
|
json = request.json
|
||||||
|
|
||||||
|
users = User.query.all()
|
||||||
|
|
||||||
|
to = []
|
||||||
|
if json["recipients"] == "org":
|
||||||
|
to = ["hack@wpi.edu"]
|
||||||
|
elif json['recipients'] == 'admin':
|
||||||
|
to = ["acm-sysadmin@wpi.edu"]
|
||||||
|
elif json['recipients'] == "all":
|
||||||
|
to = [x['email'] for x in users]
|
||||||
|
|
||||||
|
with mail.connect() as conn:
|
||||||
|
for e in to:
|
||||||
|
msg = Message(json['subject'])
|
||||||
|
msg.add_recipient(e)
|
||||||
|
msg.html = json['html']
|
||||||
|
msg.body = json['text']
|
||||||
|
|
||||||
|
conn.send(msg)
|
||||||
|
|
||||||
|
return {"status": "success"}
|
||||||
|
|
||||||
@bp.route("/check_in/<int:id>")
|
@bp.route("/check_in/<int:id>")
|
||||||
@login_required
|
@login_required
|
||||||
def check_in(id):
|
def check_in(id):
|
||||||
|
@ -79,6 +119,11 @@ def drop(id):
|
||||||
if user.checked_in:
|
if user.checked_in:
|
||||||
return {"status": "error", "msg": "Hacker is already checked in"}
|
return {"status": "error", "msg": "Hacker is already checked in"}
|
||||||
|
|
||||||
|
msg = Message("Application Dropped")
|
||||||
|
msg.add_recipient(user.email)
|
||||||
|
msg.sender = ("GoatHacks Team", "hack@wpi.edu")
|
||||||
|
msg.body = render_template("emails/dropped.txt", user=user)
|
||||||
|
|
||||||
db.session.delete(user)
|
db.session.delete(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
@ -122,6 +167,11 @@ def promote_waitlist(id):
|
||||||
user.waitlisted = False
|
user.waitlisted = False
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
msg = Message("Waitlist Promotion")
|
||||||
|
msg.add_recipient(user.email)
|
||||||
|
msg.sender = ("GoatHacks Team", "hack@wpi.edu")
|
||||||
|
msg.body = render_template("emails/waitlist_promotion.txt", user=user)
|
||||||
|
|
||||||
return {"status": "success"}
|
return {"status": "success"}
|
||||||
|
|
||||||
@bp.route("/hackers.csv")
|
@bp.route("/hackers.csv")
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
from flask import Blueprint, config, current_app, flash, redirect, render_template, request, url_for
|
||||||
import flask_login
|
import flask_login
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from goathacks.registration.forms import LoginForm, RegisterForm
|
from goathacks.registration.forms import LoginForm, RegisterForm
|
||||||
|
@ -14,8 +14,10 @@ bp = Blueprint('registration', __name__, url_prefix="/registration")
|
||||||
def register():
|
def register():
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
flash("You are already registered and logged in!")
|
flash("You are already registered and logged in!")
|
||||||
|
|
||||||
print("got register")
|
print("got register")
|
||||||
form = RegisterForm(request.form)
|
form = RegisterForm(request.form)
|
||||||
|
print(vars(form.gender))
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
print("Got form")
|
print("Got form")
|
||||||
email = request.form.get('email')
|
email = request.form.get('email')
|
||||||
|
@ -23,22 +25,32 @@ def register():
|
||||||
last_name = request.form.get('last_name')
|
last_name = request.form.get('last_name')
|
||||||
password = request.form.get('password')
|
password = request.form.get('password')
|
||||||
password_c = request.form.get('password_confirm')
|
password_c = request.form.get('password_confirm')
|
||||||
|
school = request.form.get('school')
|
||||||
|
phone = request.form.get('phone_number')
|
||||||
|
gender = request.form.get('gender')
|
||||||
|
|
||||||
|
|
||||||
if password == password_c:
|
if password == password_c:
|
||||||
# Passwords match!
|
# Passwords match!
|
||||||
|
|
||||||
|
# Count of all non-waitlisted hackers
|
||||||
|
num_not_waitlisted = len(User.query.filter_by(waitlisted=False).all())
|
||||||
|
waitlisted = False
|
||||||
|
print(num_not_waitlisted)
|
||||||
|
print(current_app.config['MAX_BEFORE_WAITLIST'])
|
||||||
|
if num_not_waitlisted >= current_app.config['MAX_BEFORE_WAITLIST']:
|
||||||
|
waitlisted = True
|
||||||
user = User(
|
user = User(
|
||||||
email=email,
|
email=email,
|
||||||
password=generate_password_hash(password),
|
password=generate_password_hash(password),
|
||||||
first_name=first_name,
|
first_name=first_name,
|
||||||
last_name=last_name,
|
last_name=last_name,
|
||||||
last_login=datetime.now(),
|
last_login=datetime.now(),
|
||||||
|
waitlisted=waitlisted,
|
||||||
|
school=school,
|
||||||
|
phone=phone,
|
||||||
|
gender=gender
|
||||||
)
|
)
|
||||||
|
|
||||||
# Count of all non-waitlisted hackers
|
|
||||||
# num_not_waitlisted = len(db.session.execute(db.select(User).filter(waitlisted=False)).scalars().all())
|
|
||||||
# print(num_not_waitlisted)
|
|
||||||
|
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flask_login.login_user(user)
|
flask_login.login_user(user)
|
||||||
|
@ -48,3 +60,7 @@ def register():
|
||||||
flash("Passwords do not match")
|
flash("Passwords do not match")
|
||||||
|
|
||||||
return render_template("register.html", form=form)
|
return render_template("register.html", form=form)
|
||||||
|
|
||||||
|
@bp.route("/login", methods=["GET", "POST"])
|
||||||
|
def login():
|
||||||
|
return "OK"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import BooleanField, PasswordField, StringField, SubmitField
|
from wtforms import BooleanField, PasswordField, SelectField, StringField, SubmitField, widgets
|
||||||
from wtforms.validators import DataRequired
|
from wtforms.validators import DataRequired
|
||||||
|
|
||||||
class RegisterForm(FlaskForm):
|
class RegisterForm(FlaskForm):
|
||||||
|
@ -10,6 +10,11 @@ class RegisterForm(FlaskForm):
|
||||||
password = PasswordField("Password", validators=[DataRequired()])
|
password = PasswordField("Password", validators=[DataRequired()])
|
||||||
password_confirm = PasswordField("Confirm Password",
|
password_confirm = PasswordField("Confirm Password",
|
||||||
validators=[DataRequired()])
|
validators=[DataRequired()])
|
||||||
|
school = StringField("School/University", validators=[DataRequired()])
|
||||||
|
phone_number = StringField("Phone number", validators=[DataRequired()])
|
||||||
|
gender = SelectField("Gender", choices=[("F", "Female"), ("M", "Male"),
|
||||||
|
("NB", "Non-binary/Other")],
|
||||||
|
widget=widgets.Select())
|
||||||
agree_coc = BooleanField("I confirm that I have read and agree to the Code of Conduct", validators=[DataRequired()])
|
agree_coc = BooleanField("I confirm that I have read and agree to the Code of Conduct", validators=[DataRequired()])
|
||||||
submit = SubmitField("Register")
|
submit = SubmitField("Register")
|
||||||
|
|
||||||
|
|
|
@ -150,8 +150,20 @@ form input[type="radio"] {
|
||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
form input[type="checkbox"]:checked {
|
||||||
|
visibility: visible;
|
||||||
|
left: unset;
|
||||||
|
position: unset;
|
||||||
|
}
|
||||||
|
|
||||||
form label {
|
form label {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
padding-left: 25px !important;
|
padding-left: 25px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
form select {
|
||||||
|
display: unset;
|
||||||
|
background: #974355;
|
||||||
|
max-width: 11rem;
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
$color-bg: #003049;
|
$color-bg: #003049;
|
||||||
$color-fg: #eee;
|
$color-fg: #eee;
|
||||||
$color-section-bg: #F5665B;
|
$color-section-bg: #974355;
|
||||||
$color-accent: #26a69a;
|
$color-accent: #26a69a;
|
||||||
@font-face {font-family: "Krungthep"; src: url("//db.onlinewebfonts.com/t/736cf5b08b01082a3645e14038868e20.eot"); src: url("//db.onlinewebfonts.com/t/736cf5b08b01082a3645e14038868e20.eot?#iefix") format("embedded-opentype"), url("//db.onlinewebfonts.com/t/736cf5b08b01082a3645e14038868e20.woff2") format("woff2"), url("//db.onlinewebfonts.com/t/736cf5b08b01082a3645e14038868e20.woff") format("woff"), url("//db.onlinewebfonts.com/t/736cf5b08b01082a3645e14038868e20.ttf") format("truetype"), url("//db.onlinewebfonts.com/t/736cf5b08b01082a3645e14038868e20.svg#Krungthep") format("svg"); }
|
@font-face {font-family: "Krungthep"; src: url("//db.onlinewebfonts.com/t/736cf5b08b01082a3645e14038868e20.eot"); src: url("//db.onlinewebfonts.com/t/736cf5b08b01082a3645e14038868e20.eot?#iefix") format("embedded-opentype"), url("//db.onlinewebfonts.com/t/736cf5b08b01082a3645e14038868e20.woff2") format("woff2"), url("//db.onlinewebfonts.com/t/736cf5b08b01082a3645e14038868e20.woff") format("woff"), url("//db.onlinewebfonts.com/t/736cf5b08b01082a3645e14038868e20.ttf") format("truetype"), url("//db.onlinewebfonts.com/t/736cf5b08b01082a3645e14038868e20.svg#Krungthep") format("svg"); }
|
||||||
|
|
||||||
|
@ -148,9 +148,19 @@ form {
|
||||||
input[type="radio"] {
|
input[type="radio"] {
|
||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
}
|
}
|
||||||
|
input[type="checkbox"]:checked {
|
||||||
|
visibility: visible;
|
||||||
|
left: unset;
|
||||||
|
position: unset;
|
||||||
|
}
|
||||||
label {
|
label {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
padding-left: 25px !important;
|
padding-left: 25px !important;
|
||||||
}
|
}
|
||||||
|
select {
|
||||||
|
display: unset;
|
||||||
|
background: $color-section-bg;
|
||||||
|
max-width: 11rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
BIN
goathacks/static/img/favicon.png
Normal file
BIN
goathacks/static/img/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
|
@ -1,46 +1,10 @@
|
||||||
{% include 'header.html' %}
|
{% include 'header.html' %}
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/sweetalert/1.1.3/sweetalert.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/sweetalert/1.1.3/sweetalert.min.js"></script>
|
||||||
<link href="../static/css/materialize.min.css" rel="stylesheet">
|
|
||||||
<script>
|
<script>
|
||||||
function drop(id) {
|
function drop(id) {
|
||||||
if(window.confirm("Are you sure you wish to drop your application? This cannot be undone. (patiently wait after clicking the button)")) {
|
if(window.confirm("Are you sure you wish to drop your application? This cannot be undone. (patiently wait after clicking the button)")) {
|
||||||
window.location.href = "/drop?mlh_id=" + id;
|
window.location.href = "/admin/drop/" + id;
|
||||||
}
|
}
|
||||||
// swal({
|
|
||||||
// title: 'Drop your application?',
|
|
||||||
// text: 'Are you sure you wish to drop your application? This cannot be undone. (patiently wait after clicking the button)',
|
|
||||||
// type: 'warning',
|
|
||||||
// showCancelButton: true,
|
|
||||||
// closeOnConfirm: false,
|
|
||||||
// confirmButtonText: 'Yes, drop!',
|
|
||||||
// confirmButtonColor: errColor
|
|
||||||
// }, () => {
|
|
||||||
// $.get('/drop?mlh_id=' + id, (data) => {
|
|
||||||
// let title = ''
|
|
||||||
// let msg = ''
|
|
||||||
// let type = ''
|
|
||||||
// if (data.status === 'success'
|
|
||||||
// )
|
|
||||||
// {
|
|
||||||
// title = 'Dropped!'
|
|
||||||
// msg = 'Your application was successfully dropped!'
|
|
||||||
// type = 'success'
|
|
||||||
// }
|
|
||||||
// else
|
|
||||||
// {
|
|
||||||
// title = 'Error!'
|
|
||||||
// msg = JSON.stringify(data)
|
|
||||||
// type = 'error'
|
|
||||||
// }
|
|
||||||
// swal(title, msg, type)
|
|
||||||
// if (data.status === 'success') {
|
|
||||||
// setTimeout(() => {window.location = '/'
|
|
||||||
// },
|
|
||||||
// 5000
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resumeChange() {
|
function resumeChange() {
|
||||||
|
@ -54,8 +18,8 @@
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row center justify-content-center" style="margin-top: 10%;">
|
<div class="row center justify-content-center" style="margin-top: 10%;">
|
||||||
<h1>Hi {{ current_user.first_name }}!</h1>
|
<h1>Hi {{ current_user.first_name }}!</h1>
|
||||||
{% if waitlisted %}
|
{% if current_user.waitlisted %}
|
||||||
<h2>You are waitlisted, if space opens up we will let you know...</h2>
|
<h2>You are waitlisted, if space opens up we will let you know</h2>
|
||||||
{% else %}
|
{% else %}
|
||||||
<h2>You are fully registered! We look forward to seeing you!</h2>
|
<h2>You are fully registered! We look forward to seeing you!</h2>
|
||||||
Let us know if you have any questions by sending them to <a href="mailto:hack@wpi.edu">hack@wpi.edu</a>
|
Let us know if you have any questions by sending them to <a href="mailto:hack@wpi.edu">hack@wpi.edu</a>
|
||||||
|
@ -115,7 +79,7 @@
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
<br>
|
<br>
|
||||||
<center><a onclick="drop('{{id}}')" id="drop-link"><p class="btn">Drop Application if you can't make it :(</p></a></center>
|
<center><a onclick="drop('{{current_user.id}}')" id="drop-link"><p class="btn">Drop Application if you can't make it :(</p></a></center>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
13
goathacks/templates/emails/dropped.txt
Normal file
13
goathacks/templates/emails/dropped.txt
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
Dear {{ user.first_name }},
|
||||||
|
|
||||||
|
Your application has been dropped. We're sorry to see you go!
|
||||||
|
|
||||||
|
If this was done in error, you can re-register by going to
|
||||||
|
https://hack.wpi.edu/registration.
|
||||||
|
|
||||||
|
Happy Hacking!
|
||||||
|
|
||||||
|
GoatHacks Team
|
||||||
|
|
||||||
|
This is an automated message. Please email hack@wpi.edu with any questions or
|
||||||
|
concerns.
|
19
goathacks/templates/emails/registration.txt
Normal file
19
goathacks/templates/emails/registration.txt
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
Dear {{ user.first_name }},
|
||||||
|
|
||||||
|
Your application for GoatHacks has been confirmed! {% if user.waitlisted
|
||||||
|
%}You're on the waitlist right now, but we'll send you another email if a spot
|
||||||
|
opens up.{% else %}You've got a confirmed spot this year! Make sure to look at
|
||||||
|
the schedule at https://hack.wpi.edu.
|
||||||
|
|
||||||
|
{% if not user.waitlisted %}
|
||||||
|
We'll send another email with more details closer to the event. In the
|
||||||
|
meantime, visit your Dashboard (https://hack.wpi.edu/dashboard) to tell us about
|
||||||
|
your shirt size and any accomodations you may need.
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
Happy Hacking!
|
||||||
|
|
||||||
|
GoatHacks Team
|
||||||
|
|
||||||
|
This is an automated message. Please email hack@wpi.edu with any questions or
|
||||||
|
concerns.
|
18
goathacks/templates/emails/waitlist_promotion.txt
Normal file
18
goathacks/templates/emails/waitlist_promotion.txt
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
Hello {{ user.first_name }}!
|
||||||
|
|
||||||
|
We're writing to let you know that a spot has opened up in our registrations,
|
||||||
|
and you've been promoted off of the waitlist! Please visit our website
|
||||||
|
(https://hack.wpi.edu/dashboard) to complete your registration information and
|
||||||
|
join our Discord.
|
||||||
|
|
||||||
|
If you can no longer make the event, please visit your dashboard and use the
|
||||||
|
"Drop my registration" link.
|
||||||
|
|
||||||
|
Happy Hacking!
|
||||||
|
|
||||||
|
GoatHacks Team
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
This is an automated message. Please email hack@wpi.edu with any questions or
|
||||||
|
concerns.
|
|
@ -34,9 +34,9 @@
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
Best,<br/>
|
Best,<br/>
|
||||||
<b>Hack@WPI Team</b><br/>
|
<b>GoatHacks Team</b><br/>
|
||||||
<i><a href="mailto:hack@wpi.edu">hack@wpi.edu</a></i><br/>
|
<i><a href="mailto:hack@wpi.edu">hack@wpi.edu</a></i><br/>
|
||||||
<img height="75px" width="75px" src="https://media.discordapp.net/attachments/829437603291856938/930311998057635880/hack317-min.png">
|
<img height="75px" width="75px" src="{{url_for('static', filename='img/favicon.png')}}">
|
||||||
</textarea>
|
</textarea>
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
|
|
|
@ -29,6 +29,18 @@
|
||||||
<div>
|
<div>
|
||||||
{{form.last_name}}<br/>{{form.last_name.label}}
|
{{form.last_name}}<br/>{{form.last_name.label}}
|
||||||
</div>
|
</div>
|
||||||
|
<hr/>
|
||||||
|
<h3>Miscellaneous Information</h3>
|
||||||
|
<div>
|
||||||
|
{{form.phone_number}}<br/>{{form.phone_number.label}}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{form.school}}<br/>{{form.school.label}}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{form.gender.label}}{{form.gender}}
|
||||||
|
</div>
|
||||||
|
<hr/>
|
||||||
<div>
|
<div>
|
||||||
<label for="agree_coc">I confirm that I have read and agree to the
|
<label for="agree_coc">I confirm that I have read and agree to the
|
||||||
Code of Conduct</label>
|
Code of Conduct</label>
|
||||||
|
|
|
@ -3,6 +3,7 @@ click==8.1.3
|
||||||
Flask==2.2.2
|
Flask==2.2.2
|
||||||
Flask-Assets
|
Flask-Assets
|
||||||
Flask-CORS
|
Flask-CORS
|
||||||
|
Flask-Mail
|
||||||
Flask-Login==0.6.2
|
Flask-Login==0.6.2
|
||||||
Flask-Migrate==4.0.0
|
Flask-Migrate==4.0.0
|
||||||
Flask-SQLAlchemy==3.0.2
|
Flask-SQLAlchemy==3.0.2
|
||||||
|
|
Loading…
Add table
Reference in a new issue