diff --git a/flask_website/__init__.py b/flask_website/__init__.py index 8ba8a261..8f84970e 100644 --- a/flask_website/__init__.py +++ b/flask_website/__init__.py @@ -1,4 +1,5 @@ from flask import Flask, session, g, render_template +from flaskext.openid import OpenID import websiteconfig as config @@ -6,6 +7,9 @@ app = Flask(__name__) app.debug = config.DEBUG app.secret_key = config.SECRET_KEY +from flask_website.openid_auth import DatabaseOpenIDStore +oid = OpenID(store_factory=DatabaseOpenIDStore) + @app.errorhandler(404) def not_found(error): return render_template('404.html'), 404 diff --git a/flask_website/database.py b/flask_website/database.py index 703417f8..da362879 100644 --- a/flask_website/database.py +++ b/flask_website/database.py @@ -32,6 +32,10 @@ class User(Model): self.name = name self.openid = openid + @property + def is_admin(self): + return self.openid in config.ADMINS + def __eq__(self, other): return type(self) is type(other) and self.id == other.id diff --git a/flask_website/openid_auth.py b/flask_website/openid_auth.py index c16693e9..534ef950 100644 --- a/flask_website/openid_auth.py +++ b/flask_website/openid_auth.py @@ -1,23 +1,14 @@ from time import time -from hashlib import sha1 from openid.association import Association from openid.store.interface import OpenIDStore -from openid.consumer.consumer import Consumer, SUCCESS, CANCEL -from openid.consumer import discover from openid.store import nonce -# python-openid is a really stupid library in that regard, we have -# to disable logging by monkey patching -from openid import oidutil -oidutil.log = lambda *a, **kw: None - -from flask import request, redirect, abort, url_for, flash, session from flask_website.database import User, db_session, OpenIDAssociation, \ OpenIDUserNonce -class WebsiteOpenIDStore(OpenIDStore): +class DatabaseOpenIDStore(OpenIDStore): """Implements the open store for the website using the database.""" def storeAssociation(self, server_url, association): @@ -30,6 +21,7 @@ class WebsiteOpenIDStore(OpenIDStore): assoc_type=association.assoc_type ) db_session.add(assoc) + db_session.commit() def getAssociation(self, server_url, handle=None): q = OpenIDAssociation.query.filter_by(server_url=server_url) @@ -46,10 +38,13 @@ class WebsiteOpenIDStore(OpenIDStore): return result_assoc def removeAssociation(self, server_url, handle): - return OpenIDAssociation.query.filter( - (OpenIDAssociation.server_url == server_url) & - (OpenIDAssociation.handle == handle) - ).delete() + try: + return OpenIDAssociation.query.filter( + (OpenIDAssociation.server_url == server_url) & + (OpenIDAssociation.handle == handle) + ).delete() + finally: + db_session.commit() def useNonce(self, server_url, timestamp, salt): if abs(timestamp - time()) > nonce.SKEW: @@ -64,64 +59,21 @@ class WebsiteOpenIDStore(OpenIDStore): rv = OpenIDUserNonce(server_url=server_url, timestamp=timestamp, salt=salt) db_session.add(rv) + db_session.commit() return True def cleanupNonces(self): - return OpenIDUserNonce.query.filter( - OpenIDUserNonce.timestamp <= int(time() - nonce.SKEW) - ).delete() + try: + return OpenIDUserNonce.query.filter( + OpenIDUserNonce.timestamp <= int(time() - nonce.SKEW) + ).delete() + finally: + db_session.commit() def cleanupAssociations(self): - return OpenIDAssociation.query.filter( - OpenIDAssociation.lifetime < int(time()) - ).delete() - - -def redirect_back(): - return redirect(request.values.get('next') or url_for('general.index')) - - -def check_return_from_provider(): - if request.args.get('openid_complete') != u'yes': - return - try: - consumer = Consumer(session, WebsiteOpenIDStore()) - openid_response = consumer.complete(request.args.to_dict(), - url_for('general.login', - _external=True)) - if openid_response.status == SUCCESS: - return create_or_login(openid_response.identity_url) - elif openid_response.status == CANCEL: - flash(u'Error: The request was cancelled') - return redirect(url_for('general.login')) - flash(u'Error: OpenID authentication error') - return redirect(url_for('general.login')) - finally: - db_session.commit() - - -def create_or_login(identity_url): - session['openid'] = identity_url - user = User.query.filter_by(openid=identity_url).first() - if user is None: - next_url = request.values.get('next') - return redirect(url_for('general.first_login', next=next_url)) - flash(u'Successfully logged in') - return redirect_back() - - -def login(identity_url): - try: try: - consumer = Consumer(session, WebsiteOpenIDStore()) - auth_request = consumer.begin(identity_url) - except discover.DiscoveryFailure: - flash(u'Error: The OpenID was invalid') - return redirect(url_for('general.login')) - trust_root = request.host_url - next_url = request.values.get('next') or url_for('general.index') - redirect_to = url_for('general.login', openid_complete='yes', - next=next_url, _external=True) - return redirect(auth_request.redirectURL(trust_root, redirect_to)) - finally: - db_session.commit() + return OpenIDAssociation.query.filter( + OpenIDAssociation.lifetime < int(time()) + ).delete() + finally: + db_session.commit() diff --git a/flask_website/static/style.css b/flask_website/static/style.css index 22929486..7c346404 100644 --- a/flask_website/static/style.css +++ b/flask_website/static/style.css @@ -25,6 +25,14 @@ blockquote { margin: 0; font-style: italic; color: #444; } margin: 5px 0 0 0; font-size: 0.9em; } .message { background: #DEEBF3; color: #004B6B; padding: 5px 30px; margin: 10px -30px; } +.actions { margin-top: 0; } +table { border: 1px solid black; border-collapse: collapse; + margin: 15px 0; } +td, th { border: 1px solid black; padding: 4px 10px; + text-align: left; } +th { background: #eee; font-weight: normal; } + +td input { border: none; padding: 0; } /* forms */ input, textarea, select { border: 1px solid black; padding: 2px; background: white; diff --git a/flask_website/templates/general/first_login.html b/flask_website/templates/general/first_login.html index ff42cbfa..69452e58 100644 --- a/flask_website/templates/general/first_login.html +++ b/flask_website/templates/general/first_login.html @@ -18,9 +18,10 @@ site. Choose wisely because you cannot change this value later:
+ diff --git a/flask_website/templates/general/login.html b/flask_website/templates/general/login.html index 7b34f771..3ada0f0f 100644 --- a/flask_website/templates/general/login.html +++ b/flask_website/templates/general/login.html @@ -16,7 +16,7 @@
OpenID URL: - + {% endblock %} diff --git a/flask_website/templates/snippets/delete_category.html b/flask_website/templates/snippets/delete_category.html new file mode 100644 index 00000000..0e2a56d1 --- /dev/null +++ b/flask_website/templates/snippets/delete_category.html @@ -0,0 +1,20 @@ +{% extends "snippets/layout.html" %} +{% block title %}Snippets Archive{% endblock %} +{% block body %} +
+ Do you want to delete this category? And if yes, what should + happen with the snippets in the category? +
+{% endblock %} diff --git a/flask_website/templates/snippets/index.html b/flask_website/templates/snippets/index.html index 0335462d..a0658678 100644 --- a/flask_website/templates/snippets/index.html +++ b/flask_website/templates/snippets/index.html @@ -17,13 +17,17 @@ {% endif %}Want to share something? Then add a - new snippet. + new snippet.
+ manage categories + {% endif %} {% if recent %}
A small Flask extension for adding + CSRF protection. + ''', + docs='http://sjl.bitbucket.org/flask-csrf/', + bitbucket='sjl/flask-csrf' + ), + Extension('flask-lesscss', 'Steve Losh', + description=''' +
+ A small Flask extension that makes it easy to use + LessCSS with your + Flask application. + ''', + docs='http://sjl.bitbucket.org/flask-lesscss/', + bitbucket='sjl/flask-lesscss' + ), + Extension('flask-urls', 'Steve Losh', + description=''' +
+ A collection of URL-related functions for Flask applications.
+ ''',
+ docs='http://sjl.bitbucket.org/flask-urls/',
+ bitbucket='sjl/flask-urls'
)
]
database.sort(key=lambda x: x.name.lower())
diff --git a/flask_website/views/general.py b/flask_website/views/general.py
index a32b1fea..7854d8d2 100644
--- a/flask_website/views/general.py
+++ b/flask_website/views/general.py
@@ -1,6 +1,6 @@
from flask import Module, render_template, session, redirect, url_for, \
request, flash, g, Response
-from flask_website import openid_auth
+from flask_website import oid
from flask_website.database import db_session, User
general = Module(__name__)
@@ -20,17 +20,30 @@ def logout():
@general.route('/login/', methods=['GET', 'POST'])
+@oid.loginhandler
def login():
if g.user is not None:
return redirect(url_for('general.index'))
- rv = openid_auth.check_return_from_provider()
- if rv is not None:
- return rv
if request.method == 'POST':
openid = request.values.get('openid')
if openid:
- return openid_auth.login(openid)
- return render_template('general/login.html')
+ return oid.try_login(openid, ask_for=['fullname', 'nickname'])
+ error = oid.fetch_error()
+ if error:
+ flash(u'Error: ' + error)
+ return render_template('general/login.html', next=oid.get_next_url())
+
+
+@oid.after_login
+def create_or_login(resp):
+ session['openid'] = resp.identity_url
+ user = User.query.filter_by(openid=resp.identity_url).first()
+ if user is not None:
+ flash(u'Successfully signed in')
+ g.user = user
+ return redirect(oid.get_next_url())
+ return redirect(url_for('first_login', next=oid.get_next_url(),
+ name=resp.fullname or resp.nickname))
@general.route('/first-login/', methods=['GET', 'POST'])
@@ -45,6 +58,7 @@ def first_login():
db_session.add(User(request.form['name'], session['openid']))
db_session.commit()
flash(u'Successfully created profile and logged in')
- return openid_auth.redirect_back()
+ return redirect(oid.get_next_url())
return render_template('general/first_login.html',
+ next=oid.get_next_url(),
openid=session['openid'])
diff --git a/flask_website/views/snippets.py b/flask_website/views/snippets.py
index 782ecf7c..4ee9da03 100644
--- a/flask_website/views/snippets.py
+++ b/flask_website/views/snippets.py
@@ -3,7 +3,7 @@ from urlparse import urljoin
from flask import Module, render_template, request, flash, abort, redirect, \
g, url_for
from werkzeug.contrib.atom import AtomFeed
-from flask_website.utils import requires_login, format_creole
+from flask_website.utils import requires_login, requires_admin, format_creole
from flask_website.database import Category, Snippet, Comment, db_session
snippets = Module(__name__, url_prefix='/snippets')
@@ -71,7 +71,7 @@ def edit(id):
snippet = Snippet.query.get(id)
if snippet is None:
abort(404)
- if snippet.author != g.user:
+ if g.user is None or (not g.user.is_admin and snippet.author != g.user):
abort(401)
preview = None
form = dict(title=snippet.title, body=snippet.body,
@@ -117,6 +117,64 @@ def category(slug):
snippets=snippets)
+@snippets.route('/manage-categories/', methods=['GET', 'POST'])
+@requires_admin
+def manage_categories():
+ categories = Category.query.order_by(Category.name).all()
+ if request.method == 'POST':
+ for category in categories:
+ category.name = request.form['name.%d' % category.id]
+ category.slug = request.form['slug.%d' % category.id]
+ db_session.commit()
+ flash(u'Categories updated')
+ return redirect(url_for('manage_categories'))
+ return render_template('snippets/manage_categories.html',
+ categories=categories)
+
+
+@snippets.route('/new-category/', methods=['POST'])
+@requires_admin
+def new_category():
+ category = Category(name=request.form['name'])
+ db_session.add(category)
+ db_session.commit()
+ flash(u'Category %s created.' % category.name)
+ return redirect(url_for('manage_categories'))
+
+
+@snippets.route('/delete-category/