Compare commits

...
Sign in to create a new pull request.

12 commits
master ... opml

Author SHA1 Message Date
951ae14248 Add OPML export of subscriptions
It's nice to be able to export all your subscriptions if you want to
move to a different reader. This patch adds a link to the subscription
page where one can download the subscription list as OPML which is
the standard way of importing/exporting between feed readers.
2017-07-13 18:07:10 +02:00
Kyle Mahan
0d16b1ef88 use sentry for error monitoring 2017-06-26 06:02:47 +00:00
Kyle Mahan
6d89600ec1 Merge branch 'master' of github.com:kylewm/woodwind 2017-06-20 14:54:45 +00:00
Kyle Mahan
32b1505498 upgrade bs4 and html5lib 2017-06-20 07:54:29 -07:00
Kyle Mahan
bfc1aa2e75 Merge branch 'master' of github.com:kylewm/woodwind 2017-05-28 19:18:29 +00:00
Kyle Mahan
9e60633973 catch errors parsing dates 2017-05-28 19:18:21 +00:00
Kyle Mahan
56927d4224 limit author properties to the SQL column widths 2017-05-28 12:08:21 -07:00
Kyle Mahan
2e53fd9527 add semicolon to sql file 2017-03-06 04:41:38 +00:00
Kyle Mahan
28c1ab5058 simplify vacuum script a little 2017-03-05 15:05:46 -08:00
Kyle Mahan
dcf195d2c1 add sql script to remove old entries from feeds 2017-03-05 14:52:50 -08:00
Marty McGuire
9de0892cfc Add missing paren in views.py (#63) 2016-11-29 13:27:21 -08:00
Jonny Barnes
e05e33f14b Swap indiwebcam.comp references for indieweb.org (#62) 2016-10-24 10:55:36 -07:00
14 changed files with 73 additions and 19 deletions

View file

@ -5,7 +5,7 @@ window.loadIndieConfig = (function () {
// Indie-Config Loading script // Indie-Config Loading script
// by Pelle Wessman, voxpelli.com // by Pelle Wessman, voxpelli.com
// MIT-licensed // MIT-licensed
// http://indiewebcamp.com/indie-config // https://indieweb.org/indie-config
var config, configFrame, configTimeout, var config, configFrame, configTimeout,
callbacks = [], callbacks = [],

View file

@ -1,5 +1,5 @@
asyncio-redis==0.14.2 asyncio-redis==0.14.2
beautifulsoup4==4.4.1 beautifulsoup4==4.6.0
bleach==1.4.3 bleach==1.4.3
blinker==1.4 blinker==1.4
certifi==2015.04.28 # rq.filter: <=2015.04.28 certifi==2015.04.28 # rq.filter: <=2015.04.28
@ -12,7 +12,7 @@ Flask-DebugToolbar==0.10.0
Flask-Login==0.3.2 Flask-Login==0.3.2
Flask-Micropub==0.2.6 Flask-Micropub==0.2.6
Flask-SQLAlchemy==2.1 Flask-SQLAlchemy==2.1
html5lib==0.9999999 html5lib==0.999999999
idna==2.1 idna==2.1
itsdangerous==0.24 itsdangerous==0.24
Jinja2==2.8 Jinja2==2.8

View file

@ -7,5 +7,5 @@ setup(name='Woodwind',
description='Stream-style indieweb reader', description='Stream-style indieweb reader',
author='Kyle Mahan', author='Kyle Mahan',
author_email='kyle@kylewm.com', author_email='kyle@kylewm.com',
url='https://indiewebcamp.com/Woodwind', url='https://indieweb.org/Woodwind',
packages=['woodwind']) packages=['woodwind'])

9
vacuum.sql Normal file
View file

@ -0,0 +1,9 @@
DELETE FROM entry
USING (
SELECT
id,
ROW_NUMBER() OVER (PARTITION BY feed_id ORDER BY retrieved DESC) AS row
FROM entry
) AS numbered
WHERE entry.id = numbered.id
AND (row > 2000 OR retrieved < CURRENT_DATE - INTERVAL '365 days');

View file

@ -1,3 +1,4 @@
from raven.contrib.flask import Sentry
from woodwind import extensions from woodwind import extensions
from woodwind.api import api from woodwind.api import api
from woodwind.push import push from woodwind.push import push
@ -20,6 +21,8 @@ Message:
%(message)s %(message)s
''' '''
sentry = Sentry()
def create_app(config_path='../woodwind.cfg'): def create_app(config_path='../woodwind.cfg'):
app = flask.Flask('woodwind') app = flask.Flask('woodwind')
@ -38,6 +41,8 @@ def configure_logging(app):
app.logger.setLevel(logging.DEBUG) app.logger.setLevel(logging.DEBUG)
sentry.init_app(app, dsn=app.config.get('SENTRY_DSN'), logging=True, level=logging.WARNING)
handler = logging.StreamHandler(sys.stdout) handler = logging.StreamHandler(sys.stdout)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter) handler.setFormatter(formatter)

View file

@ -5,7 +5,7 @@ window.loadIndieConfig = (function () {
// Indie-Config Loading script // Indie-Config Loading script
// by Pelle Wessman, voxpelli.com // by Pelle Wessman, voxpelli.com
// MIT-licensed // MIT-licensed
// http://indiewebcamp.com/indie-config // https://indieweb.org/indie-config
var config, configFrame, configTimeout, var config, configFrame, configTimeout,
callbacks = [], callbacks = [],

View file

@ -397,14 +397,21 @@ def process_xml_feed_for_new_entries(feed, content, backfill, now):
continue continue
if 'updated_parsed' in p_entry and p_entry.updated_parsed: if 'updated_parsed' in p_entry and p_entry.updated_parsed:
updated = datetime.datetime.fromtimestamp( try:
time.mktime(p_entry.updated_parsed)) updated = datetime.datetime.fromtimestamp(
time.mktime(p_entry.updated_parsed))
except:
current_app.logger.debug('mktime failed with updated timestamp: %v', p_entry.updated_parsed)
else: else:
updated = None updated = None
if 'published_parsed' in p_entry and p_entry.published_parsed: if 'published_parsed' in p_entry and p_entry.published_parsed:
published = datetime.datetime.fromtimestamp( try:
time.mktime(p_entry.published_parsed)) published = datetime.datetime.fromtimestamp(
time.mktime(p_entry.published_parsed))
except:
current_app.logger.debug('mktime failed with published timestamp: %v', p_entry.published_parsed)
published = updated
else: else:
published = updated published = updated
@ -526,6 +533,13 @@ def hentry_to_entry(hentry, feed, backfill, now):
author_photo = author.get('photo') author_photo = author.get('photo')
author_url = author.get('url') author_url = author.get('url')
if author_name and len(author_name) > Entry.author_name.property.columns[0].type.length:
author_name = None
if author_photo and len(author_photo) > Entry.author_photo.property.columns[0].type.length:
author_photo = None
if author_url and len(author_url) > Entry.author_url.property.columns[0].type.length:
author_url = None
entry = Entry( entry = Entry(
uid=uid, uid=uid,
retrieved=retrieved, retrieved=retrieved,

View file

@ -10,7 +10,7 @@
<link rel="stylesheet" href="{{ url_for('static', filename='style.css', version='2016-03-08') }}"/> <link rel="stylesheet" href="{{ url_for('static', filename='style.css', version='2016-03-08') }}"/>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css"/> <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css"/>
<link rel="manifest" href="/manifest.json"/> <link rel="manifest" href="/manifest.json"/>
<script src="/node_modules/jquery/dist/jquery.js"></script> <script src="/node_modules/jquery/dist/jquery.js"></script>
@ -62,7 +62,7 @@
<input type="hidden" name="next" placeholder="{{ request.path }}" /> <input type="hidden" name="next" placeholder="{{ request.path }}" />
<button style="text-align: right;" type="submit">Login</button> <button style="text-align: right;" type="submit">Login</button>
</form> </form>
Your Woodwind account is tied to your personal domain name. Check out IndieWebCamp's <a href="http://indiewebcamp.com/Getting_Started" target="_blank">Getting Started</a> page for details. Your Woodwind account is tied to your personal domain name. Check out the IndieWeb's <a href="https://indieweb.org/Getting_Started" target="_blank">Getting Started</a> page for details.
{% endif %} {% endif %}
{% endblock login %} {% endblock login %}
@ -77,7 +77,7 @@
{% block foot %}{% endblock %} {% block foot %}{% endblock %}
<div class="footer"> <div class="footer">
Woodwind is <a href="https://github.com/kylewm/woodwind">on GitHub</a>. Have any problems? File an issue or come chat in <code>#indiewebcamp</code> on Freenode IRC. Woodwind is <a href="https://github.com/kylewm/woodwind">on GitHub</a>. Have any problems? File an issue or come chat in <code>#indieweb</code> on Freenode IRC.
</div> </div>
<script> <script>

View file

@ -7,12 +7,12 @@
<p> <p>
<input type="radio" id="reply-method-micropub" name="reply-method" value="micropub" {% if reply_method == 'micropub' %}checked{% endif %}/> <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> <label for="reply-method-micropub">Micropub.</label>
Each post will have Like, Repost, and Reply buttons that will post content to your site directly via micropub. See <a href="https://indiewebcamp.com/Micropub">Micropub</a> for details. Each post will have Like, Repost, and Reply buttons that will post content to your site directly via micropub. See <a href="https://indieweb.org/Micropub">Micropub</a> for details.
</p> </p>
<p> <p>
<input type="radio" id="reply-method-indie-config" name="reply-method" value="indie-config" {% if reply_method == 'indie-config' %}checked{% endif %}/> <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> <label for="reply-method-indie-config">Indie-config.</label>
Clicking an indie-action link will invoke your <code>web+action</code> handler if registered. See <a href="https://indiewebcamp.com/indie-config">indie-config</a> for details. Clicking an indie-action link will invoke your <code>web+action</code> handler if registered. See <a href="https://indieweb.org/indie-config">indie-config</a> for details.
</p> </p>
<p> <p>
<input type="radio" id="reply-method-action-urls" name="reply-method" value="action-urls" {% if reply_method == 'action-urls' %}checked{% endif %}/> <input type="radio" id="reply-method-action-urls" name="reply-method" value="action-urls" {% if reply_method == 'action-urls' %}checked{% endif %}/>

View file

@ -5,7 +5,7 @@
<!-- reply via indie-config --> <!-- reply via indie-config -->
<h2>Indie-Config</h2> <h2>Indie-Config</h2>
<p> <p>
Clicking an indie-action link will invoke your <code>web+action</code> handler if registered. See <a href="https://indiewebcamp.com/indie-config">indie-config</a> for details. Clicking an indie-action link will invoke your <code>web+action</code> handler if registered. See <a href="https://indieweb.org/indie-config">indie-config</a> for details.
</p> </p>
{% set selectedActions = settings.get('indie-config-actions', []) %} {% set selectedActions = settings.get('indie-config-actions', []) %}

View file

@ -4,7 +4,7 @@
<!-- reply via micropub --> <!-- reply via micropub -->
<h2>Micropub</h2> <h2>Micropub</h2>
<p> <p>
Each post will have Like, Repost, and Reply buttons that will post content to your site directly via micropub. See <a href="https://indiewebcamp.com/Micropub">Micropub</a> for details. Each post will have Like, Repost, and Reply buttons that will post content to your site directly via micropub. See <a href="https://indieweb.org/Micropub">Micropub</a> for details.
</p> </p>
<p> <p>
Configure micropub credentials. Configure micropub credentials.

View file

@ -19,6 +19,7 @@
<form action="{{ url_for('.update_all') }}" method="POST"> <form action="{{ url_for('.update_all') }}" method="POST">
<button type="submit">Poll All</button> <button type="submit">Poll All</button>
<a href="{{ url_for('.subscriptions_opml')}}">Export as OPML</a>
</form> </form>
</article> </article>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<opml version="2.0">
<body>
<outline text="Subscriptions" title="Woodwind">
{% for s in subscriptions %}
<outline xmlUrl="{{ s.feed.feed }}" />
{% endfor %}
</outline>
</body>
</opml>

View file

@ -55,7 +55,8 @@ def index():
.join(Subscription.user)\ .join(Subscription.user)\
.filter(User.id == flask_login.current_user.id)\ .filter(User.id == flask_login.current_user.id)\
.filter(db.or_(Entry.deleted == None, .filter(db.or_(Entry.deleted == None,
Entry.deleted >= now)) Entry.deleted >= now))\
.order_by(Entry.published.desc())
if 'entry' in flask.request.args: if 'entry' in flask.request.args:
entry_url = flask.request.args.get('entry') entry_url = flask.request.args.get('entry')
@ -126,6 +127,21 @@ def subscriptions():
subscriptions=subscs) subscriptions=subscs)
@views.route('/subscriptions_opml.xml')
@flask_login.login_required
def subscriptions_opml():
subscs = Subscription\
.query\
.filter_by(user_id=flask_login.current_user.id)\
.options(sqlalchemy.orm.subqueryload(Subscription.feed))\
.order_by(db.func.lower(Subscription.name))\
.all()
template = flask.render_template('subscriptions_opml.xml',
subscriptions=subscs)
response = flask.make_response(template)
response.headers['Content-Type'] = 'application/xml'
return response
@views.route('/settings', methods=['GET', 'POST']) @views.route('/settings', methods=['GET', 'POST'])
@flask_login.login_required @flask_login.login_required
def settings(): def settings():
@ -381,8 +397,7 @@ def update_micropub_syndicate_to():
flask_login.current_user.set_setting('syndicate-to', syndicate_tos) flask_login.current_user.set_setting('syndicate-to', syndicate_tos)
db.session.commit() db.session.commit()
except ValueError as e: except ValueError as e:
flask.flash('Could not parse syndicate-to response: {}'.format(e) flask.flash('Could not parse syndicate-to response: {}'.format(e))
@views.route('/deauthorize') @views.route('/deauthorize')