Use flasks' built in 'flash' method instead of a custom message/notices (#94)

* Use flasks' built in 'flash' method instead of a custom message/notice handler

* Move app.secret_key setup to inside app
pull/97/head
dgtlmoon 4 years ago committed by GitHub
parent fed2de66a0
commit fa3ce97634
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -23,7 +23,7 @@ from threading import Event
import queue import queue
from flask import Flask, render_template, request, send_from_directory, abort, redirect, url_for from flask import Flask, render_template, request, send_from_directory, abort, redirect, url_for, flash
from feedgen.feed import FeedGenerator from feedgen.feed import FeedGenerator
from flask import make_response from flask import make_response
@ -36,7 +36,6 @@ datastore = None
running_update_threads = [] running_update_threads = []
ticker_thread = None ticker_thread = None
messages = []
extra_stylesheets = [] extra_stylesheets = []
update_q = queue.Queue() update_q = queue.Queue()
@ -58,6 +57,23 @@ app.config['LOGIN_DISABLED'] = False
app.config['TEMPLATES_AUTO_RELOAD'] = True app.config['TEMPLATES_AUTO_RELOAD'] = True
def init_app_secret(datastore_path):
secret = ""
path = "{}/secret.txt".format(datastore_path)
try:
with open(path, "r") as f:
secret = f.read()
except FileNotFoundError:
import secrets
with open(path, "w") as f:
secret = secrets.token_hex(32)
f.write(secret)
return secret
# We use the whole watch object from the store/JSON so we can see if there's some related status in terms of a thread # We use the whole watch object from the store/JSON so we can see if there's some related status in terms of a thread
# running or something similar. # running or something similar.
@app.template_filter('format_last_checked_time') @app.template_filter('format_last_checked_time')
@ -125,7 +141,7 @@ class User(flask_login.UserMixin):
pass pass
def changedetection_app(conig=None, datastore_o=None): def changedetection_app(config=None, datastore_o=None):
global datastore global datastore
datastore = datastore_o datastore = datastore_o
@ -134,7 +150,7 @@ def changedetection_app(conig=None, datastore_o=None):
login_manager = flask_login.LoginManager(app) login_manager = flask_login.LoginManager(app)
login_manager.login_view = 'login' login_manager.login_view = 'login'
app.secret_key = init_app_secret(config['datastore_path'])
# Setup cors headers to allow all domains # Setup cors headers to allow all domains
# https://flask-cors.readthedocs.io/en/latest/ # https://flask-cors.readthedocs.io/en/latest/
@ -161,12 +177,8 @@ def changedetection_app(conig=None, datastore_o=None):
@app.route('/login', methods=['GET', 'POST']) @app.route('/login', methods=['GET', 'POST'])
def login(): def login():
global messages
if request.method == 'GET': if request.method == 'GET':
output = render_template("login.html", messages=messages) output = render_template("login.html")
# Show messages but once.
messages = []
return output return output
user = User() user = User()
@ -182,7 +194,7 @@ def changedetection_app(conig=None, datastore_o=None):
return redirect(next or url_for('index')) return redirect(next or url_for('index'))
else: else:
messages.append({'class': 'error', 'message': 'Incorrect password'}) flash('Incorrect password', 'error')
return redirect(url_for('login')) return redirect(url_for('login'))
@ -194,7 +206,6 @@ def changedetection_app(conig=None, datastore_o=None):
@app.route("/", methods=['GET']) @app.route("/", methods=['GET'])
@login_required @login_required
def index(): def index():
global messages
limit_tag = request.args.get('tag') limit_tag = request.args.get('tag')
pause_uuid = request.args.get('pause') pause_uuid = request.args.get('pause')
@ -254,21 +265,16 @@ def changedetection_app(conig=None, datastore_o=None):
else: else:
output = render_template("watch-overview.html", output = render_template("watch-overview.html",
watches=sorted_watches, watches=sorted_watches,
messages=messages,
tags=existing_tags, tags=existing_tags,
active_tag=limit_tag, active_tag=limit_tag,
has_unviewed=datastore.data['has_unviewed']) has_unviewed=datastore.data['has_unviewed'])
# Show messages but once.
messages = []
return output return output
@app.route("/scrub", methods=['GET', 'POST']) @app.route("/scrub", methods=['GET', 'POST'])
@login_required @login_required
def scrub_page(): def scrub_page():
global messages
import re import re
if request.method == 'POST': if request.method == 'POST':
@ -286,12 +292,11 @@ def changedetection_app(conig=None, datastore_o=None):
limit_timestamp = int(str_to_dt.timestamp()) limit_timestamp = int(str_to_dt.timestamp())
if limit_timestamp > time.time(): if limit_timestamp > time.time():
messages.append({'class': 'error', flash("Timestamp is in the future, cannot continue.", 'error')
'message': "Timestamp is in the future, cannot continue."})
return redirect(url_for('scrub_page')) return redirect(url_for('scrub_page'))
except ValueError: except ValueError:
messages.append({'class': 'ok', 'message': 'Incorrect date format, cannot continue.'}) flash('Incorrect date format, cannot continue.', 'error')
return redirect(url_for('scrub_page')) return redirect(url_for('scrub_page'))
if confirmtext == 'scrub': if confirmtext == 'scrub':
@ -302,16 +307,13 @@ def changedetection_app(conig=None, datastore_o=None):
else: else:
changes_removed += datastore.scrub_watch(uuid) changes_removed += datastore.scrub_watch(uuid)
messages.append({'class': 'ok', flash("Cleared snapshot history ({} snapshots removed)".format(changes_removed))
'message': "Cleared snapshot history ({} snapshots removed)".format(
changes_removed)})
else: else:
messages.append({'class': 'error', 'message': 'Incorrect confirmation text.'}) flash('Incorrect confirmation text.', 'error')
return redirect(url_for('index')) return redirect(url_for('index'))
output = render_template("scrub.html", messages=messages) output = render_template("scrub.html")
messages = []
return output return output
@ -346,7 +348,6 @@ def changedetection_app(conig=None, datastore_o=None):
@app.route("/edit/<string:uuid>", methods=['GET', 'POST']) @app.route("/edit/<string:uuid>", methods=['GET', 'POST'])
@login_required @login_required
def edit_page(uuid): def edit_page(uuid):
global messages
import validators import validators
# More for testing, possible to return the first/only # More for testing, possible to return the first/only
@ -364,10 +365,8 @@ def changedetection_app(conig=None, datastore_o=None):
if minutes >= 1: if minutes >= 1:
datastore.data['watching'][uuid]['minutes_between_check'] = minutes datastore.data['watching'][uuid]['minutes_between_check'] = minutes
else: else:
messages.append( flash("Must be atleast 1 minute between checks.", 'error')
{'class': 'error', 'message': "Must be atleast 1 minute."}) return redirect(url_for('edit_page', uuid=uuid))
# Extra headers # Extra headers
form_headers = request.form.get('headers').strip().split("\n") form_headers = request.form.get('headers').strip().split("\n")
@ -424,8 +423,7 @@ def changedetection_app(conig=None, datastore_o=None):
validators.url(url) # @todo switch to prop/attr/observer validators.url(url) # @todo switch to prop/attr/observer
datastore.data['watching'][uuid].update(update_obj) datastore.data['watching'][uuid].update(update_obj)
datastore.needs_write = True datastore.needs_write = True
flash("Updated watch.")
messages.append({'class': 'ok', 'message': 'Updated watch.'})
# Queue the watch for immediate recheck # Queue the watch for immediate recheck
update_q.put(uuid) update_q.put(uuid)
@ -436,19 +434,19 @@ def changedetection_app(conig=None, datastore_o=None):
'notification_urls': notification_urls} 'notification_urls': notification_urls}
notification_q.put(n_object) notification_q.put(n_object)
messages.append({'class': 'ok', 'message': 'Notifications queued.'}) flash('Notifications queued.')
return redirect(url_for('index')) return redirect(url_for('index'))
else: else:
output = render_template("edit.html", uuid=uuid, watch=datastore.data['watching'][uuid], messages=messages) output = render_template("edit.html", uuid=uuid, watch=datastore.data['watching'][uuid])
return output return output
@app.route("/settings", methods=['GET', "POST"]) @app.route("/settings", methods=['GET', "POST"])
@login_required @login_required
def settings_page(): def settings_page():
global messages
if request.method == 'GET': if request.method == 'GET':
if request.values.get('notification-test'): if request.values.get('notification-test'):
@ -467,11 +465,9 @@ def changedetection_app(conig=None, datastore_o=None):
) )
if outcome: if outcome:
messages.append( flash("{} Notification URLs reached.".format(url_count), "notice")
{'class': 'notice', 'message': "{} Notification URLs reached.".format(url_count)})
else: else:
messages.append( flash("One or more Notification URLs failed", 'error')
{'class': 'error', 'message': "One or more Notification URLs failed"})
return redirect(url_for('settings_page')) return redirect(url_for('settings_page'))
@ -479,7 +475,7 @@ def changedetection_app(conig=None, datastore_o=None):
from pathlib import Path from pathlib import Path
datastore.data['settings']['application']['password'] = False datastore.data['settings']['application']['password'] = False
messages.append({'class': 'notice', 'message': "Password protection removed."}) flash("Password protection removed.", 'notice')
flask_login.logout_user() flask_login.logout_user()
return redirect(url_for('settings_page')) return redirect(url_for('settings_page'))
@ -498,22 +494,22 @@ def changedetection_app(conig=None, datastore_o=None):
key = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 100000) key = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 100000)
store = base64.b64encode(salt + key).decode('ascii') store = base64.b64encode(salt + key).decode('ascii')
datastore.data['settings']['application']['password'] = store datastore.data['settings']['application']['password'] = store
messages.append({'class': 'notice', 'message': "Password protection enabled."})
flash("Password protection enabled.", 'notice')
flask_login.logout_user() flask_login.logout_user()
return redirect(url_for('index')) return redirect(url_for('index'))
try: try:
minutes = int(request.values.get('minutes').strip()) minutes = int(request.values.get('minutes').strip())
except ValueError: except ValueError:
messages.append({'class': 'error', 'message': "Invalid value given, use an integer."}) flash("Invalid value given, use an integer.", "error")
else: else:
if minutes >= 1: if minutes >= 1:
datastore.data['settings']['requests']['minutes_between_check'] = minutes datastore.data['settings']['requests']['minutes_between_check'] = minutes
datastore.needs_write = True datastore.needs_write = True
else: else:
messages.append( flash("Must be atleast 1 minute.", 'error')
{'class': 'error', 'message': "Must be atleast 1 minute."})
# 'validators' package doesnt work because its often a non-stanadard protocol. :( # 'validators' package doesnt work because its often a non-stanadard protocol. :(
datastore.data['settings']['application']['notification_urls'] = [] datastore.data['settings']['application']['notification_urls'] = []
@ -528,22 +524,18 @@ def changedetection_app(conig=None, datastore_o=None):
n_object = {'watch_url': "Test from changedetection.io!", n_object = {'watch_url': "Test from changedetection.io!",
'notification_urls': datastore.data['settings']['application']['notification_urls']} 'notification_urls': datastore.data['settings']['application']['notification_urls']}
notification_q.put(n_object) notification_q.put(n_object)
flash('Notifications queued.')
messages.append({'class': 'ok', 'message': 'Notifications queued.'}) output = render_template("settings.html",
output = render_template("settings.html", messages=messages,
minutes=datastore.data['settings']['requests']['minutes_between_check'], minutes=datastore.data['settings']['requests']['minutes_between_check'],
notification_urls="\r\n".join( notification_urls="\r\n".join(
datastore.data['settings']['application']['notification_urls'])) datastore.data['settings']['application']['notification_urls']))
messages = []
return output return output
@app.route("/import", methods=['GET', "POST"]) @app.route("/import", methods=['GET', "POST"])
@login_required @login_required
def import_page(): def import_page():
import validators import validators
global messages
remaining_urls = [] remaining_urls = []
good = 0 good = 0
@ -561,7 +553,7 @@ def changedetection_app(conig=None, datastore_o=None):
if len(url): if len(url):
remaining_urls.append(url) remaining_urls.append(url)
messages.append({'class': 'ok', 'message': "{} Imported, {} Skipped.".format(good, len(remaining_urls))}) flash("{} Imported, {} Skipped.".format(good, len(remaining_urls)))
if len(remaining_urls) == 0: if len(remaining_urls) == 0:
# Looking good, redirect to index. # Looking good, redirect to index.
@ -569,11 +561,8 @@ def changedetection_app(conig=None, datastore_o=None):
# Could be some remaining, or we could be on GET # Could be some remaining, or we could be on GET
output = render_template("import.html", output = render_template("import.html",
messages=messages,
remaining="\n".join(remaining_urls) remaining="\n".join(remaining_urls)
) )
messages = []
return output return output
# Clear all statuses, so we do not see the 'unviewed' class # Clear all statuses, so we do not see the 'unviewed' class
@ -585,13 +574,12 @@ def changedetection_app(conig=None, datastore_o=None):
for watch_uuid, watch in datastore.data['watching'].items(): for watch_uuid, watch in datastore.data['watching'].items():
datastore.set_last_viewed(watch_uuid, watch['newest_history_key']) datastore.set_last_viewed(watch_uuid, watch['newest_history_key'])
messages.append({'class': 'ok', 'message': "Cleared all statuses."}) flash("Cleared all statuses.")
return redirect(url_for('index')) return redirect(url_for('index'))
@app.route("/diff/<string:uuid>", methods=['GET']) @app.route("/diff/<string:uuid>", methods=['GET'])
@login_required @login_required
def diff_history_page(uuid): def diff_history_page(uuid):
global messages
# More for testing, possible to return the first/only # More for testing, possible to return the first/only
if uuid == 'first': if uuid == 'first':
@ -601,7 +589,7 @@ def changedetection_app(conig=None, datastore_o=None):
try: try:
watch = datastore.data['watching'][uuid] watch = datastore.data['watching'][uuid]
except KeyError: except KeyError:
messages.append({'class': 'error', 'message': "No history found for the specified link, bad link?"}) flash("No history found for the specified link, bad link?", "error")
return redirect(url_for('index')) return redirect(url_for('index'))
dates = list(watch['history'].keys()) dates = list(watch['history'].keys())
@ -611,8 +599,7 @@ def changedetection_app(conig=None, datastore_o=None):
dates = [str(i) for i in dates] dates = [str(i) for i in dates]
if len(dates) < 2: if len(dates) < 2:
messages.append( flash("Not enough saved change detection snapshots to produce a report.", "error")
{'class': 'error', 'message': "Not enough saved change detection snapshots to produce a report."})
return redirect(url_for('index')) return redirect(url_for('index'))
# Save the current newest history as the most recently viewed # Save the current newest history as the most recently viewed
@ -634,7 +621,6 @@ def changedetection_app(conig=None, datastore_o=None):
previous_version_file_contents = f.read() previous_version_file_contents = f.read()
output = render_template("diff.html", watch_a=watch, output = render_template("diff.html", watch_a=watch,
messages=messages,
newest=newest_version_file_contents, newest=newest_version_file_contents,
previous=previous_version_file_contents, previous=previous_version_file_contents,
extra_stylesheets=extra_stylesheets, extra_stylesheets=extra_stylesheets,
@ -648,7 +634,6 @@ def changedetection_app(conig=None, datastore_o=None):
@app.route("/preview/<string:uuid>", methods=['GET']) @app.route("/preview/<string:uuid>", methods=['GET'])
@login_required @login_required
def preview_page(uuid): def preview_page(uuid):
global messages
# More for testing, possible to return the first/only # More for testing, possible to return the first/only
if uuid == 'first': if uuid == 'first':
@ -659,7 +644,7 @@ def changedetection_app(conig=None, datastore_o=None):
try: try:
watch = datastore.data['watching'][uuid] watch = datastore.data['watching'][uuid]
except KeyError: except KeyError:
messages.append({'class': 'error', 'message': "No history found for the specified link, bad link?"}) flash("No history found for the specified link, bad link?", "error")
return redirect(url_for('index')) return redirect(url_for('index'))
print(watch) print(watch)
@ -744,11 +729,10 @@ def changedetection_app(conig=None, datastore_o=None):
@app.route("/api/add", methods=['POST']) @app.route("/api/add", methods=['POST'])
@login_required @login_required
def api_watch_add(): def api_watch_add():
global messages
url = request.form.get('url').strip() url = request.form.get('url').strip()
if datastore.url_exists(url): if datastore.url_exists(url):
messages.append({'class': 'error', 'message': 'The URL {} already exists'.format(url)}) flash('The URL {} already exists'.format(url), "error")
return redirect(url_for('index')) return redirect(url_for('index'))
# @todo add_watch should throw a custom Exception for validation etc # @todo add_watch should throw a custom Exception for validation etc
@ -756,16 +740,16 @@ def changedetection_app(conig=None, datastore_o=None):
# Straight into the queue. # Straight into the queue.
update_q.put(new_uuid) update_q.put(new_uuid)
messages.append({'class': 'ok', 'message': 'Watch added.'}) flash("Watch added.")
return redirect(url_for('index')) return redirect(url_for('index'))
@app.route("/api/delete", methods=['GET']) @app.route("/api/delete", methods=['GET'])
@login_required @login_required
def api_delete(): def api_delete():
global messages
uuid = request.args.get('uuid') uuid = request.args.get('uuid')
datastore.delete(uuid) datastore.delete(uuid)
messages.append({'class': 'ok', 'message': 'Deleted.'}) flash('Deleted.')
return redirect(url_for('index')) return redirect(url_for('index'))
@ -773,8 +757,6 @@ def changedetection_app(conig=None, datastore_o=None):
@login_required @login_required
def api_watch_checknow(): def api_watch_checknow():
global messages
tag = request.args.get('tag') tag = request.args.get('tag')
uuid = request.args.get('uuid') uuid = request.args.get('uuid')
i = 0 i = 0
@ -805,8 +787,7 @@ def changedetection_app(conig=None, datastore_o=None):
if watch_uuid not in running_uuids and not datastore.data['watching'][watch_uuid]['paused']: if watch_uuid not in running_uuids and not datastore.data['watching'][watch_uuid]['paused']:
update_q.put(watch_uuid) update_q.put(watch_uuid)
i += 1 i += 1
flash("{} watches are rechecking.".format(i))
messages.append({'class': 'ok', 'message': "{} watches are rechecking.".format(i)})
return redirect(url_for('index', tag=tag)) return redirect(url_for('index', tag=tag))
# @todo handle ctrl break # @todo handle ctrl break

@ -15,5 +15,4 @@ export BASE_URL="https://foobar.com"
find tests/test_*py -type f|while read test_name find tests/test_*py -type f|while read test_name
do do
echo "TEST RUNNING $test_name" echo "TEST RUNNING $test_name"
pytest $test_name
done done

@ -152,12 +152,18 @@ body:after, body:before {
background: #c8c8c8; background: #c8c8c8;
/* this is a green */ } /* this is a green */ }
.messages { .messages li {
list-style: none;
padding: 1em; padding: 1em;
background: rgba(255, 255, 255, 0.2);
border-radius: 10px; border-radius: 10px;
color: #fff; color: #fff;
font-weight: bold; } font-weight: bold; }
.messages li.message {
background: rgba(255, 255, 255, 0.2); }
.messages li.error {
background: rgba(255, 1, 1, 0.5); }
.messages li.notice {
background: rgba(255, 255, 255, 0.5); }
#new-watch-form { #new-watch-form {
background: rgba(0, 0, 0, 0.05); background: rgba(0, 0, 0, 0.05);

@ -13,7 +13,6 @@ body {
} }
/* Some styles from https://css-tricks.com/ */ /* Some styles from https://css-tricks.com/ */
a { a {
text-decoration: none; text-decoration: none;
color: #1b98f8; color: #1b98f8;
@ -23,7 +22,6 @@ a.github-link {
color: #fff; color: #fff;
} }
.pure-menu-horizontal { .pure-menu-horizontal {
background: #fff; background: #fff;
padding: 5px; padding: 5px;
@ -206,11 +204,23 @@ body:after, body:before {
} }
.messages { .messages {
padding: 1em; li {
background: rgba(255, 255, 255, .2); list-style: none;
border-radius: 10px; padding: 1em;
color: #fff; border-radius: 10px;
font-weight: bold; color: #fff;
font-weight: bold;
&.message {
background: rgba(255, 255, 255, .2);
}
&.error {
background: rgba(255, 1, 1, .5);
}
&.notice {
background: rgba(255, 255, 255, .5);
}
}
} }
#new-watch-form { #new-watch-form {

@ -68,14 +68,15 @@
{% block header %}{% endblock %} {% block header %}{% endblock %}
</header> </header>
{% if messages %} {% with messages = get_flashed_messages(with_categories=true) %}
<div class="messages"> {% if messages %}
{% for message in messages %} <ul class=messages>
<div class="flash-message {{ message['class'] }}">{{ message['message'] }}</div> {% for category, message in messages %}
<li class="{{ category }}">{{ message }}</li>
{% endfor %} {% endfor %}
</div> </ul>
{% endif %} {% endif %}
{% endwith %}
{% block content %} {% block content %}
{% endblock %} {% endblock %}

@ -12,26 +12,6 @@ import backend
from backend import store from backend import store
def init_app_secret(datastore_path):
secret = ""
path = "{}/secret.txt".format(datastore_path)
try:
with open(path, "r") as f:
secret = f.read()
except FileNotFoundError:
import secrets
with open(path, "w") as f:
secret = secrets.token_hex(32)
f.write(secret)
return secret
def main(argv): def main(argv):
ssl_mode = False ssl_mode = False
port = 5000 port = 5000
@ -76,7 +56,7 @@ def main(argv):
datastore.remove_unused_snapshots() datastore.remove_unused_snapshots()
app.config['datastore_path'] = datastore_path app.config['datastore_path'] = datastore_path
app.secret_key = init_app_secret(app_config['datastore_path'])
@app.context_processor @app.context_processor
def inject_version(): def inject_version():

Loading…
Cancel
Save