added connection status macro and background task scheduling

This commit is contained in:
Bryan Bailey
2022-04-06 22:38:47 -04:00
parent 4f3e0e6feb
commit 4f3d7175f6
16 changed files with 136 additions and 15 deletions

View File

@@ -10,7 +10,7 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, windows-latest] os: [ubuntu-latest, windows-latest]
python-version: ['3.7', '3.8', '3.9'] python-version: ['3.8', '3.9', '3.10']
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2

View File

@@ -15,5 +15,5 @@ test:
script: script:
- pytest --junitxml report.xml tests/test_basic.py - pytest --junitxml report.xml tests/test_basic.py
- pytest --junitxml report.xml tests/test_web.py - pytest --junitxml report.xml tests/test_web.py
- pytest --junitxml report.xml tests/test_user_model.py - pytest --junitxml report.xml tests/test_model_user.py

View File

@@ -1,9 +1,14 @@
from distutils.command.config import config import atexit
from flask import Flask
from apscheduler.schedulers.background import BackgroundScheduler
import requests
from flask import Flask, current_app, g
from flask_login import LoginManager from flask_login import LoginManager
from flask_mail import Mail from flask_mail import Mail
from flask_moment import Moment from flask_moment import Moment
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from config import configuration, DevelopmentConfig, TestingConfig, ProductionConfig from config import configuration, DevelopmentConfig, TestingConfig, ProductionConfig
@@ -14,6 +19,14 @@ db = SQLAlchemy()
login_manager = LoginManager() login_manager = LoginManager()
login_manager.login_view = 'auth.login' login_manager.login_view = 'auth.login'
def scheduled_task(func, seconds=60, args=None):
scheduler = BackgroundScheduler()
scheduler.add_job(func=func, trigger='interval', seconds=seconds, args=args)
scheduler.start()
atexit.register(lambda: scheduler.shutdown())
def create_app(config_name): def create_app(config_name):
app = Flask(__name__) app = Flask(__name__)

View File

@@ -1,13 +1,26 @@
from flask import render_template, redirect, request, url_for, flash from flask import render_template, redirect, request, url_for, flash
from flask_login import login_user, logout_user, login_required from flask_login import login_user, logout_user, login_required, current_user
from app import db from app import db
from app.auth import auth from app.auth import auth
from app.email import send_email
from app.models import User from app.models import User
from app.auth.forms import LoginForm, RegistrationForm from app.auth.forms import LoginForm, RegistrationForm
@auth.before_app_request
def before_request():
if current_user.is_authenticated and not current_user.confirmed and request.blueprint != 'auth' and request.endpoint != 'static':
return redirect(url_for('auth.unconfirmed'))
@auth.route('/unconfirmed')
def unconfirmed():
if current_user.is_anonymous or current_user.confirmed:
return redirect(url_for('main.index'))
return render_template('auth/unconfirmed.html')
@auth.route('/login', methods=['get', 'post']) @auth.route('/login', methods=['get', 'post'])
def login(): def login():
form = LoginForm() form = LoginForm()
@@ -43,8 +56,46 @@ def register():
if form.validate_on_submit(): if form.validate_on_submit():
user = User(email=form.email.data, username=form.username.data, password=form.password.data) user = User(email=form.email.data, username=form.username.data, password=form.password.data)
db.session.add(user) db.session.add(user)
db.session.commit() db.session.commit()
flash('You can now login', category='success')
token = user.generate_confirmation_token()
send_email(user.email, 'Confirm Your Yo!nk Account', 'auth/email/confirm', user=user, token=token)
flash(f'A confirmation email was sent to {user.email}', category='success')
return redirect(url_for('auth.login')) return redirect(url_for('auth.login'))
return render_template('auth/register.html', form=form) return render_template('auth/register.html', form=form)
@auth.route('/confirm/<token>')
@login_required
def confirm(token):
if current_user.confirmed:
return redirect(url_for('main.index'))
if current_user.confirm(token):
db.session.commit()
flash('Your account has been confirmed. Thank you!')
else:
flash('Invalid or expired confirmation link')
return redirect(url_for('main.index'))
@auth.route('/confirm')
@login_required
def resend_confirm():
token = current_user.generate_confirmation_token()
send_email(current_user.email, 'Confirm Your Yo!nk Account', 'auth/email/confirm', user=current_user, token=token)
flash(f'A new confirmation email was sent to {current_user.email}', category='success')
return redirect(url_for('main.index'))
# TODO: password updates
# TODO: password reset
# TODO: email updates

View File

@@ -3,7 +3,7 @@ import threading
from flask import current_app, render_template from flask import current_app, render_template
from flask_mail import Message from flask_mail import Message
from app import mail, config from app import mail
def send_async_email(app, msg): def send_async_email(app, msg):

View File

@@ -1,7 +1,7 @@
import os import os
from datetime import datetime from datetime import datetime
from flask import render_template, session, send_from_directory, redirect, url_for, flash from flask import render_template, session, send_from_directory, redirect, url_for, flash, g
from app.main import main from app.main import main
from app.main.forms import DownloadForm from app.main.forms import DownloadForm
@@ -89,7 +89,7 @@ def index():
latest = get_comic_library_meta() latest = get_comic_library_meta()
form.url.data = '' form.url.data = ''
return render_template('index.html', form=form, url=url, series=series, latest=latest), 200 return render_template('index.html', form=form, url=url, series=series, latest=latest, status=g.status_code), 200
if form.series.data: if form.series.data:

BIN
app/static/down.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 B

BIN
app/static/up.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 B

View File

@@ -0,0 +1,12 @@
Welcome to Yo!nk {{ user.username }},
To confirm your account please click on the following link:
{{ url_for('auth.confirm', token=token, _external=True) }}
Sincerely,
The Yo!nk Team
Note: replies to this email address are unmonitored.

View File

@@ -0,0 +1,14 @@
{% extends "base.html" %}
{% block meta %}
{{ super() }}
<!-- Add additional meta tags here -->
{% endblock %}
{% block title %}Testing{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Register</h1>
</div>
{% endblock %}

View File

@@ -1,3 +1,5 @@
{% import 'macros/connection_status.html' as connection %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
@@ -15,7 +17,7 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.min.js" integrity="sha384-QJHtvGhmr9XOIpI6YVutG+2QOK9T+ZnN4kzFN1RtK3zEFEIsxhlmWl5/YESvpZ13" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.min.js" integrity="sha384-QJHtvGhmr9XOIpI6YVutG+2QOK9T+ZnN4kzFN1RtK3zEFEIsxhlmWl5/YESvpZ13" crossorigin="anonymous"></script>
{{ moment.include_moment() }} {{ moment.include_moment() }}
{% endblock %} {% endblock %}
<link rel="shortcut icon" href="{{ url_for('static', filename='images/comic.png') }}" type="image/x-icon"> <link rel="shortcut icon" href="{{ connection.toggle_connection_status(status_code) }}" type="image/x-icon">
</head> </head>
<body> <body>
{% block navbar %} {% block navbar %}

View File

@@ -1,5 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% import 'macros/library.html' as library %} {% import 'macros/library.html' as library %}
{% import 'macros/connection_status.html' as connection %}
{% block meta %} {% block meta %}
{{ super() }} {{ super() }}
@@ -12,7 +14,6 @@
<div class="page-header"> <div class="page-header">
<h1>Y!oink Web App</h1> <h1>Y!oink Web App</h1>
</div> </div>
<form method="post" onsubmit="download.disabled = true; url.readOnly = true; return true;"> <form method="post" onsubmit="download.disabled = true; url.readOnly = true; return true;">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
<div class="input-group mb-3"> <div class="input-group mb-3">

View File

@@ -0,0 +1,7 @@
{% macro toggle_connection_status(status_code) %}
{% if status_code == 200 %}
{{ url_for('static', filename='up.png') }}
{% else %}
{{ url_for('static', filename='down.png') }}
{% endif %}
{% endmacro %}

View File

@@ -1,4 +1,5 @@
alembic==1.7.7 alembic==1.7.7
APScheduler==3.9.1
asgiref==3.5.0 asgiref==3.5.0
attrs==21.4.0 attrs==21.4.0
beautifulsoup4==4.10.0 beautifulsoup4==4.10.0
@@ -34,6 +35,8 @@ pluggy==1.0.0
py==1.11.0 py==1.11.0
pyparsing==3.0.7 pyparsing==3.0.7
pytest==7.1.0 pytest==7.1.0
pytz==2022.1
pytz-deprecation-shim==0.1.0.post0
requests==2.27.1 requests==2.27.1
six==1.16.0 six==1.16.0
soupsieve==2.3.1 soupsieve==2.3.1
@@ -41,6 +44,8 @@ SQLAlchemy==1.4.34
toml==0.10.2 toml==0.10.2
tomli==2.0.1 tomli==2.0.1
tox==3.24.5 tox==3.24.5
tzdata==2022.1
tzlocal==4.2
urllib3==1.26.8 urllib3==1.26.8
virtualenv==20.14.0 virtualenv==20.14.0
Werkzeug==2.1.0 Werkzeug==2.1.0

View File

@@ -1,13 +1,13 @@
[tox] [tox]
minversion = 3.8.0 minversion = 3.8.0
envlist = py37, py38, py39 envlist = py38, py39, py310
isolated_build = true isolated_build = true
[gh-actions] [gh-actions]
python = python =
3.7: py37
3.8: py38 3.8: py38
3.9: py39 3.9: py39
3.10: py310
[testenv] [testenv]
setenv = setenv =
@@ -16,3 +16,5 @@ deps =
-r{toxinidir}/requirements_dev.txt -r{toxinidir}/requirements_dev.txt
commands = commands =
pytest --junitxml report.xml yoink/tests/test_basic.py pytest --junitxml report.xml yoink/tests/test_basic.py
pytest --junitxml report.xml tests/test_web.py
pytest --junitxml report.xml tests/test_model_user.py

14
web.py
View File

@@ -1,5 +1,10 @@
import atexit
import click
import os import os
import time
import requests
from apscheduler.schedulers.background import BackgroundScheduler
from flask_migrate import Migrate from flask_migrate import Migrate
from app import create_app, db from app import create_app, db
@@ -8,12 +13,21 @@ from yoink.config import config
from yoink.comic import Comic from yoink.comic import Comic
def ping(url: str):
''' check url for connection status'''
return requests.get(url).status_code
app = create_app(os.environ.get('FLASK_CONFIG') or 'default') app = create_app(os.environ.get('FLASK_CONFIG') or 'default')
migrate = Migrate(app, db) migrate = Migrate(app, db)
@app.shell_context_processor @app.shell_context_processor
def make_shell_ctx(): return dict(db=db, User=User, Role=Role, ComicMeta=ComicMeta) def make_shell_ctx(): return dict(db=db, User=User, Role=Role, ComicMeta=ComicMeta)
@app.context_processor
def inject_status_code():
return dict(status_code=ping('http://readallcomics.com'))
@app.cli.command() @app.cli.command()
def test(): def test():
''' run unit tests ''' ''' run unit tests '''