Added snippet database to the website.

This commit is contained in:
Armin Ronacher 2010-05-03 11:20:52 +02:00
parent a81cf3a67c
commit 0ab7a9cb67
22 changed files with 825 additions and 118 deletions

View file

@ -1,17 +1,25 @@
from flask import Flask, render_template
from flask import Flask, session, g, render_template
import websiteconfig as config
app = Flask(__name__)
app.debug = config.DEBUG
app.secret_key = config.SECRET_KEY
@app.errorhandler(404)
def not_found(error):
return render_template('404.html'), 404
@app.before_request
def load_currrent_user():
g.user = User.query.filter_by(openid=session['openid']).first() \
if 'openid' in session else None
from flask_website.views.general import general
from flask_website.views.mailinglist import mailinglist
from flask_website.views.snippets import snippets
app.register_module(general)
app.register_module(mailinglist)
app.register_module(snippets)
from flask_website.database import User

View file

@ -1,83 +0,0 @@
from __future__ import with_statement
from time import time
from hashlib import sha1
from contextlib import closing
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
from sqlalchemy.orm import scoped_session
from sqlalchemy.exceptions import SQLError
from flask import request, redirect, abort, url_for, flash
from flask_website.database import User, db_session
class WebsiteOpenIDStore(OpenIDStore):
"""Implements the open store for the website using the database."""
def storeAssociation(self, server_url, association):
assoc = OpenIDAssociation(
server_url=server_url,
handle=association.handle,
secret=association.secret.encode('base64'),
issued=association.issued,
lifetime=association.lifetime,
assoc_type=association.assoc_type
)
db_session.add(assoc)
def getAssociation(self, server_url, handle=None):
q = OpenIDAssociation.query.filter_by(server_url=server_url)
if handle is not None:
q = q.filter_by(handle=handle)
result_assoc = None
for item in q.all():
assoc = Association(item.handle, item.secret.decode('base64'),
item.issued, item.lifetime, item.assoc_type)
if assoc.getExpiresIn() <= 0:
self.removeAssociation(server_url, assoc.handle)
else:
result_assoc = assoc
return result_assoc
def removeAssociation(self, server_url, handle):
return OpenIDAssociation.filter(
(OpenIDAssociation.server_url == server_url) &
(OpenIDAssociation.handle == handle)
).delete()
def useNonce(self, server_url, timestamp, salt):
if abs(timestamp - time()) > nonce.SKEW:
return False
rv = OpenIDUserNonces.query.filter(
(OpenIDUserNonces.server_url == server_url) &
(OpenIDUserNonces.timestamp == timestamp) &
(OpenIDUserNonces.salt == salt)
).first()
if rv is not None:
return False
rv = OpenIDUserNonces(server_url=server_url, timestamp=timestamp,
salt=salt)
session.add(rv)
return True
def cleanupNonces(self):
return OpenIDUserNonces.filter(
OpenIDUserNonces.timestamp <= int(time() - nonce.SKEW)
).delete()
def cleanupAssociations(self):
return OpenIDAssociation.filter(
OpenIDAssociation.lifetime < int(time())
).delete()
def getAuthKey(self):
return sha1(config.SECRET_KEY).hexdigest()[:self.AUTH_KEY_LEN]
def isDump(self):
return False

View file

@ -1,9 +1,12 @@
from datetime import datetime
from sqlalchemy import create_engine, MetaData, Table, Column, Integer, \
String, DateTime, ForeignKey
from sqlalchemy.orm import scoped_session, sessionmaker, backref
from sqlalchemy.orm import scoped_session, sessionmaker, backref, relation
from sqlalchemy.ext.declarative import declarative_base
from werkzeug import cached_property
from flask import url_for
from flask_website import config
engine = create_engine(config.DATABASE_URI)
@ -15,20 +18,25 @@ def init_db():
Model.metadata.create_all(bind=engine)
class Model(declarative_base()):
query = db_session.query_property()
Model = declarative_base(name='Model')
Model.query = db_session.query_property()
class User(Model):
__tablename__ = 'users'
id = Column('user_id', Integer, primary_key=True)
openid = Column('openid', String(200))
username = Column(String(40), unique=True)
password = Column(String(80))
name = Column(String(200), unique=True)
def __init__(self, username, password):
self.username = username
self.password = password
def __init__(self, name, openid):
self.name = name
self.openid = openid
def __eq__(self, other):
return type(self) is type(other) and self.id == other.id
def __ne__(self, other):
return not self.__eq__(other)
class Category(Model):
@ -37,40 +45,73 @@ class Category(Model):
name = Column(String(50))
slug = Column(String(50))
def __init__(self, name):
self.name = name
self.slug = '-'.join(name.split()).lower()
@cached_property
def count(self):
return self.snippets.count()
@property
def url(self):
return url_for('snippets.category', slug=self.slug)
class Snippet(Model):
__tablename__ = 'snippets'
id = Column('snippet_id', Integer, primary_key=True)
author = ForeignKey(User, backref=backref('snippets', lazy='dynamic'))
category = ForeignKey(Category, backref=backref('snippets', lazy='dynamic'))
author_id = Column(Integer, ForeignKey('users.user_id'))
author = relation(User, backref=backref('snippets', lazy='dynamic'))
category_id = Column(Integer, ForeignKey('categories.category_id'))
category = relation(Category, backref=backref('snippets', lazy='dynamic'))
title = Column(String(200))
body = Column(String)
pub_date = DateTime()
pub_date = Column(DateTime)
def __init__(self, author, title, body):
def __init__(self, author, title, body, category):
self.author = author
self.title = title
self.body = body
self.category = category
self.pub_date = datetime.utcnow()
@property
def url(self):
return url_for('snippets.show', id=self.id)
@property
def rendered_body(self):
from flask_website.utils import format_creole
return format_creole(self.body)
class Comment(Model):
__tablename__ = 'comments'
id = Column('comment_id', Integer, primary_key=True)
snippet = ForeignKey(Snippet, backref='lazy')
author = ForeignKey(User, backref=backref('comments', lazy='dynamic'))
snippet_id = Column(Integer, ForeignKey('snippets.snippet_id'))
snippet = relation(Snippet, backref=backref('comments', lazy=True))
author_id = Column(Integer, ForeignKey('users.user_id'))
author = relation(User, backref=backref('comments', lazy='dynamic'))
title = Column(String(200))
text = Column(String)
pub_date = DateTime()
pub_date = Column(DateTime)
def __init__(self, author, title, text):
def __init__(self, snippet, author, title, text):
self.snippet = snippet
self.author = author
self.title = title
self.text = text
self.pub_date = datetime.utcnow()
@property
def rendered_text(self):
from flask_website.utils import format_creole
return format_creole(self.text)
class OpenIDAssociation(Model):
__tablename__ = 'openid_associations'
id = Column('association_id', Integer, primary_key=True)
server_url = Column(String(1024))
handle = Column(String(255))
@ -80,7 +121,8 @@ class OpenIDAssociation(Model):
assoc_type = Column(String(64))
class OpenIDUserNonces(Model):
class OpenIDUserNonce(Model):
__tablename__ = 'openid_user_nonces'
id = Column('user_nonce_id', Integer, primary_key=True)
server_url = Column(String(1024))
timestamp = Column(Integer)

View file

@ -0,0 +1,86 @@
# flasky extensions. flasky pygments style based on tango style
from pygments.style import Style
from pygments.token import Keyword, Name, Comment, String, Error, \
Number, Operator, Generic, Whitespace, Punctuation, Other, Literal
class FlaskyStyle(Style):
background_color = "#f8f8f8"
default_style = ""
styles = {
# No corresponding class for the following:
#Text: "", # class: ''
Whitespace: "underline #f8f8f8", # class: 'w'
Error: "#a40000 border:#ef2929", # class: 'err'
Other: "#000000", # class 'x'
Comment: "italic #8f5902", # class: 'c'
Comment.Preproc: "noitalic", # class: 'cp'
Keyword: "bold #004461", # class: 'k'
Keyword.Constant: "bold #004461", # class: 'kc'
Keyword.Declaration: "bold #004461", # class: 'kd'
Keyword.Namespace: "bold #004461", # class: 'kn'
Keyword.Pseudo: "bold #004461", # class: 'kp'
Keyword.Reserved: "bold #004461", # class: 'kr'
Keyword.Type: "bold #004461", # class: 'kt'
Operator: "#582800", # class: 'o'
Operator.Word: "bold #004461", # class: 'ow' - like keywords
Punctuation: "bold #000000", # class: 'p'
# because special names such as Name.Class, Name.Function, etc.
# are not recognized as such later in the parsing, we choose them
# to look the same as ordinary variables.
Name: "#000000", # class: 'n'
Name.Attribute: "#c4a000", # class: 'na' - to be revised
Name.Builtin: "#004461", # class: 'nb'
Name.Builtin.Pseudo: "#3465a4", # class: 'bp'
Name.Class: "#000000", # class: 'nc' - to be revised
Name.Constant: "#000000", # class: 'no' - to be revised
Name.Decorator: "#888", # class: 'nd' - to be revised
Name.Entity: "#ce5c00", # class: 'ni'
Name.Exception: "bold #cc0000", # class: 'ne'
Name.Function: "#000000", # class: 'nf'
Name.Property: "#000000", # class: 'py'
Name.Label: "#f57900", # class: 'nl'
Name.Namespace: "#000000", # class: 'nn' - to be revised
Name.Other: "#000000", # class: 'nx'
Name.Tag: "bold #004461", # class: 'nt' - like a keyword
Name.Variable: "#000000", # class: 'nv' - to be revised
Name.Variable.Class: "#000000", # class: 'vc' - to be revised
Name.Variable.Global: "#000000", # class: 'vg' - to be revised
Name.Variable.Instance: "#000000", # class: 'vi' - to be revised
Number: "#990000", # class: 'm'
Literal: "#000000", # class: 'l'
Literal.Date: "#000000", # class: 'ld'
String: "#4e9a06", # class: 's'
String.Backtick: "#4e9a06", # class: 'sb'
String.Char: "#4e9a06", # class: 'sc'
String.Doc: "italic #8f5902", # class: 'sd' - like a comment
String.Double: "#4e9a06", # class: 's2'
String.Escape: "#4e9a06", # class: 'se'
String.Heredoc: "#4e9a06", # class: 'sh'
String.Interpol: "#4e9a06", # class: 'si'
String.Other: "#4e9a06", # class: 'sx'
String.Regex: "#4e9a06", # class: 'sr'
String.Single: "#4e9a06", # class: 's1'
String.Symbol: "#4e9a06", # class: 'ss'
Generic: "#000000", # class: 'g'
Generic.Deleted: "#a40000", # class: 'gd'
Generic.Emph: "italic #000000", # class: 'ge'
Generic.Error: "#ef2929", # class: 'gr'
Generic.Heading: "bold #000080", # class: 'gh'
Generic.Inserted: "#00A000", # class: 'gi'
Generic.Output: "#888", # class: 'go'
Generic.Prompt: "#745334", # class: 'gp'
Generic.Strong: "bold #000000", # class: 'gs'
Generic.Subheading: "bold #800080", # class: 'gu'
Generic.Traceback: "bold #a40000", # class: 'gt'
}

View file

@ -0,0 +1,134 @@
from __future__ import with_statement
from time import time
from hashlib import sha1
from contextlib import closing
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
from sqlalchemy.orm import scoped_session
from sqlalchemy.exceptions import SQLError
from flask import request, redirect, abort, url_for, flash, session
from flask_website.database import User, db_session, OpenIDAssociation, \
OpenIDUserNonce
class WebsiteOpenIDStore(OpenIDStore):
"""Implements the open store for the website using the database."""
def storeAssociation(self, server_url, association):
assoc = OpenIDAssociation(
server_url=server_url,
handle=association.handle,
secret=association.secret.encode('base64'),
issued=association.issued,
lifetime=association.lifetime,
assoc_type=association.assoc_type
)
db_session.add(assoc)
def getAssociation(self, server_url, handle=None):
q = OpenIDAssociation.query.filter_by(server_url=server_url)
if handle is not None:
q = q.filter_by(handle=handle)
result_assoc = None
for item in q.all():
assoc = Association(item.handle, item.secret.decode('base64'),
item.issued, item.lifetime, item.assoc_type)
if assoc.getExpiresIn() <= 0:
self.removeAssociation(server_url, assoc.handle)
else:
result_assoc = assoc
return result_assoc
def removeAssociation(self, server_url, handle):
return OpenIDAssociation.filter(
(OpenIDAssociation.server_url == server_url) &
(OpenIDAssociation.handle == handle)
).delete()
def useNonce(self, server_url, timestamp, salt):
if abs(timestamp - time()) > nonce.SKEW:
return False
rv = OpenIDUserNonce.query.filter(
(OpenIDUserNonce.server_url == server_url) &
(OpenIDUserNonce.timestamp == timestamp) &
(OpenIDUserNonce.salt == salt)
).first()
if rv is not None:
return False
rv = OpenIDUserNonce(server_url=server_url, timestamp=timestamp,
salt=salt)
db_session.add(rv)
return True
def cleanupNonces(self):
return OpenIDUserNonce.filter(
OpenIDUserNonce.timestamp <= int(time() - nonce.SKEW)
).delete()
def cleanupAssociations(self):
return OpenIDAssociation.filter(
OpenIDAssociation.lifetime < int(time())
).delete()
def getAuthKey(self):
return sha1(config.SECRET_KEY).hexdigest()[:self.AUTH_KEY_LEN]
def isDump(self):
return False
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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 954 B

View file

@ -2,12 +2,15 @@ body { font-family: 'Georgia', serif; font-size: 17px; color: #000; }
a { color: #004B6B; }
a:hover { color: #6D4100; }
.box { width: 540px; margin: 40px auto; }
h1, h2, h3 { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; }
h2, h3 { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; }
h1 { margin: 0 0 30px 0; background: url(/static/flask.png) no-repeat center;
height: 165px; }
h1 span { display: none; }
h2 { font-size: 28px; margin: 15px 0 5px 0; }
h3 { font-size: 22px; margin: 15px 0 5px 0; }
code,
textarea, code,
pre { font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono',
monospace; font-size: 15px; background: #eee; }
monospace!important; font-size: 15px; background: #eee; }
pre { padding: 7px 30px; margin: 15px -30px; line-height: 1.3; }
.ig { color: #888; }
p { line-height: 1.4; }
@ -19,6 +22,27 @@ blockquote { margin: 0; font-style: italic; color: #444; }
.nav a { font-style: italic; }
.backnav { float: right; color: #444; font-style: italic;
margin: 5px 0 0 0; font-size: 0.9em; }
.message { background: #DEEBF3; color: #004B6B; padding: 5px 30px;
margin: 10px -30px; }
/* forms */
input, textarea { border: 1px solid black; padding: 2px; background: white;
font-family: 'Georgia', serif; font-size: 17px; color: #004B6B; }
textarea { width: 99%; }
input[type="submit"] { background: #DEEBF3; border-color: #004B6B; }
input.openid { background: url(openid.png) no-repeat 4px center;
padding-left: 26px; }
/* snippets */
.snippet-author { margin: 0 0 20px 0; font-size: 0.9em; }
#comment-box { background: #fafafa; margin: 45px -30px 15px -30px; padding: 10px 30px; }
.comments { margin-top: 0; }
.comments .title { border-bottom: 1px solid black; margin: 0; font-style: italic; }
.comments .body { margin: 0 0 0 30px; }
.comments .body pre { padding: 7px 30px 7px 60px; margin: 10px -30px 10px -60px; }
.comments .body p { margin: 10px 0; }
.comments li:before { display: none; }
#preview { margin: 20px -30px; padding: 10px 30px; background: #fafafa; }
/* mailinglist */
.pagination { text-align: center; font-size: 15px; margin: 20px 0 0 0; }
@ -45,3 +69,74 @@ blockquote { margin: 0; font-style: italic; color: #444; }
line-height: 1.15; }
.mail .quote { color: #004B6B; }
.mail .sig { color: #888; }
/* pygments style, same as flaskystyle */
.hll { background-color: #ffffcc }
.c { color: #8f5902; font-style: italic } /* Comment */
.err { color: #a40000; border: 1px solid #ef2929 } /* Error */
.g { color: #000000 } /* Generic */
.k { color: #004461; font-weight: bold } /* Keyword */
.l { color: #000000 } /* Literal */
.n { color: #000000 } /* Name */
.o { color: #582800 } /* Operator */
.x { color: #000000 } /* Other */
.p { color: #000000; font-weight: bold } /* Punctuation */
.cm { color: #8f5902; font-style: italic } /* Comment.Multiline */
.cp { color: #8f5902 } /* Comment.Preproc */
.c1 { color: #8f5902; font-style: italic } /* Comment.Single */
.cs { color: #8f5902; font-style: italic } /* Comment.Special */
.gd { color: #a40000 } /* Generic.Deleted */
.ge { color: #000000; font-style: italic } /* Generic.Emph */
.gr { color: #ef2929 } /* Generic.Error */
.gh { color: #000080; font-weight: bold } /* Generic.Heading */
.gi { color: #00A000 } /* Generic.Inserted */
.go { color: #808080 } /* Generic.Output */
.gp { color: #745334 } /* Generic.Prompt */
.gs { color: #000000; font-weight: bold } /* Generic.Strong */
.gu { color: #800080; font-weight: bold } /* Generic.Subheading */
.gt { color: #a40000; font-weight: bold } /* Generic.Traceback */
.kc { color: #004461; font-weight: bold } /* Keyword.Constant */
.kd { color: #004461; font-weight: bold } /* Keyword.Declaration */
.kn { color: #004461; font-weight: bold } /* Keyword.Namespace */
.kp { color: #004461; font-weight: bold } /* Keyword.Pseudo */
.kr { color: #004461; font-weight: bold } /* Keyword.Reserved */
.kt { color: #004461; font-weight: bold } /* Keyword.Type */
.ld { color: #000000 } /* Literal.Date */
.m { color: #990000 } /* Literal.Number */
.s { color: #4e9a06 } /* Literal.String */
.na { color: #c4a000 } /* Name.Attribute */
.nb { color: #004461 } /* Name.Builtin */
.nc { color: #000000 } /* Name.Class */
.no { color: #000000 } /* Name.Constant */
.nd { color: #808080 } /* Name.Decorator */
.ni { color: #ce5c00 } /* Name.Entity */
.ne { color: #cc0000; font-weight: bold } /* Name.Exception */
.nf { color: #000000 } /* Name.Function */
.nl { color: #f57900 } /* Name.Label */
.nn { color: #000000 } /* Name.Namespace */
.nx { color: #000000 } /* Name.Other */
.py { color: #000000 } /* Name.Property */
.nt { color: #004461; font-weight: bold } /* Name.Tag */
.nv { color: #000000 } /* Name.Variable */
.ow { color: #004461; font-weight: bold } /* Operator.Word */
.w { color: #f8f8f8; text-decoration: underline } /* Text.Whitespace */
.mf { color: #990000 } /* Literal.Number.Float */
.mh { color: #990000 } /* Literal.Number.Hex */
.mi { color: #990000 } /* Literal.Number.Integer */
.mo { color: #990000 } /* Literal.Number.Oct */
.sb { color: #4e9a06 } /* Literal.String.Backtick */
.sc { color: #4e9a06 } /* Literal.String.Char */
.sd { color: #8f5902; font-style: italic } /* Literal.String.Doc */
.s2 { color: #4e9a06 } /* Literal.String.Double */
.se { color: #4e9a06 } /* Literal.String.Escape */
.sh { color: #4e9a06 } /* Literal.String.Heredoc */
.si { color: #4e9a06 } /* Literal.String.Interpol */
.sx { color: #4e9a06 } /* Literal.String.Other */
.sr { color: #4e9a06 } /* Literal.String.Regex */
.s1 { color: #4e9a06 } /* Literal.String.Single */
.ss { color: #4e9a06 } /* Literal.String.Symbol */
.bp { color: #3465a4 } /* Name.Builtin.Pseudo */
.vc { color: #000000 } /* Name.Variable.Class */
.vg { color: #000000 } /* Name.Variable.Global */
.vi { color: #000000 } /* Name.Variable.Instance */
.il { color: #990000 } /* Literal.Number.Integer.Long */

View file

@ -0,0 +1,27 @@
{% extends "layout.html" %}
{% block head %}
{{ super() }}
<style type=text/css>
h1 { background-image: url(/static/login.png); }
</style>
{% endblock %}
{% block title %}First Login{% endblock %}
{% block body %}
<h2>This is your First Login</h2>
<form action="{{ url_for('general.first_login') }}" method=post>
<p>
This is your first login on this website. You are about to sign
in with the following OpenID: <a href="{{ openid }}">{{ openid }}</a>
<p>
Before you can use this site we need a name for you. This is what
will be displayed next to all of your public contributions to this
site. Choose wisely because you cannot change this value later:
<dl>
<dt>Name:
<dd><input type=text name=name size=30>
</dl>
<p>
<input type=submit value=Continue>
<input type=submit name=cancel value=Cancel>
</form>
{% endblock %}

View file

@ -0,0 +1,22 @@
{% extends "layout.html" %}
{% block head %}
{{ super() }}
<style type=text/css>
h1 { background-image: url(/static/login.png); }
</style>
{% endblock %}
{% block title %}Login{% endblock %}
{% block body %}
<form action="{{ url_for('general.login') }}" method=post>
<p>
For some of the features on this site (such as creating snippets
or adding comments) you have to be signed in. You don't need to
create an account on this website, just sign in with an existing
<a href=http://openid.net/>OpenID</a> account.
<p>
OpenID URL:
<input type=text name=openid class=openid size=30>
<input type=hidden name=next value="{{ request.values.next or request.referrer }}">
<input type=submit value=Login>
</form>
{% endblock %}

View file

@ -15,6 +15,15 @@
<a href=/docs/>documentation</a> //
<a href=/mailinglist/>mailinglist</a> //
<a href=/snippets/>snippets</a>
{% for message in get_flashed_messages() %}
<p class=message>{{ message }}
{% endfor %}
{% block body %}{% endblock %}
<p class=footer>&copy; Copyright 2010 by <a href=http://lucumr.pocoo.org/>Armin Ronacher</a>
<p class=footer>
&copy; Copyright 2010 by <a href=http://lucumr.pocoo.org/>Armin Ronacher</a> //
{% if g.user %}
<a href=/logout/ title="logged in as {{ g.user.name }} [{{ g.user.openid }}]">logout</a>
{% else %}
<a href=/login/>login</a>
{% endif %}
</div>

View file

@ -2,8 +2,7 @@
{% block head %}
{{ super() }}
<style type=text/css>
h1 { margin: 0 0 30px 0; background: url(/static/mailinglist.png) no-repeat center; height: 165px; }
h1 span { display: none; }
h1 { background-image: url(/static/mailinglist.png); }
</style>
{% endblock %}
{% block body_title %}

View file

@ -0,0 +1,19 @@
{% extends "snippets/layout.html" %}
{% block title %}{{ category.name }}{% endblock %}
{% block body %}
<h2>{{ category.name }}</h2>
{% if category.count > 3 %}
<p>
Here you can find all {{ category.count }} snippets of this
category. You can also
{% else %}
This category is pretty empty so far. Why not
{% endif %}
<a href="{{ url_for('snippets.new',
category=category.slug) }}">add a new one</a>.
<ul>
{% for snippet in snippets %}
<li><a href="{{ snippet.url }}">{{ snippet.title }}</a>
{% endfor %}
</ul>
{% endblock %}

View file

@ -0,0 +1,44 @@
{% extends "snippets/layout.html" %}
{% block head %}
{{ super() }}
<style type=text/css>
h1 { background-image: url(/static/new-snippet.png); }
</style>
{% endblock %}
{% block title %}Edit Snippet “{{ snippet.title }}”{% endblock %}
{% block body %}
<script type=text/javascript>
$(function() {
$('input[name="delete"]').click(function() {
if (!confirm("Do you really want to delete this Snippet?\n\n" +
"THIS CANNOT BE UNDONE!"))
return false;
});
});
</script>
<form action="" method=post>
<dl>
<dt>Title:
<dd><input type=text name=title value="{{ form.title }}" size=40>
<dt>Category:
<dd>
<select name=category>
{% for category in categories %}
<option{% if category.id == form.category %} selected{% endif
%} value={{ category.id }}>{{ category.name }}</option>
{% endfor %}
</select>
</dl>
<p><textarea name=body cols=40 rows=20>{{ form.body }}</textarea>
<p>
<input type=submit value="Edit Snippet">
<input type=submit name=delete value="Delete">
<input type=submit name=preview value="Preview">
</form>
{% if preview %}
<div id=preview>
<h2>Preview</h2>
{{ preview }}
</div>
{% endif %}
{% endblock %}

View file

@ -4,14 +4,33 @@
<p>
Welcome to the Flask snippet archive. This is the place where anyone
can drop helpful pieces of code for others to use.
{% if g.user %}
<p>
You're signed in as “<span title="{{ g.user.openid }}">{{ g.user.name
}}</span>”. You can <a href="{{ url_for('general.logout')
}}">sign out</a> here after you're done if you want.
{% else %}
<p>
In order to add snippets to this page or to add comments, all you need
is an <a href=http://en.wikipedia.org/wiki/OpenID>OpenID</a> account.
<form action=/snippets/search/ method=get>
<p>
Search snippets:
<input type=text name=q size=30>
<input type=submit value=Search>
</form>
You can <a href="{{ url_for('general.login') }}">sign in</a> here.
{% endif %}
<p>
Want to share something? Then add a
<a href="{{ url_for('snippets.new') }}">new snippet</a>.
<h2>Snippets by Category</h2>
<ul>
{% for category in categories %}
<li><a href="{{ category.url }}">{{ category.name }}</a> ({{ category.count }})
{% endfor %}
</ul>
{% if recent %}
<h2>Recently Added</h2>
<ul>
{% for snippet in recent %}
<li><a href="{{ snippet.url }}">{{ snippet.title }}</a> in
<a href="{{ snippet.category.url }}">{{ snippet.category.name }}</a>
{% endfor %}
</ul>
{% endif %}
{% endblock %}

View file

@ -2,8 +2,7 @@
{% block head %}
{{ super() }}
<style type=text/css>
h1 { margin: 0 0 30px 0; background: url(/static/snippets.png) no-repeat center; height: 165px; }
h1 span { display: none; }
h1 { background-image: url(/static/snippets.png); }
</style>
{% endblock %}
{% block body_title %}

View file

@ -0,0 +1,43 @@
{% extends "snippets/layout.html" %}
{% block head %}
{{ super() }}
<style type=text/css>
h1 { background-image: url(/static/new-snippet.png); }
</style>
{% endblock %}
{% block title %}New Snippet{% endblock %}
{% block body %}
<p>
Got something to share? Here you can create a new snippet and
publish it here. By adding the snippet here, you hereby grant
the user of the snippet all rights.
<p>
The syntax used for snippets is <a href="http://www.wikicreole.org/">Creole</a>.
To highlight Python code or Jinja templates, prefix your code blocks
with <code>#!python</code>, <code>#!html+jinja</code> or any other
<a href="http://pygments.org/docs/lexers">Pygments lexer name</a>.
<form action="" method=post>
<dl>
<dt>Title:
<dd><input type=text name=title value="{{ request.form.title }}" size=40>
<dt>Category:
<dd>
<select name=category>
{% for category in categories %}
<option{% if category.id == active_category %} selected{% endif
%} value={{ category.id }}>{{ category.name }}</option>
{% endfor %}
</select>
</dl>
<p><textarea name=body cols=40 rows=20>{{ request.form.body }}</textarea>
<p>
<input type=submit value="Add Snippet">
<input type=submit name=preview value="Preview">
</form>
{% if preview %}
<div id=preview>
<h2>Preview</h2>
{{ preview }}
</div>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,38 @@
{% extends "snippets/layout.html" %}
{% block title %}{{ snippet.title }}{% endblock %}
{% block body %}
<h2>{{ snippet.title }}</h2>
<p class=snippet-author>By {{ snippet.author.name }}
filed in <a href="{{ snippet.category.url }}">{{ snippet.category.name }}</a>
{% if snippet.author == g.user %}
(<a href="{{ url_for('snippets.edit', id=snippet.id) }}">edit</a>)
{% endif %}
{{ snippet.rendered_body }}
{% if snippet.comments or g.user %}
<div id=comment-box>
{% if snippet.comments %}
<h2>Comments</h2>
<ul class=comments>
{% for comment in snippet.comments %}
<li>
<p class=title>
{{ comment.title or "Comment" }}
by {{ comment.author.name }}
on {{ comment.pub_date.strftime('%Y-%m-%d @ %H:%M') }}
<div class=body>{{ comment.rendered_text }}</div></li>
{% endfor %}
</ul>
{% endif %}
{% if g.user %}
<div id=add-comment>
<h2>Add Comment</h2>
<form action=#add-comment method=post>
<p>Title: <input type=text name=title value="{{ request.form.title }}" size=30>
<p><textarea name=text cols=40 rows=8>{{ request.form.text }}</textarea>
<p><input type=submit value="Add Comment">
</form>
</div>
{% endif %}
</div>
{% endif %}
{% endblock %}

56
flask_website/utils.py Normal file
View file

@ -0,0 +1,56 @@
import creoleparser
from genshi import builder
from functools import wraps
from creoleparser.elements import PreBlock
from pygments import highlight
from pygments.formatters import HtmlFormatter
from pygments.lexers import get_lexer_by_name
from pygments.util import ClassNotFound
from flask import g, url_for, flash, request, redirect, Markup
from flask_website.flaskystyle import FlaskyStyle # same as docs
from flask_website.database import User
pygments_formatter = HtmlFormatter(style=FlaskyStyle)
class CodeBlock(PreBlock):
def __init__(self):
super(CodeBlock, self).__init__('pre', ['{{{', '}}}'])
def _build(self, mo, element_store, environ):
lines = self.regexp2.sub(r'\1', mo.group(1)).splitlines()
if lines and lines[0].startswith('#!'):
try:
lexer = get_lexer_by_name(lines.pop(0)[2:].strip())
except ClassNotFound:
pass
else:
return Markup(highlight(u'\n'.join(lines), lexer,
pygments_formatter))
return builder.tag.pre(u'\n'.join(lines))
custom_dialect = creoleparser.create_dialect(creoleparser.creole10_base)
custom_dialect.pre = CodeBlock()
_parser = creoleparser.Parser(
dialect=custom_dialect,
method='html'
)
def format_creole(text):
return Markup(_parser.render(text, encoding=None))
def requires_login(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if g.user is None:
flash(u'You need to be signed in for this page.')
return redirect(url_for('general.login', next=request.path))
return f(*args, **kwargs)
return decorated_function

View file

@ -1,4 +1,7 @@
from flask import Module, render_template
from flask import Module, render_template, session, redirect, url_for, \
request, flash, g, Response
from flask_website import openid_auth
from flask_website.database import db_session, User
general = Module(__name__)
@ -6,3 +9,42 @@ general = Module(__name__)
@general.route('/')
def index():
return render_template('general/index.html')
@general.route('/logout/')
def logout():
if 'openid' in session:
flash(u'Logged out')
del session['openid']
return redirect(request.referrer or url_for('general.index'))
@general.route('/login/', methods=['GET', 'POST'])
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')
@general.route('/first-login/', methods=['GET', 'POST'])
def first_login():
if g.user is not None or 'openid' not in session:
return redirect(url_for('login'))
if request.method == 'POST':
if 'cancel' in request.form:
del session['openid']
flash(u'Login was aborted')
return redirect(url_for('general.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 render_template('general/first_login.html',
openid=session['openid'])

View file

@ -1,8 +1,116 @@
from flask import Module, render_template
from flask import Module, render_template, request, flash, abort, redirect, \
g, url_for
from flask_website.utils import requires_login, format_creole
from flask_website.database import Category, Snippet, Comment, db_session
snippets = Module(__name__, url_prefix='/snippets')
@snippets.route('/')
def index():
return render_template('snippets/index.html')
return render_template('snippets/index.html',
categories=Category.query.order_by(Category.name).all(),
recent=Snippet.query.order_by(Snippet.pub_date.desc()).limit(5).all())
@snippets.route('/new/', methods=['GET', 'POST'])
@requires_login
def new():
category_id = None
preview = None
if 'category' in request.args:
rv = Category.query.filter_by(slug=request.args['category']).first()
if rv is not None:
category_id = rv.id
if request.method == 'POST':
if 'preview' in request.form:
preview = format_creole(request.form['body'])
else:
title = request.form['title']
body = request.form['body']
category_id = request.form.get('category', type=int)
if not body:
flash(u'Error: you have to enter a snippet')
else:
category = Category.query.get(category_id)
if category is not None:
snippet = Snippet(g.user, title, body, category)
db_session.add(snippet)
db_session.commit()
flash(u'Your snippet was added')
return redirect(snippet.url)
return render_template('snippets/new.html',
categories=Category.query.order_by(Category.name).all(),
active_category=category_id, preview=preview)
@snippets.route('/<int:id>/', methods=['GET', 'POST'])
def show(id):
snippet = Snippet.query.get(id)
if snippet is None:
abort(404)
if request.method == 'POST':
title = request.form['title']
text = request.form['text']
if not title:
flash(u'Error: the title is required')
elif not text:
flash(u'Error: the text is required')
else:
db_session.add(Comment(snippet, g.user, title, text))
db_session.commit()
flash(u'Your comment was added')
return redirect(snippet.url)
return render_template('snippets/show.html', snippet=snippet)
@snippets.route('/edit/<int:id>/', methods=['GET', 'POST'])
@requires_login
def edit(id):
snippet = Snippet.query.get(id)
if snippet is None:
abort(404)
if snippet.author != g.user:
abort(401)
preview = None
form = dict(title=snippet.title, body=snippet.body,
category=snippet.category.id)
if request.method == 'POST':
form['title'] = request.form['title']
form['body'] = request.form['body']
form['category'] = request.form.get('category', type=int)
if 'preview' in request.form:
preview = format_creole(request.form['body'])
elif 'delete' in request.form:
for comment in snippet.comments:
db_session.delete(comment)
db_session.delete(snippet)
db_session.commit()
flash(u'Your snippet was deleted')
return redirect(url_for('snippets.index'))
else:
category_id = request.form.get('category', type=int)
if not form['body']:
flash(u'Error: you have to enter a snippet')
else:
category = Category.query.get(category_id)
if category is not None:
snippet.title = form['title']
snippet.body = form['body']
snippet.category = category
db_session.commit()
flash(u'Your snippet was modified')
return redirect(snippet.url)
return render_template('snippets/edit.html',
snippet=snippet, preview=preview, form=form,
categories=Category.query.order_by(Category.name).all())
@snippets.route('/category/<slug>/')
def category(slug):
category = Category.query.filter_by(slug=slug).first()
if category is None:
abort(404)
snippets = category.snippets.order_by(Snippet.title).all()
return render_template('snippets/category.html', category=category,
snippets=snippets)