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;
}
.banner.success {
.success {
border-color: #4a2;
background: #dec;
border-left-color: #4a2;
}
.banner.error {
.error {
border-color: #a24;
background: #ecd;
}
.banner.warning {
.warning {
border-color: #dc4;
background: #eec;
}
@ -126,3 +126,57 @@ footer a {
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():
import shutil
assets = ('icon.png', 'logotype.png', 'version.js')
assets = ('icon.png', 'logotype.png', 'version.js', 'settings_form.js')
for asset in assets:
yield dict(
name=asset,

View File

@ -1,6 +1,7 @@
from .auth import require_auth
from . import auth
from .interval import decompose_interval
from .interval import SCALES as interval_scales
from .cachebust import cachebust
from .session import set_session_cookie, get_viewer_session, get_viewer
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):
from functools import update_wrapper
def require_auth(fun):
@wraps(fun)
def wrapper(*args, **kwargs):
if not g.viewer:
if redir:
return redirect('/')
else:
return 403
else:
return fun(*args, **kwargs)
return redirect('/')
return fun(*args, **kwargs)
return wrapper
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

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
import lib.twitter
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 get_viewer_session
from lib import get_viewer_session, get_viewer
from model import Account, Session, Post, TwitterArchive
from app import app, db, sentry, limiter
import tasks
@ -13,6 +13,7 @@ from twitter import TwitterError
from urllib.error import URLError
import version
import lib.brotli
import lib.settings
@app.before_request
def load_viewer():
@ -111,15 +112,7 @@ def upload_tweet_archive():
@app.route('/settings', methods=('POST',))
@require_auth
def settings():
for attr in (
'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',
):
for attr in lib.settings.attrs:
try:
if attr in request.form:
setattr(g.viewer.account, attr, request.form[attr])
@ -174,3 +167,16 @@ def logout():
@app.route('/api/about')
def api_about():
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>
{% endif -%}
<script defer src="{{ st('version.js') }}"></script>
{% block scripts %}{% endblock %}
</head>
<body>
<header>

View File

@ -1,5 +1,8 @@
{% from 'lib/interval.html' import interval_input -%}
{% extends 'lib/layout.html' %}
{% block scripts %}
<script async src="{{st('settings_form.js')}}"></script>
{% endblock %}
{% block body -%}
<section class=viewer>
@ -30,8 +33,8 @@
{% endif %}
</section>
<section>
<h2>Your expiration rules</h2>
<section id='settings-section'>
<h2 id="settings-title">Your expiration rules</h2>
{% if request.args.get('settings_saved') != None %}
<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>
{% 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
{{interval_input(g.viewer.account, 'policy_keep_younger', scales)}}
old and are not one of your