Added snippet database to the website.
This commit is contained in:
parent
a81cf3a67c
commit
0ab7a9cb67
22 changed files with 825 additions and 118 deletions
|
|
@ -1,17 +1,25 @@
|
||||||
from flask import Flask, render_template
|
from flask import Flask, session, g, render_template
|
||||||
|
|
||||||
import websiteconfig as config
|
import websiteconfig as config
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.debug = config.DEBUG
|
app.debug = config.DEBUG
|
||||||
|
app.secret_key = config.SECRET_KEY
|
||||||
|
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
def not_found(error):
|
def not_found(error):
|
||||||
return render_template('404.html'), 404
|
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.general import general
|
||||||
from flask_website.views.mailinglist import mailinglist
|
from flask_website.views.mailinglist import mailinglist
|
||||||
from flask_website.views.snippets import snippets
|
from flask_website.views.snippets import snippets
|
||||||
app.register_module(general)
|
app.register_module(general)
|
||||||
app.register_module(mailinglist)
|
app.register_module(mailinglist)
|
||||||
app.register_module(snippets)
|
app.register_module(snippets)
|
||||||
|
|
||||||
|
from flask_website.database import User
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from sqlalchemy import create_engine, MetaData, Table, Column, Integer, \
|
from sqlalchemy import create_engine, MetaData, Table, Column, Integer, \
|
||||||
String, DateTime, ForeignKey
|
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 sqlalchemy.ext.declarative import declarative_base
|
||||||
|
|
||||||
|
from werkzeug import cached_property
|
||||||
|
|
||||||
|
from flask import url_for
|
||||||
from flask_website import config
|
from flask_website import config
|
||||||
|
|
||||||
engine = create_engine(config.DATABASE_URI)
|
engine = create_engine(config.DATABASE_URI)
|
||||||
|
|
@ -15,20 +18,25 @@ def init_db():
|
||||||
Model.metadata.create_all(bind=engine)
|
Model.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
|
||||||
class Model(declarative_base()):
|
Model = declarative_base(name='Model')
|
||||||
query = db_session.query_property()
|
Model.query = db_session.query_property()
|
||||||
|
|
||||||
|
|
||||||
class User(Model):
|
class User(Model):
|
||||||
__tablename__ = 'users'
|
__tablename__ = 'users'
|
||||||
id = Column('user_id', Integer, primary_key=True)
|
id = Column('user_id', Integer, primary_key=True)
|
||||||
openid = Column('openid', String(200))
|
openid = Column('openid', String(200))
|
||||||
username = Column(String(40), unique=True)
|
name = Column(String(200), unique=True)
|
||||||
password = Column(String(80))
|
|
||||||
|
|
||||||
def __init__(self, username, password):
|
def __init__(self, name, openid):
|
||||||
self.username = username
|
self.name = name
|
||||||
self.password = password
|
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):
|
class Category(Model):
|
||||||
|
|
@ -37,40 +45,73 @@ class Category(Model):
|
||||||
name = Column(String(50))
|
name = Column(String(50))
|
||||||
slug = 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):
|
class Snippet(Model):
|
||||||
__tablename__ = 'snippets'
|
__tablename__ = 'snippets'
|
||||||
id = Column('snippet_id', Integer, primary_key=True)
|
id = Column('snippet_id', Integer, primary_key=True)
|
||||||
author = ForeignKey(User, backref=backref('snippets', lazy='dynamic'))
|
author_id = Column(Integer, ForeignKey('users.user_id'))
|
||||||
category = ForeignKey(Category, backref=backref('snippets', lazy='dynamic'))
|
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))
|
title = Column(String(200))
|
||||||
body = Column(String)
|
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.author = author
|
||||||
self.title = title
|
self.title = title
|
||||||
self.body = body
|
self.body = body
|
||||||
|
self.category = category
|
||||||
self.pub_date = datetime.utcnow()
|
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):
|
class Comment(Model):
|
||||||
__tablename__ = 'comments'
|
__tablename__ = 'comments'
|
||||||
id = Column('comment_id', Integer, primary_key=True)
|
id = Column('comment_id', Integer, primary_key=True)
|
||||||
snippet = ForeignKey(Snippet, backref='lazy')
|
snippet_id = Column(Integer, ForeignKey('snippets.snippet_id'))
|
||||||
author = ForeignKey(User, backref=backref('comments', lazy='dynamic'))
|
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))
|
title = Column(String(200))
|
||||||
text = Column(String)
|
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.author = author
|
||||||
self.title = title
|
self.title = title
|
||||||
self.text = text
|
self.text = text
|
||||||
self.pub_date = datetime.utcnow()
|
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):
|
class OpenIDAssociation(Model):
|
||||||
|
__tablename__ = 'openid_associations'
|
||||||
id = Column('association_id', Integer, primary_key=True)
|
id = Column('association_id', Integer, primary_key=True)
|
||||||
server_url = Column(String(1024))
|
server_url = Column(String(1024))
|
||||||
handle = Column(String(255))
|
handle = Column(String(255))
|
||||||
|
|
@ -80,7 +121,8 @@ class OpenIDAssociation(Model):
|
||||||
assoc_type = Column(String(64))
|
assoc_type = Column(String(64))
|
||||||
|
|
||||||
|
|
||||||
class OpenIDUserNonces(Model):
|
class OpenIDUserNonce(Model):
|
||||||
|
__tablename__ = 'openid_user_nonces'
|
||||||
id = Column('user_nonce_id', Integer, primary_key=True)
|
id = Column('user_nonce_id', Integer, primary_key=True)
|
||||||
server_url = Column(String(1024))
|
server_url = Column(String(1024))
|
||||||
timestamp = Column(Integer)
|
timestamp = Column(Integer)
|
||||||
|
|
|
||||||
86
flask_website/flaskystyle.py
Normal file
86
flask_website/flaskystyle.py
Normal 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'
|
||||||
|
}
|
||||||
134
flask_website/openid_auth.py
Normal file
134
flask_website/openid_auth.py
Normal 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()
|
||||||
BIN
flask_website/static/login.png
Normal file
BIN
flask_website/static/login.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
flask_website/static/new-snippet.png
Normal file
BIN
flask_website/static/new-snippet.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
flask_website/static/openid.png
Normal file
BIN
flask_website/static/openid.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 954 B |
|
|
@ -2,12 +2,15 @@ body { font-family: 'Georgia', serif; font-size: 17px; color: #000; }
|
||||||
a { color: #004B6B; }
|
a { color: #004B6B; }
|
||||||
a:hover { color: #6D4100; }
|
a:hover { color: #6D4100; }
|
||||||
.box { width: 540px; margin: 40px auto; }
|
.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; }
|
h2 { font-size: 28px; margin: 15px 0 5px 0; }
|
||||||
h3 { font-size: 22px; 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',
|
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; }
|
pre { padding: 7px 30px; margin: 15px -30px; line-height: 1.3; }
|
||||||
.ig { color: #888; }
|
.ig { color: #888; }
|
||||||
p { line-height: 1.4; }
|
p { line-height: 1.4; }
|
||||||
|
|
@ -19,6 +22,27 @@ blockquote { margin: 0; font-style: italic; color: #444; }
|
||||||
.nav a { font-style: italic; }
|
.nav a { font-style: italic; }
|
||||||
.backnav { float: right; color: #444; font-style: italic;
|
.backnav { float: right; color: #444; font-style: italic;
|
||||||
margin: 5px 0 0 0; font-size: 0.9em; }
|
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 */
|
/* mailinglist */
|
||||||
.pagination { text-align: center; font-size: 15px; margin: 20px 0 0 0; }
|
.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; }
|
line-height: 1.15; }
|
||||||
.mail .quote { color: #004B6B; }
|
.mail .quote { color: #004B6B; }
|
||||||
.mail .sig { color: #888; }
|
.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 */
|
||||||
|
|
|
||||||
27
flask_website/templates/general/first_login.html
Normal file
27
flask_website/templates/general/first_login.html
Normal 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 %}
|
||||||
22
flask_website/templates/general/login.html
Normal file
22
flask_website/templates/general/login.html
Normal 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 %}
|
||||||
|
|
@ -15,6 +15,15 @@
|
||||||
<a href=/docs/>documentation</a> //
|
<a href=/docs/>documentation</a> //
|
||||||
<a href=/mailinglist/>mailinglist</a> //
|
<a href=/mailinglist/>mailinglist</a> //
|
||||||
<a href=/snippets/>snippets</a>
|
<a href=/snippets/>snippets</a>
|
||||||
|
{% for message in get_flashed_messages() %}
|
||||||
|
<p class=message>{{ message }}
|
||||||
|
{% endfor %}
|
||||||
{% block body %}{% endblock %}
|
{% block body %}{% endblock %}
|
||||||
<p class=footer>© Copyright 2010 by <a href=http://lucumr.pocoo.org/>Armin Ronacher</a>
|
<p class=footer>
|
||||||
|
© 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>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,7 @@
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<style type=text/css>
|
<style type=text/css>
|
||||||
h1 { margin: 0 0 30px 0; background: url(/static/mailinglist.png) no-repeat center; height: 165px; }
|
h1 { background-image: url(/static/mailinglist.png); }
|
||||||
h1 span { display: none; }
|
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block body_title %}
|
{% block body_title %}
|
||||||
|
|
|
||||||
19
flask_website/templates/snippets/category.html
Normal file
19
flask_website/templates/snippets/category.html
Normal 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 %}
|
||||||
44
flask_website/templates/snippets/edit.html
Normal file
44
flask_website/templates/snippets/edit.html
Normal 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 %}
|
||||||
|
|
@ -4,14 +4,33 @@
|
||||||
<p>
|
<p>
|
||||||
Welcome to the Flask snippet archive. This is the place where anyone
|
Welcome to the Flask snippet archive. This is the place where anyone
|
||||||
can drop helpful pieces of code for others to use.
|
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>
|
<p>
|
||||||
In order to add snippets to this page or to add comments, all you need
|
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.
|
is an <a href=http://en.wikipedia.org/wiki/OpenID>OpenID</a> account.
|
||||||
<form action=/snippets/search/ method=get>
|
You can <a href="{{ url_for('general.login') }}">sign in</a> here.
|
||||||
|
{% endif %}
|
||||||
<p>
|
<p>
|
||||||
Search snippets:
|
Want to share something? Then add a
|
||||||
<input type=text name=q size=30>
|
<a href="{{ url_for('snippets.new') }}">new snippet</a>.
|
||||||
<input type=submit value=Search>
|
|
||||||
</form>
|
|
||||||
<h2>Snippets by Category</h2>
|
<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 %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,7 @@
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<style type=text/css>
|
<style type=text/css>
|
||||||
h1 { margin: 0 0 30px 0; background: url(/static/snippets.png) no-repeat center; height: 165px; }
|
h1 { background-image: url(/static/snippets.png); }
|
||||||
h1 span { display: none; }
|
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block body_title %}
|
{% block body_title %}
|
||||||
|
|
|
||||||
43
flask_website/templates/snippets/new.html
Normal file
43
flask_website/templates/snippets/new.html
Normal 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 %}
|
||||||
38
flask_website/templates/snippets/show.html
Normal file
38
flask_website/templates/snippets/show.html
Normal 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
56
flask_website/utils.py
Normal 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
|
||||||
|
|
@ -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__)
|
general = Module(__name__)
|
||||||
|
|
||||||
|
|
@ -6,3 +9,42 @@ general = Module(__name__)
|
||||||
@general.route('/')
|
@general.route('/')
|
||||||
def index():
|
def index():
|
||||||
return render_template('general/index.html')
|
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'])
|
||||||
|
|
|
||||||
|
|
@ -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 = Module(__name__, url_prefix='/snippets')
|
||||||
|
|
||||||
|
|
||||||
@snippets.route('/')
|
@snippets.route('/')
|
||||||
def index():
|
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)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue