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
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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 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)
|
||||
|
|
|
|||
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: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 */
|
||||
|
|
|
|||
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=/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>© 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>
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
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>
|
||||
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 %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
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__)
|
||||
|
||||
|
|
@ -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'])
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue