edit settings dynamically

This commit is contained in:
codl 2017-08-12 01:04:22 +02:00
parent ced259d7e8
commit d07503dd6f
No known key found for this signature in database
GPG Key ID: 6CD7C8891ED1233A
9 changed files with 218 additions and 31 deletions

111
assets/settings_form.js Normal file
View File

@ -0,0 +1,111 @@
(function(){
if(!('fetch' in window)){
return;
}
let status_timeout = null;
let form = document.forms.settings;
let backoff_level = 0
function hide_status(){
status_display.classList.remove('error', 'success', 'saving');
status_display.classList.add('hidden');
status_display.innerHTML='';
}
function show_error(e){
hide_status();
status_display.textContent='Could not save. Retrying...';
status_display.classList.add('error');
status_display.classList.remove('hidden');
}
function show_success(){
hide_status();
status_display.textContent='Saved!';
status_display.classList.add('success');
status_display.classList.remove('hidden');
}
function show_saving(){
hide_status();
status_display.textContent='Saving...';
status_display.classList.add('saving');
status_display.classList.remove('hidden');
status_timeout = setTimeout(show_still_saving, 5000);
}
function show_still_saving(){
status_display.textContent='Still saving...';
}
function on_change(e){
hide_status();
clearTimeout(status_timeout);
status_timeout = setTimeout(show_saving, 70);
send_settings(get_all_inputs())
.then(data => {
show_success();
clearTimeout(status_timeout);
status_timeout = setTimeout(hide_status, 3000);
backoff_level = 0;
})
.catch(e => {
console.error('Fetch rejected:', e);
show_error();
clearTimeout(status_timeout);
status_timeout = setTimeout(save, Math.pow(2, backoff_level)*1000);
backoff_level += 1;
backoff_level = Math.min(backoff_level, 5);
});
// remove server-rendered banner
let settings_section = document.querySelector('#settings-section');
let banner = settings_section.querySelector('.banner');
if(banner){
settings_section.removeChild(banner);
}
}
function get_all_inputs(){
let o = Object()
for(input of form.elements){
if(input.type != 'radio' || input.checked){
o[input.name] = input.value;
}
}
return o
}
function send_settings(body){
return fetch('/api/settings', {
method:'PUT',
credentials:'same-origin',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify(body)
})
.then(resp => { if(!resp.ok){ return Promise.reject(resp) }; return resp; })
.then(resp => resp.json())
.then(data => {
if(data.status == 'error'){ return Promise.reject(data) }
return data
});
}
for(input of form.elements){
input.addEventListener('change', on_change);
input.addEventListener('change', e=>console.log(e.target));
}
// remove submit button since we're doing live updates
let submit = form.querySelector('input[type=submit]');
form.removeChild(submit);
let status_display = document.createElement('span');
status_display.classList.add('status-display', 'hidden');
let settings_title = document.querySelector('#settings-title');
settings_title.appendChild(status_display);
// silently send_settings in case the user changed settings while the page was loading
send_settings(get_all_inputs());
})();

View File

@ -82,17 +82,17 @@ body > section > .banner {
background: #eee; background: #eee;
} }
.banner.success { .success {
border-color: #4a2;
background: #dec; background: #dec;
border-left-color: #4a2;
} }
.banner.error { .error {
border-color: #a24; border-color: #a24;
background: #ecd; background: #ecd;
} }
.banner.warning { .warning {
border-color: #dc4; border-color: #dc4;
background: #eec; background: #eec;
} }
@ -126,3 +126,57 @@ footer a {
color: inherit; color: inherit;
} }
.status-display {
font-size: 0.8rem;
display: inline-block;
margin-left: 1ch;
padding: 0 0.4em;
vertical-align: top;
}
.status-display.hidden {
display: none;
}
.status-display.error {
background: #ecd;
}
.status-display.success {
display: inline-block;
background: #dec;
animation: fade-background 2s forwards;
}
@keyframes fade-background {
to {
background: transparent;
}
}
.status-display::before {
content: "";
display: inline-block;
border-radius: 50%;
width: 0.8em;
height: 0.8em;
border-color: inherit;
border-style: solid;
border-width: 0.4em;
margin-right: 0.3em;
}
.status-display.saving::before {
border-color: #666;
border-width: 2px;
animation: blink infinite 0.6s steps(1) normal;
}
@keyframes blink {
50% {
opacity: 0;
}
}

View File

@ -23,7 +23,7 @@ def task_gen_logo():
def task_copy_asset(): def task_copy_asset():
import shutil import shutil
assets = ('icon.png', 'logotype.png', 'version.js') assets = ('icon.png', 'logotype.png', 'version.js', 'settings_form.js')
for asset in assets: for asset in assets:
yield dict( yield dict(
name=asset, name=asset,

View File

@ -1,6 +1,7 @@
from .auth import require_auth from . import auth
from .interval import decompose_interval from .interval import decompose_interval
from .interval import SCALES as interval_scales from .interval import SCALES as interval_scales
from .cachebust import cachebust from .cachebust import cachebust
from .session import set_session_cookie, get_viewer_session, get_viewer from .session import set_session_cookie, get_viewer_session, get_viewer
from . import brotli from . import brotli
from . import settings

View File

@ -1,18 +1,20 @@
from flask import g, redirect from flask import g, redirect, jsonify, make_response
from functools import wraps
def require_auth(fun, redir=True): def require_auth(fun):
@wraps(fun)
from functools import update_wrapper
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
if not g.viewer: if not g.viewer:
if redir: return redirect('/')
return redirect('/') return fun(*args, **kwargs)
else: return wrapper
return 403
else:
return fun(*args, **kwargs)
update_wrapper(wrapper, fun) def require_auth_api(fun):
@wraps(fun)
def wrapper(*args, **kwargs):
if not g.viewer:
return make_response((jsonify(status='error', error='not logged in'), 403))
return fun(*args, **kwargs)
return wrapper return wrapper

9
lib/settings.py Normal file
View File

@ -0,0 +1,9 @@
attrs = (
'policy_keep_favourites',
'policy_keep_latest',
'policy_delete_every_scale',
'policy_delete_every_significand',
'policy_keep_younger_scale',
'policy_keep_younger_significand',
'policy_keep_media',
)

View File

@ -2,9 +2,9 @@ from flask import render_template, url_for, redirect, request, g, Response, json
from datetime import datetime, timedelta from datetime import datetime, timedelta
import lib.twitter import lib.twitter
import lib import lib
from lib import require_auth from lib.auth import require_auth, require_auth_api
from lib import set_session_cookie from lib import set_session_cookie
from lib import get_viewer_session from lib import get_viewer_session, get_viewer
from model import Account, Session, Post, TwitterArchive from model import Account, Session, Post, TwitterArchive
from app import app, db, sentry, limiter from app import app, db, sentry, limiter
import tasks import tasks
@ -13,6 +13,7 @@ from twitter import TwitterError
from urllib.error import URLError from urllib.error import URLError
import version import version
import lib.brotli import lib.brotli
import lib.settings
@app.before_request @app.before_request
def load_viewer(): def load_viewer():
@ -111,15 +112,7 @@ def upload_tweet_archive():
@app.route('/settings', methods=('POST',)) @app.route('/settings', methods=('POST',))
@require_auth @require_auth
def settings(): def settings():
for attr in ( for attr in lib.settings.attrs:
'policy_keep_favourites',
'policy_keep_latest',
'policy_delete_every_scale',
'policy_delete_every_significand',
'policy_keep_younger_scale',
'policy_keep_younger_significand',
'policy_keep_media',
):
try: try:
if attr in request.form: if attr in request.form:
setattr(g.viewer.account, attr, request.form[attr]) setattr(g.viewer.account, attr, request.form[attr])
@ -174,3 +167,16 @@ def logout():
@app.route('/api/about') @app.route('/api/about')
def api_about(): def api_about():
return jsonify(service='Forget', version=version.version) return jsonify(service='Forget', version=version.version)
@app.route('/api/settings', methods=('PUT',))
@require_auth_api
def api_settings_put():
viewer = get_viewer()
data = request.json
updated = dict()
for key in lib.settings.attrs:
if key in data:
setattr(viewer, key, data[key])
updated[key] = data[key]
db.session.commit()
return jsonify(status='success', updated=updated)

View File

@ -18,6 +18,7 @@
onload="Raven.config('{{sentry_dsn}}').install()"></script> onload="Raven.config('{{sentry_dsn}}').install()"></script>
{% endif -%} {% endif -%}
<script defer src="{{ st('version.js') }}"></script> <script defer src="{{ st('version.js') }}"></script>
{% block scripts %}{% endblock %}
</head> </head>
<body> <body>
<header> <header>

View File

@ -1,5 +1,8 @@
{% from 'lib/interval.html' import interval_input -%} {% from 'lib/interval.html' import interval_input -%}
{% extends 'lib/layout.html' %} {% extends 'lib/layout.html' %}
{% block scripts %}
<script async src="{{st('settings_form.js')}}"></script>
{% endblock %}
{% block body -%} {% block body -%}
<section class=viewer> <section class=viewer>
@ -30,8 +33,8 @@
{% endif %} {% endif %}
</section> </section>
<section> <section id='settings-section'>
<h2>Your expiration rules</h2> <h2 id="settings-title">Your expiration rules</h2>
{% if request.args.get('settings_saved') != None %} {% if request.args.get('settings_saved') != None %}
<div class='banner success'>Rules saved successfully</div> <div class='banner success'>Rules saved successfully</div>
@ -41,7 +44,7 @@
<div class='banner error'>Something went wrong trying to save your settings. Please try again later.</div> <div class='banner error'>Something went wrong trying to save your settings. Please try again later.</div>
{% endif %} {% endif %}
<form action='{{url_for("settings")}}' method='post' enctype='multipart/form-data'> <form name='settings' action='{{url_for("settings")}}' method='post' enctype='multipart/form-data'>
<p>Posts that are over <p>Posts that are over
{{interval_input(g.viewer.account, 'policy_keep_younger', scales)}} {{interval_input(g.viewer.account, 'policy_keep_younger', scales)}}
old and are not one of your old and are not one of your