add user settings; support for various reply mechanisms

This commit is contained in:
Kyle Mahan 2015-02-08 11:56:27 -08:00
parent dc90b729d4
commit 8d0d6da450
11 changed files with 386 additions and 71 deletions

View file

@ -1,4 +1,5 @@
import bleach
import json
from .extensions import db
@ -12,6 +13,24 @@ bleach.ALLOWED_ATTRIBUTES.update({
})
class JsonType(db.TypeDecorator):
"""Represents an immutable structure as a json-encoded string.
http://docs.sqlalchemy.org/en/rel_0_9/core/types.html#marshal-json-strings
"""
impl = db.Text
def process_bind_param(self, value, dialect):
if value is not None:
value = json.dumps(value)
return value
def process_result_value(self, value, dialect):
if value is not None:
value = json.loads(value)
return value
users_to_feeds = db.Table(
'users_to_feeds', db.Model.metadata,
db.Column('user_id', db.Integer, db.ForeignKey('user.id'), index=True),
@ -24,6 +43,7 @@ class User(db.Model):
domain = db.Column(db.String(256))
micropub_endpoint = db.Column(db.String(512))
access_token = db.Column(db.String(512))
settings = db.Column(JsonType)
# Flask-Login integration
def is_authenticated(self):

View file

@ -0,0 +1,70 @@
/*jslint browser: true, plusplus: true, vars: true, indent: 2 */
window.loadIndieConfig = (function () {
'use strict';
// Indie-Config Loading script
// by Pelle Wessman, voxpelli.com
// MIT-licensed
// http://indiewebcamp.com/indie-config
var config, configFrame, configTimeout,
callbacks = [],
handleConfig, parseConfig;
// When the configuration has been loaded deregister all loading mechanics and call all callbacks
handleConfig = function () {
config = config || {};
configFrame.parentNode.removeChild(configFrame);
configFrame = undefined;
window.removeEventListener('message', parseConfig);
clearTimeout(configTimeout);
while (callbacks[0]) {
callbacks.shift()(config);
}
};
// When we receive a message, check if the source is right and try to parse it
parseConfig = function (message) {
var correctSource = (configFrame && message.source === configFrame.contentWindow);
if (correctSource && config === undefined) {
try {
config = JSON.parse(message.data);
} catch (ignore) {}
handleConfig();
}
};
return function (callback) {
// If the config is already loaded, call callback right away
if (config) {
callback(config);
return;
}
// Otherwise add the callback to the queue
callbacks.push(callback);
// Are we already trying to load the Indie-Config, then wait
if (configFrame) {
return;
}
// Create the iframe that will load the Indie-Config
configFrame = document.createElement('iframe');
configFrame.src = 'web+action:load';
document.getElementsByTagName('body')[0].appendChild(configFrame);
configFrame.style.display = 'none';
// Listen for messages so we will catch the Indie-Config message
window.addEventListener('message', parseConfig);
// And if no such Indie-Config message has been loaded in a while, abort the loading
configTimeout = setTimeout(handleConfig, 3000);
};
}());

View file

@ -415,11 +415,32 @@ article {
label {
font-weight: bold;
display: block; }
display: inline-block;
margin-bottom: 5px;
max-width: 100%; }
textarea, input[type="text"], input[type="url"] {
width: 100%;
margin: 0.25em 0; }
textarea.input-25, input[type="text"].input-25, input[type="url"].input-25 {
width: 23%; }
textarea.input-50, input[type="text"].input-50, input[type="url"].input-50 {
width: 48%; }
textarea.input-75, input[type="text"].input-75, input[type="url"].input-75 {
width: 73%; }
.reply-area {
text-align: center; }
.reply-area a {
display: inline-block;
padding: 0.2em;
border: 1px solid #687D77;
border-radius: 4px;
background-color: #ECEBF0;
text-decoration: none;
color: #484A47;
min-width: 50px;
text-align: center; }
button {
padding: 0.25em; }

View file

@ -1,6 +1,6 @@
{
"version": 3,
"mappings": "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",
"mappings": "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",
"sources": ["normalize.scss","style.scss"],
"names": [],
"file": "style.css"

View file

@ -92,12 +92,39 @@ article {
label {
font-weight: bold;
display: block;
display: inline-block;
margin-bottom: 5px;
max-width: 100%;
}
textarea, input[type="text"], input[type="url"] {
width: 100%;
margin: 0.25em 0 ;
margin: 0.25em 0;
&.input-25 {
width: 23%;
}
&.input-50 {
width: 48%;
}
&.input-75 {
width: 73%;
}
}
.reply-area {
text-align: center;
a {
display: inline-block;
padding: 0.2em;
border: 1px solid $sirocco;
border-radius: 4px;
background-color: $athens-gray;
text-decoration: none;
color: $lunar-green;
min-width: 50px;
text-align: center;
}
}
button {

View file

@ -0,0 +1,63 @@
/*jslint browser: true, plusplus: true, vars: true, indent: 2 */
(function () {
'use strict';
var loadingClassRegexp = /(^|\s)indieconfig-loading(\s|$)/;
var doTheAction = function (indieConfig) {
var href, action, anchors;
// Don't block the tag anymore as the queued action is now handled
this.className = this.className.replace(loadingClassRegexp, ' ');
// Pick the correct endpoint for the correct action
action = this.getAttribute('do');
href = indieConfig[action];
// If no endpoint is found, try the URL of the first a-tag within it
if (!href) {
anchors = this.getElementsByTagName('a');
if (anchors[0]) {
href = anchors[0].href;
}
}
// We have found an endpoint!
if (href) {
//Resolve a relative target
var target = document.createElement('a');
target.href = this.getAttribute('with');
target = target.href;
// Insert the target into the endpoint
href = href.replace('{url}', encodeURIComponent(target || window.location.href));
// And redirect to it
window.open( href, '_blank');
}
};
// Event handler for a click on an indie-action tag
var handleTheAction = function (e) {
// Prevent the default of eg. any a-tag fallback within the indie-action tag
e.preventDefault();
// Make sure this tag hasn't already been queued for the indieconfig-load
if (!loadingClassRegexp.test(this.className)) {
this.className += ' indieconfig-loading';
// Set "doTheAction" to be called when the indie-config has been loaded
window.loadIndieConfig(doTheAction.bind(this));
}
};
// Once the page is loased add click event listeners to all indie-action tags
window.addEventListener('DOMContentLoaded', function () {
var actions = document.querySelectorAll('indie-action'),
i,
length = actions.length;
for (i = 0; i < length; i++) {
actions[i].addEventListener('click', handleTheAction);
}
});
}());

View file

@ -0,0 +1,34 @@
{% set settings = current_user.settings or {} %}
{% set replyMethod = settings.get('reply-method') %}
{% if replyMethod == 'micropub' and current_user.micropub_endpoint %}
<form class="like-form" action="{{ current_user.micropub_endpoint }}" method="POST" style="display:inline">
<input type="hidden" name="access_token" value="{{current_user.access_token}}"/>
<input type="hidden" name="h" value="entry"/>
<input type="hidden" name="like-of" value="{{ entry.permalink }}"/>
<button type="submit" class="like-button">Like</button>
<span class="submit-response"></span>
</form>
<button class="show-reply-form">Reply</button>
<form class="reply-form" action="{{ current_user.micropub_endpoint }}" method="POST">
<input type="hidden" name="access_token" value="{{current_user.access_token}}"/>
<input type="hidden" name="h" value="entry"/>
<input type="hidden" name="in-reply-to" value="{{ entry.permalink }}"/>
<textarea name="content"></textarea>
<button type="submit" class="reply-button">Reply</button>
<span class="submit-response"></span>
</form>
{% elif replyMethod == 'indie-config' %}
{% for action in settings.get('indie-config-actions', []) %}
<indie-action with="{{ entry.permalink }}" do="{{ action }}">
<a class="reply-link" href="#">{{ action | capitalize }}</a>
</indie-action>
{% endfor %}
{% elif replyMethod == 'action-urls' %}
{% for action, url in settings.get('action-urls', []) %}
<a class="reply-link" href="{{ url | replace('{url}', entry.permalink) }}" target="_blank">{{ action | capitalize }}</a>
{% endfor %}
{% endif %}

View file

@ -1,6 +1,13 @@
{% extends "base.jinja2" %}
{% block head %}
<script src="{{url_for('static', filename='feed.js')}}"></script>
<script src="{{url_for('static', filename='feed.js')}}"></script>
{% if current_user and current_user.settings
and current_user.settings.get('reply-method') == 'indie-config' %}
<script src="{{url_for('static', filename='indieconfig.js')}}"></script>
<script src="{{url_for('static', filename='webaction.js')}}"></script>
{% endif %}
{% endblock head %}
{% block header %}
@ -34,28 +41,9 @@
{% endif %}
<footer>
<a href="{{ entry.permalink }}">{{ entry.published }}</a>
{% if current_user.micropub_endpoint %}
<form class="like-form" action="{{ current_user.micropub_endpoint }}" method="POST" style="display:inline">
<input type="hidden" name="access_token" value="{{current_user.access_token}}"/>
<input type="hidden" name="h" value="entry"/>
<input type="hidden" name="like-of" value="{{ entry.permalink }}"/>
<button type="submit" class="like-button">Like</button>
<span class="submit-response"></span>
</form>
<button class="show-reply-form">Reply</button>
<form class="reply-form" action="{{ current_user.micropub_endpoint }}" method="POST">
<input type="hidden" name="access_token" value="{{current_user.access_token}}"/>
<input type="hidden" name="h" value="entry"/>
<input type="hidden" name="in-reply-to" value="{{ entry.permalink }}"/>
<textarea name="content"></textarea>
<button type="submit" class="reply-button">Reply</button>
<span class="submit-response"></span>
</form>
{% endif %}
<div class="reply-area">
{% include '_reply.jinja2' with context %}
</div>
</footer>
</article>
{% endfor %}

View file

@ -1,7 +0,0 @@
{% extends "base.jinja2" %}
{% block body %}
<form action="{{ url_for('.login') }}" method="GET">
<input type="text" name="me" placeholder="mydomain.com" />
<button type="submit">Login</button>
</form>
{% endblock %}

View file

@ -2,45 +2,123 @@
{% block body %}
<main>
<!-- reply via micropub -->
<form name="settings" action="{{ url_for('.settings') }}" method="POST">
<button type="submit">Save Changes</button>
{% if current_user.micropub_endpoint or current_user.access_token %}
<form>
<label>Micropub Endpoint</label>
<input type="text" value="{{ current_user.micropub_endpoint }}" readonly />
<div id="reply-mechanism-settings">
{% set reply_method = settings.get('reply-method') %}
<label>Access Token</label>
<input type="text" value="{{ current_user.access_token }}" readonly />
</form>
<h2>Reply Mechanism</h2>
<p>
<input type="radio" id="reply-method-micropub"
name="reply-method" value="micropub"
{% if reply_method == 'micropub' %}checked{% endif %}/>
<label for="reply-method-micropub">Micropub.</label>
Each post will have a Like and Reply button that will post content to your site directly via micropub. See <a href="https://indiewebcamp.com/Micropub">Micropub</a> for details.
</p>
<form action="{{ url_for('.authorize') }}" method="POST">
<button type="submit">
Reauthorize Micropub
</button>
<input type="hidden" name="next" value="{{ request.path }}" />
</form>
<p>
<input type="radio" id="reply-method-indie-config"
name="reply-method" value="indie-config"
{% if reply_method == 'indie-config' %}checked{% endif %}/>
<label for="reply-method-indie-config">Indie-config.</label>
Clicking Like or Reply will invoke your <code>web+action</code> handler if registered. See <a href="https://indiewebcamp.com/indie-config">indie-config</a> for details.
</p>
<form action="{{ url_for('.deauthorize') }}" method="POST">
<button type="submit">Revoke Credentials</button>
<input type="hidden" name="next" value="{{ request.path }}" />
</form>
<p>
<input type="radio" id="reply-method-action-urls"
name="reply-method" value="action-urls"
{% if reply_method == 'action-urls' %}checked{% endif %}/>
<label for="reply-method-action-urls">Configurable action urls.</label>
Configure Woodwind with your preferred web action handlers. The placeholder <code>{url}</code> will be replaced with the permalink URL of each entry.
</p>
</div>
{% else %}
<form action="{{ url_for('.authorize') }}" method="POST">
<button type="submit">
Authorize Micropub
</button>
<input type="hidden" name="next" value="{{ request.path }}" />
</form>
{% endif %}
<!-- reply via micropub -->
<!-- reply via indie-config -->
<div id="micropub-settings">
<h2>Micropub</h2>
<p>
Configure micropub credentials.
</p>
{% if current_user.micropub_endpoint or current_user.access_token %}
<input type="text" value="{{ current_user.micropub_endpoint }}" readonly />
<input type="text" value="{{ current_user.access_token }}" readonly />
<p>
<a href="{{url_for('.authorize', next=request.path)}}">Reauthorize Micropub</a>
</p>
<p>
<a href="{{url_for('.deauthorize', next=request.path)}}">Revoke Credentials</a>
</p>
{% else %}
<p>
<a href="{{url_for('.authorize', next=request.path)}}">Authorize Micropub</a>
</p>
{% endif %}
</div>
<!-- reply via indie-config -->
<div id="indie-config-settings">
<h2>Indie-Config</h2>
<p>
Select indie-config actions.
</p>
{% set selectedActions = settings.get('indie-config-actions', []) %}
<!-- configure endpoints manually -->
{% for action in ['like', 'favorite', 'reply', 'repost', 'bookmark'] %}
<label>
<input type="checkbox" name="indie-config-action" value="{{ action }}"
{% if action in selectedActions %}checked{% endif %} />
{{ action | capitalize }}
</label><br/>
{% endfor %}
</p>
</div>
<!-- configure endpoints manually -->
<div id="action-urls-settings">
<h2>Action URLs</h2>
<div id="action-urls-inputs">
{% set actionUrls = settings.get('action-urls', {}) %}
{% for action, url in actionUrls %}
<input type="text" name="action" class="input-25" value="{{action}}" />
<input type="text" name="action-url" class="input-75" value="{{url}}" />
{% endfor %}
<input type="text" id="action-1" name="action" class="input-25" placeholder="Like" />
<input type="text" id="action-url-1" name="action-url" class="input-75" placeholder="http://example.com/new/like?url={url}" />
</div>
<button id="add-action">Add</button>
</form>
</main>
<script>
$('#add-action').click(function(evt) {
evt.preventDefault();
$('#action-1').clone().appendTo('#action-urls-inputs');
$('#action-url-1').clone().appendTo('#action-urls-inputs');
});
function showRelevantSection() {
var replyMethod = $('input[name=reply-method]:checked').val();
$('#micropub-settings').hide();
$('#indie-config-settings').hide();
$('#action-urls-settings').hide();
if (replyMethod == 'micropub') {
$('#micropub-settings').show();
} else if (replyMethod == 'indie-config') {
$('#indie-config-settings').show();
} else if (replyMethod == 'action-urls') {
$('#action-urls-settings').show();
}
}
$('input[name=reply-method]').change(showRelevantSection);
$(showRelevantSection);
</script>
{% endblock body %}

View file

@ -43,10 +43,31 @@ def feeds():
return flask.render_template('feeds.jinja2', feeds=sorted_feeds)
@views.route('/settings')
@views.route('/settings', methods=['GET', 'POST'])
@flask_login.login_required
def settings():
return flask.render_template('settings.jinja2')
settings = flask_login.current_user.settings or {}
if flask.request.method == 'GET':
return flask.render_template('settings.jinja2', settings=settings)
settings = dict(settings)
reply_method = flask.request.form.get('reply-method')
settings['reply-method'] = reply_method
if reply_method == 'micropub':
pass
elif reply_method == 'indie-config':
settings['indie-config-actions'] = flask.request.form.getlist(
'indie-config-action')
elif reply_method == 'action-urls':
zipped = zip(
flask.request.form.getlist('action'),
flask.request.form.getlist('action-url'))
settings['action-urls'] = [[k, v] for k, v in zipped if k and v]
flask_login.current_user.settings = settings
db.session.commit()
return flask.render_template('settings.jinja2', settings=settings)
@views.route('/update_feed')
@ -103,7 +124,7 @@ def login():
def login_callback(resp):
if not resp.me:
flask.flash(cgi.escape('Login error: ' + resp.error))
return flask.redirect(flask.url_for('.login'))
return flask.redirect(flask.url_for('.index'))
if resp.error:
flask.flash(cgi.escape('Warning: ' + resp.error))
@ -121,12 +142,12 @@ def login_callback(resp):
return flask.redirect(resp.next_url or flask.url_for('.index'))
@views.route('/authorize', methods=['POST'])
@views.route('/authorize')
@flask_login.login_required
def authorize():
return micropub.authorize(
me=flask_login.current_user.url,
next_url=flask.request.form.get('next'),
next_url=flask.request.args.get('next'),
scope='post')
@ -135,13 +156,13 @@ def authorize():
def micropub_callback(resp):
if not resp.me or resp.error:
flask.flash(cgi.escape('Authorize error: ' + resp.error))
return flask.redirect(flask.url_for('.login'))
return flask.redirect(flask.url_for('.index'))
domain = urllib.parse.urlparse(resp.me).netloc
user = load_user(domain)
if not user:
flask.flash(cgi.escape('Unknown user for domain: ' + domain))
return flask.redirect(flask.url_for('.login'))
return flask.redirect(flask.url_for('.index'))
user.micropub_endpoint = resp.micropub_endpoint
user.access_token = resp.access_token
@ -149,13 +170,13 @@ def micropub_callback(resp):
return flask.redirect(resp.next_url or flask.url_for('.index'))
@views.route('/deauthorize', methods=['POST'])
@views.route('/deauthorize')
@flask_login.login_required
def deauthorize():
flask_login.current_user.micropub_endpoint = None
flask_login.current_user.access_token = None
db.session.commit()
return flask.redirect(flask.request.form.get('next')
return flask.redirect(flask.request.args.get('next')
or flask.url_for('.index'))