added connection status macro and background task scheduling
This commit is contained in:
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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__)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
BIN
app/static/down.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 492 B |
BIN
app/static/up.png
Normal file
BIN
app/static/up.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 515 B |
12
app/templates/auth/email/confirm.html
Normal file
12
app/templates/auth/email/confirm.html
Normal 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.
|
||||||
14
app/templates/auth/register.html
Normal file
14
app/templates/auth/register.html
Normal 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 %}
|
||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
7
app/templates/macros/connection_status.html
Normal file
7
app/templates/macros/connection_status.html
Normal 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 %}
|
||||||
@@ -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
|
||||||
|
|||||||
6
tox.ini
6
tox.ini
@@ -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
|
||||||
|
|||||||
16
web.py
16
web.py
@@ -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,15 +13,24 @@ 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 '''
|
||||||
import unittest
|
import unittest
|
||||||
tests = unittest.TestLoader().discover('tests')
|
tests = unittest.TestLoader().discover('tests')
|
||||||
unittest.TextTestRunner(verbosity=2).run(tests)
|
unittest.TextTestRunner(verbosity=2).run(tests)
|
||||||
|
|||||||
Reference in New Issue
Block a user