Merge branch 'master' into editor
This commit is contained in:
commit
d83999aa43
@ -0,0 +1,60 @@
|
|||||||
|
from .config import Development as Config
|
||||||
|
from .install import install_app
|
||||||
|
from .models import Entry, Dataset, Test, User
|
||||||
|
from .extensions import bootstrap, csrf, db, login_manager, mail
|
||||||
|
from .tools.data import save
|
||||||
|
from .tools.logs import write
|
||||||
|
|
||||||
|
from flask import flash, Flask, render_template, request
|
||||||
|
from flask.helpers import url_for
|
||||||
|
from flask.json import jsonify
|
||||||
|
from flask_wtf.csrf import CSRFError
|
||||||
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
|
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
def create_app():
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config.from_object(Config())
|
||||||
|
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto= 1, x_host= 1)
|
||||||
|
|
||||||
|
bootstrap.init_app(app)
|
||||||
|
csrf.init_app(app)
|
||||||
|
db.init_app(app)
|
||||||
|
login_manager.init_app(app)
|
||||||
|
mail.init_app(app)
|
||||||
|
|
||||||
|
login_manager.login_view = 'admin._login'
|
||||||
|
@login_manager.user_loader
|
||||||
|
def _load_user(id):
|
||||||
|
return User.query.filter_by(id=id).first()
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def _check_cookie_consent():
|
||||||
|
if request.cookies.get('cookie_consent'):
|
||||||
|
return
|
||||||
|
if any([ request.path.startswith(x) for x in [ '/admin/static/', '/root/', '/quiz/static', '/cookies/' ] ]):
|
||||||
|
return
|
||||||
|
flash(f'<strong>Cookie Consent</strong>: This web site only stores minimal, functional cookies. It does not store any tracking information. By using this site, you consent to this use of cookies. For more information, see our <a href="{url_for("views._privacy")}">privacy policy</a>.', 'cookie_alert')
|
||||||
|
|
||||||
|
@app.errorhandler(404)
|
||||||
|
def _404_handler(error): return render_template('404.html')
|
||||||
|
@app.errorhandler(CSRFError)
|
||||||
|
def _csrf_handler(): return jsonify({'error':'Could not validate a secure connection.'}), 403
|
||||||
|
@app.context_processor
|
||||||
|
def _now(): return {'now': datetime.now()}
|
||||||
|
|
||||||
|
from .admin.views import admin
|
||||||
|
from .api.views import api
|
||||||
|
from .quiz.views import quiz
|
||||||
|
from .views import views
|
||||||
|
|
||||||
|
app.register_blueprint(admin, url_prefix='/admin')
|
||||||
|
app.register_blueprint(api, url_prefix='/api')
|
||||||
|
app.register_blueprint(views)
|
||||||
|
app.register_blueprint(quiz)
|
||||||
|
|
||||||
|
install_app(app)
|
||||||
|
|
||||||
|
return app
|
@ -1,7 +1,7 @@
|
|||||||
from ..forms.admin import AddTimeAdjustment, CreateTest, CreateUser, DeleteUser, Login, Register, ResetPassword, UpdatePassword, UpdateUser, UploadData
|
from ..forms.admin import AddTimeAdjustment, CreateTest, CreateUser, DeleteUser, Login, Register, ResetPassword, UpdatePassword, UpdateUser, UploadData
|
||||||
from ..models import Dataset, Entry, Test, User
|
from ..models import Dataset, Entry, Test, User
|
||||||
from ..tools.auth import disable_if_logged_in, require_account_creation
|
from ..tools.auth import disable_if_logged_in, require_account_creation
|
||||||
from ..tools.forms import get_dataset_choices, get_time_options
|
from ..tools.forms import get_dataset_choices, get_time_options, send_errors_to_client
|
||||||
from ..tools.data import check_is_json, validate_json
|
from ..tools.data import check_is_json, validate_json
|
||||||
from ..tools.test import answer_options, get_correct_answers
|
from ..tools.test import answer_options, get_correct_answers
|
||||||
|
|
||||||
@ -61,8 +61,7 @@ def _login():
|
|||||||
return jsonify({'success': f'Successfully logged in.'}), 200
|
return jsonify({'success': f'Successfully logged in.'}), 200
|
||||||
return jsonify({'error': f'The password you entered is incorrect.'}), 401
|
return jsonify({'error': f'The password you entered is incorrect.'}), 401
|
||||||
return jsonify({'error': f'The username you entered does not exist.'}), 401
|
return jsonify({'error': f'The username you entered does not exist.'}), 401
|
||||||
errors = [*form.username.errors, *form.password.errors]
|
return send_errors_to_client(form=form)
|
||||||
return jsonify({ 'error': errors}), 400
|
|
||||||
if 'remembered_username' in session: form.username.data = session.pop('remembered_username')
|
if 'remembered_username' in session: form.username.data = session.pop('remembered_username')
|
||||||
next = request.args.get('next')
|
next = request.args.get('next')
|
||||||
return render_template('/admin/auth/login.html', form=form, next=next)
|
return render_template('/admin/auth/login.html', form=form, next=next)
|
||||||
@ -90,8 +89,7 @@ def _register():
|
|||||||
return jsonify({'success': message}), 200
|
return jsonify({'success': message}), 200
|
||||||
flash(message=message, category='error')
|
flash(message=message, category='error')
|
||||||
return jsonify({'error': message}), 401
|
return jsonify({'error': message}), 401
|
||||||
errors = [*form.username.errors, *form.email.errors, *form.password.errors, *form.password_reenter.errors]
|
return send_errors_to_client(form=form)
|
||||||
return jsonify({ 'error': errors}), 400
|
|
||||||
return render_template('admin/auth/register.html', form=form)
|
return render_template('admin/auth/register.html', form=form)
|
||||||
|
|
||||||
@admin.route('/reset/', methods=['GET','POST'])
|
@admin.route('/reset/', methods=['GET','POST'])
|
||||||
@ -108,8 +106,7 @@ def _reset():
|
|||||||
if not user: return jsonify({'error': 'The user account does not exist.'}), 400
|
if not user: return jsonify({'error': 'The user account does not exist.'}), 400
|
||||||
if not user.get_email() == request.form.get('email'): return jsonify({'error': 'The email address does not match the user account.'}), 400
|
if not user.get_email() == request.form.get('email'): return jsonify({'error': 'The email address does not match the user account.'}), 400
|
||||||
return user.reset_password()
|
return user.reset_password()
|
||||||
errors = [*form.username.errors, *form.email.errors]
|
return send_errors_to_client(form=form)
|
||||||
return jsonify({ 'error': errors}), 400
|
|
||||||
|
|
||||||
token = request.args.get('token')
|
token = request.args.get('token')
|
||||||
if token:
|
if token:
|
||||||
@ -134,8 +131,7 @@ def _update_password():
|
|||||||
session['remembered_username'] = user.get_username()
|
session['remembered_username'] = user.get_username()
|
||||||
flash('Your password has been reset.', 'success')
|
flash('Your password has been reset.', 'success')
|
||||||
return jsonify({'success':'Your password has been reset'}), 200
|
return jsonify({'success':'Your password has been reset'}), 200
|
||||||
errors = [*form.password.errors, *form.password_reenter.errors]
|
return send_errors_to_client(form=form)
|
||||||
return jsonify({ 'error': errors}), 401
|
|
||||||
|
|
||||||
@admin.route('/settings/users/', methods=['GET', 'POST'])
|
@admin.route('/settings/users/', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@ -152,8 +148,7 @@ def _users():
|
|||||||
success, message = new_user.register(notify=request.form.get('notify'), password=password)
|
success, message = new_user.register(notify=request.form.get('notify'), password=password)
|
||||||
if success: return jsonify({'success': message}), 200
|
if success: return jsonify({'success': message}), 200
|
||||||
return jsonify({'error': message}), 401
|
return jsonify({'error': message}), 401
|
||||||
errors = [*form.username.errors, *form.email.errors, *form.password.errors]
|
return send_errors_to_client(form=form)
|
||||||
return jsonify({ 'error': errors}), 401
|
|
||||||
return render_template('/admin/settings/users.html', form = form, users = users)
|
return render_template('/admin/settings/users.html', form = form, users = users)
|
||||||
|
|
||||||
@admin.route('/settings/users/delete/<string:id>', methods=['GET', 'POST'])
|
@admin.route('/settings/users/delete/<string:id>', methods=['GET', 'POST'])
|
||||||
@ -170,8 +165,7 @@ def _delete_user(id:str):
|
|||||||
success, message = user.delete(notify=request.form.get('notify'))
|
success, message = user.delete(notify=request.form.get('notify'))
|
||||||
if success: return jsonify({'success': message}), 200
|
if success: return jsonify({'success': message}), 200
|
||||||
return jsonify({'error': message}), 400
|
return jsonify({'error': message}), 400
|
||||||
errors = form.password.errors
|
return send_errors_to_client(form=form)
|
||||||
return jsonify({ 'error': errors}), 400
|
|
||||||
|
|
||||||
if id == current_user.id:
|
if id == current_user.id:
|
||||||
flash('Cannot delete your own user account.', 'error')
|
flash('Cannot delete your own user account.', 'error')
|
||||||
@ -199,8 +193,7 @@ def _update_user(id:str):
|
|||||||
flash(message, 'success')
|
flash(message, 'success')
|
||||||
return jsonify({'success': message}), 200
|
return jsonify({'success': message}), 200
|
||||||
return jsonify({'error': message}), 400
|
return jsonify({'error': message}), 400
|
||||||
errors = [*form.confirm_password.errors, *form.email.errors, *form.password.errors, *form.password_reenter.errors]
|
return send_errors_to_client(form=form)
|
||||||
return jsonify({ 'error': errors}), 400
|
|
||||||
if not user:
|
if not user:
|
||||||
flash('User not found.', 'error')
|
flash('User not found.', 'error')
|
||||||
return redirect(url_for('admin._users'))
|
return redirect(url_for('admin._users'))
|
||||||
@ -222,8 +215,7 @@ def _questions():
|
|||||||
)
|
)
|
||||||
if success: return jsonify({'success': message}), 200
|
if success: return jsonify({'success': message}), 200
|
||||||
return jsonify({'error': message}), 400
|
return jsonify({'error': message}), 400
|
||||||
errors = form.data_file.errors
|
return send_errors_to_client(form=form)
|
||||||
return jsonify({ 'error': errors}), 400
|
|
||||||
|
|
||||||
data = Dataset.query.all()
|
data = Dataset.query.all()
|
||||||
return render_template('/admin/settings/questions.html', form=form, data=data)
|
return render_template('/admin/settings/questions.html', form=form, data=data)
|
||||||
@ -299,9 +291,7 @@ def _create_test():
|
|||||||
flash(message=message, category='success')
|
flash(message=message, category='success')
|
||||||
return jsonify({'success': message}), 200
|
return jsonify({'success': message}), 200
|
||||||
return jsonify({'error': message}), 400
|
return jsonify({'error': message}), 400
|
||||||
else:
|
return send_errors_to_client(form=form)
|
||||||
errors = [*form.start_date.errors, *form.expiry_date.errors, *form.time_limit.errors]
|
|
||||||
return jsonify({ 'error': errors}), 400
|
|
||||||
|
|
||||||
@admin.route('/tests/edit/', methods=['POST'])
|
@admin.route('/tests/edit/', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@ -385,7 +375,7 @@ def _view_entry(id:str=None):
|
|||||||
@admin.route('/certificate/',methods=['POST'])
|
@admin.route('/certificate/',methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def _generate_certificate():
|
def _generate_certificate():
|
||||||
from main import db
|
from ..extensions import db
|
||||||
id = request.get_json()['id']
|
id = request.get_json()['id']
|
||||||
entry = Entry.query.filter_by(id=id).first()
|
entry = Entry.query.filter_by(id=id).first()
|
||||||
if not entry: return jsonify({'error': 'Invalid entry ID.'}), 404
|
if not entry: return jsonify({'error': 'Invalid entry ID.'}), 404
|
||||||
|
@ -13,7 +13,7 @@ class Config(object):
|
|||||||
SECRET_KEY = os.getenv('SECRET_KEY')
|
SECRET_KEY = os.getenv('SECRET_KEY')
|
||||||
SERVER_NAME = os.getenv('SERVER_NAME')
|
SERVER_NAME = os.getenv('SERVER_NAME')
|
||||||
SESSION_COOKIE_SECURE = True
|
SESSION_COOKIE_SECURE = True
|
||||||
SQLALCHEMY_DATABASE_URI = f'sqlite:///{Path(DATA)}/database.db'
|
SQLALCHEMY_DATABASE_URI = f'sqlite:///{Path(os.path.abspath(f"{DATA}/database.db"))}'
|
||||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
|
|
||||||
MAIL_SERVER = os.getenv('MAIL_SERVER')
|
MAIL_SERVER = os.getenv('MAIL_SERVER')
|
||||||
@ -34,6 +34,7 @@ class Production(Config):
|
|||||||
class Development(Config):
|
class Development(Config):
|
||||||
APP_HOST = '127.0.0.1'
|
APP_HOST = '127.0.0.1'
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
SERVER_NAME = '127.0.0.1:5000'
|
||||||
SESSION_COOKIE_SECURE = False
|
SESSION_COOKIE_SECURE = False
|
||||||
MAIL_SERVER = 'localhost'
|
MAIL_SERVER = 'localhost'
|
||||||
MAIL_DEBUG = True
|
MAIL_DEBUG = True
|
||||||
|
33
ref-test/app/install.py
Normal file
33
ref-test/app/install.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
from .extensions import db
|
||||||
|
from .tools.data import save
|
||||||
|
from .tools.logs import write
|
||||||
|
|
||||||
|
from sqlalchemy_utils import create_database, database_exists
|
||||||
|
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
|
from os import mkdir, path
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def install_app(app):
|
||||||
|
with app.app_context():
|
||||||
|
data = Path(app.config.get('DATA'))
|
||||||
|
database_uri = app.config.get('SQLALCHEMY_DATABASE_URI')
|
||||||
|
if not path.isdir(f'./{data}'): mkdir(f'./{data}')
|
||||||
|
if not path.isdir(f'./{data}/questions'): mkdir(f'./{data}/questions')
|
||||||
|
if not path.isfile(f'./{data}/.gitignore'):
|
||||||
|
with open(f'./{data}/.gitignore', 'a+') as file: file.write(f'*')
|
||||||
|
if not path.isfile(f'./{data}/config.json'): save({}, 'config.json')
|
||||||
|
if not path.isdir(f'./{data}/logs'): mkdir(f'./{data}/logs')
|
||||||
|
if not path.isfile(f'./{data}/logs/users.log'): write('users.log', 'Log file created.')
|
||||||
|
if not path.isfile(f'./{data}/logs/system.log'): write('system.log', 'Log file created.')
|
||||||
|
if not path.isfile(f'./{data}/logs/tests.log'): write('tests.log', 'Log file created.')
|
||||||
|
if not database_exists(database_uri):
|
||||||
|
create_database(database_uri)
|
||||||
|
write('system.log', 'No database found. Creating a new database.')
|
||||||
|
db.create_all()
|
||||||
|
write('system.log', 'Creating database schema.')
|
||||||
|
if not path.isfile(f'./{data}/.encryption.key'):
|
||||||
|
write('system.log', 'No encryption key found. Generating new encryption key.')
|
||||||
|
with open(f'./{data}/.encryption.key', 'wb') as key_file:
|
||||||
|
key = Fernet.generate_key()
|
||||||
|
key_file.write(key)
|
@ -189,7 +189,10 @@ class User(UserMixin, db.Model):
|
|||||||
if not password and not email: return False, 'There were no changes requested.'
|
if not password and not email: return False, 'There were no changes requested.'
|
||||||
if password: self.set_password(password)
|
if password: self.set_password(password)
|
||||||
old_email = self.get_email()
|
old_email = self.get_email()
|
||||||
if email: self.set_email(email)
|
if email:
|
||||||
|
for entry in User.query.all():
|
||||||
|
if entry.get_email() == email and not entry == self: return False, f'The email address {email} is already in use.'
|
||||||
|
self.set_email(email)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
write('system.log', f'Information for user {self.get_username()} has been updated by {current_user.get_username()}.')
|
write('system.log', f'Information for user {self.get_username()} has been updated by {current_user.get_username()}.')
|
||||||
if notify:
|
if notify:
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from ..forms.quiz import StartQuiz
|
from ..forms.quiz import StartQuiz
|
||||||
from ..models import Entry, Test
|
from ..models import Entry, Test
|
||||||
|
from ..tools.forms import send_errors_to_client
|
||||||
from ..tools.test import redirect_if_started
|
from ..tools.test import redirect_if_started
|
||||||
|
|
||||||
from flask import abort, Blueprint, jsonify, redirect, render_template, request, session
|
from flask import abort, Blueprint, jsonify, redirect, render_template, request, session
|
||||||
@ -52,8 +53,7 @@ def _start():
|
|||||||
'id': entry.id
|
'id': entry.id
|
||||||
}), 200
|
}), 200
|
||||||
return jsonify({'error': 'There was an error processing the user test and/or user codes.'}), 400
|
return jsonify({'error': 'There was an error processing the user test and/or user codes.'}), 400
|
||||||
errors = [*form.test_code.errors, *form.user_code.errors, *form.first_name.errors, *form.surname.errors, *form.email.errors, *form.club.errors]
|
return send_errors_to_client(form=form)
|
||||||
return jsonify({ 'error': errors}), 400
|
|
||||||
return render_template('/quiz/start_quiz.html', form = form)
|
return render_template('/quiz/start_quiz.html', form = form)
|
||||||
|
|
||||||
@quiz.route('/quiz/')
|
@quiz.route('/quiz/')
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
from ..extensions import db
|
from ..extensions import db
|
||||||
|
|
||||||
|
from flask import jsonify
|
||||||
from wtforms.validators import ValidationError
|
from wtforms.validators import ValidationError
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@ -54,3 +55,7 @@ def get_dataset_choices():
|
|||||||
choice = (dataset.id, label)
|
choice = (dataset.id, label)
|
||||||
dataset_choices.append(choice)
|
dataset_choices.append(choice)
|
||||||
return dataset_choices
|
return dataset_choices
|
||||||
|
|
||||||
|
def send_errors_to_client(form):
|
||||||
|
errors = [*form.errors]
|
||||||
|
return jsonify({ 'error': errors}), 400
|
@ -1,86 +1,3 @@
|
|||||||
from app.config import Development as Config
|
from app import create_app
|
||||||
from app.models import Entry, Dataset, Test, User
|
|
||||||
from app.extensions import bootstrap, csrf, db, login_manager, mail
|
|
||||||
from app.tools.data import save
|
|
||||||
from app.tools.logs import write
|
|
||||||
|
|
||||||
from flask import flash, Flask, render_template, request
|
|
||||||
from flask.helpers import url_for
|
|
||||||
from flask.json import jsonify
|
|
||||||
from flask_wtf.csrf import CSRFError
|
|
||||||
from sqlalchemy_utils import database_exists, create_database
|
|
||||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
|
||||||
|
|
||||||
from cryptography.fernet import Fernet
|
|
||||||
from datetime import datetime
|
|
||||||
from os import mkdir, path
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
def create_app():
|
|
||||||
app = Flask(__name__)
|
|
||||||
app.config.from_object(Config())
|
|
||||||
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto= 1, x_host= 1)
|
|
||||||
|
|
||||||
bootstrap.init_app(app)
|
|
||||||
csrf.init_app(app)
|
|
||||||
db.init_app(app)
|
|
||||||
login_manager.init_app(app)
|
|
||||||
mail.init_app(app)
|
|
||||||
|
|
||||||
login_manager.login_view = 'admin._login'
|
|
||||||
@login_manager.user_loader
|
|
||||||
def _load_user(id):
|
|
||||||
return User.query.filter_by(id=id).first()
|
|
||||||
|
|
||||||
@app.before_request
|
|
||||||
def _check_cookie_consent():
|
|
||||||
if request.cookies.get('cookie_consent'):
|
|
||||||
return
|
|
||||||
if any([ request.path.startswith(x) for x in [ '/admin/static/', '/root/', '/quiz/static', '/admin/editor/static/', '/cookies/' ] ]):
|
|
||||||
return
|
|
||||||
flash(f'<strong>Cookie Consent</strong>: This web site only stores minimal, functional cookies. It does not store any tracking information. By using this site, you consent to this use of cookies. For more information, see our <a href="{url_for("views._privacy")}">privacy policy</a>.', 'cookie_alert')
|
|
||||||
|
|
||||||
@app.errorhandler(404)
|
|
||||||
def _404_handler(error): return render_template('404.html')
|
|
||||||
@app.errorhandler(CSRFError)
|
|
||||||
def _csrf_handler(): return jsonify({'error':'Could not validate a secure connection.'}), 403
|
|
||||||
@app.context_processor
|
|
||||||
def _now(): return {'now': datetime.now()}
|
|
||||||
|
|
||||||
from app.admin.views import admin
|
|
||||||
from app.api.views import api
|
|
||||||
from app.quiz.views import quiz
|
|
||||||
from app.views import views
|
|
||||||
from app.editor.views import editor
|
|
||||||
|
|
||||||
app.register_blueprint(admin, url_prefix='/admin')
|
|
||||||
app.register_blueprint(api, url_prefix='/api')
|
|
||||||
app.register_blueprint(views)
|
|
||||||
app.register_blueprint(quiz)
|
|
||||||
app.register_blueprint(editor, url_prefix='/admin/editor')
|
|
||||||
|
|
||||||
data = Path(app.config.get('DATA'))
|
|
||||||
if not path.isdir(f'./{data}'): mkdir(f'./{data}')
|
|
||||||
if not path.isdir(f'./{data}/questions'): mkdir(f'./{data}/questions')
|
|
||||||
if not path.isfile(f'./{data}/.gitignore'):
|
|
||||||
with open(f'./{data}/.gitignore', 'a+') as file: file.write(f'*')
|
|
||||||
if not path.isfile(f'./{data}/config.json'): save({}, 'config.json')
|
|
||||||
if not path.isdir(f'./{data}/logs'): mkdir(f'./{data}/logs')
|
|
||||||
if not path.isfile(f'./{data}/logs/users.log'): write('users.log', 'Log file created.')
|
|
||||||
if not path.isfile(f'./{data}/logs/system.log'): write('system.log', 'Log file created.')
|
|
||||||
if not path.isfile(f'./{data}/logs/tests.log'): write('tests.log', 'Log file created.')
|
|
||||||
if not database_exists(Config.SQLALCHEMY_DATABASE_URI):
|
|
||||||
create_database(Config.SQLALCHEMY_DATABASE_URI)
|
|
||||||
write('system.log', 'No database found. Creating a new database.')
|
|
||||||
with app.app_context(): db.create_all()
|
|
||||||
write('system.log', 'Creating database schema.')
|
|
||||||
if not path.isfile(f'./{data}/.encryption.key'):
|
|
||||||
write('system.log', 'No encryption key found. Generating new encryption key.')
|
|
||||||
with open(f'./{data}/.encryption.key', 'wb') as key_file:
|
|
||||||
key = Fernet.generate_key()
|
|
||||||
key_file.write(key)
|
|
||||||
return app
|
|
||||||
|
|
||||||
app = create_app()
|
app = create_app()
|
||||||
|
|
||||||
if __name__ == '__main__': app.run()
|
if __name__ == '__main__': app.run()
|
Loading…
Reference in New Issue
Block a user