forked from coleifer/peewee
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
19 changed files
with
787 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,265 @@ | ||
import datetime | ||
import functools | ||
import os | ||
import re | ||
import urllib | ||
|
||
from flask import (Flask, flash, Markup, redirect, render_template, request, | ||
Response, session, url_for) | ||
from markdown import markdown | ||
from markdown.extensions.codehilite import CodeHiliteExtension | ||
from markdown.extensions.extra import ExtraExtension | ||
from micawber import bootstrap_basic, parse_html | ||
from micawber.cache import Cache as OEmbedCache | ||
from peewee import * | ||
from playhouse.flask_utils import FlaskDB, get_object_or_404, object_list | ||
from playhouse.sqlite_ext import * | ||
|
||
|
||
# Blog configuration values. | ||
|
||
# You may consider using a one-way hash to generate the password, and then | ||
# use the hash again in the login view to perform the comparison. This is just | ||
# for simplicity. | ||
ADMIN_PASSWORD = 'secret' | ||
APP_DIR = os.path.dirname(os.path.realpath(__file__)) | ||
|
||
# The playhouse.flask_utils.FlaskDB object accepts database URL configuration. | ||
DATABASE = 'sqliteext:///%s' % os.path.join(APP_DIR, 'blog.db') | ||
DEBUG = False | ||
|
||
# The secret key is used internally by Flask to encrypt session data stored | ||
# in cookies. Make this unique for your app. | ||
SECRET_KEY = 'shhh, secret!' | ||
|
||
# This is used by micawber, which will attempt to generate rich media | ||
# embedded objects with maxwidth=800. | ||
SITE_WIDTH = 800 | ||
|
||
|
||
# Create a Flask WSGI app and configure it using values from the module. | ||
app = Flask(__name__) | ||
app.config.from_object(__name__) | ||
|
||
# FlaskDB is a wrapper for a peewee database that sets up pre/post-request | ||
# hooks for managing database connections. | ||
flask_db = FlaskDB(app) | ||
|
||
# The `database` is the actual peewee database, as opposed to flask_db which is | ||
# the wrapper. | ||
database = flask_db.database | ||
|
||
# Configure micawber with the default OEmbed providers (YouTube, Flickr, etc). | ||
# We'll use a simple in-memory cache so that multiple requests for the same | ||
# video don't require multiple network requests. | ||
oembed_providers = bootstrap_basic(OEmbedCache()) | ||
|
||
|
||
class Entry(flask_db.Model): | ||
title = CharField() | ||
slug = CharField(unique=True) | ||
content = TextField() | ||
published = BooleanField(index=True) | ||
timestamp = DateTimeField(default=datetime.datetime.now, index=True) | ||
|
||
@property | ||
def html_content(self): | ||
""" | ||
Generate HTML representation of the markdown-formatted blog entry, | ||
and also convert any media URLs into rich media objects such as video | ||
players or images. | ||
""" | ||
hilite = CodeHiliteExtension(linenums=False, css_class='highlight') | ||
extras = ExtraExtension() | ||
markdown_content = markdown(self.content, extensions=[hilite, extras]) | ||
oembed_content = parse_html( | ||
markdown_content, | ||
oembed_providers, | ||
urlize_all=True, | ||
maxwidth=app.config['SITE_WIDTH']) | ||
return Markup(oembed_content) | ||
|
||
def save(self, *args, **kwargs): | ||
# Generate a URL-friendly representation of the entry's title. | ||
if not self.slug: | ||
self.slug = re.sub('[^\w]+', '-', self.title.lower()).strip('-') | ||
ret = super(Entry, self).save(*args, **kwargs) | ||
|
||
# Store search content. | ||
self.update_search_index() | ||
return ret | ||
|
||
def update_search_index(self): | ||
# Create a row in the FTSEntry table with the post content. This will | ||
# allow us to use SQLite's awesome full-text search extension to | ||
# search our entries. | ||
try: | ||
fts_entry = FTSEntry.get(FTSEntry.entry_id == self.id) | ||
except FTSEntry.DoesNotExist: | ||
fts_entry = FTSEntry(entry_id=self.id) | ||
force_insert = True | ||
else: | ||
force_insert = False | ||
fts_entry.content = '\n'.join((self.title, self.content)) | ||
fts_entry.save(force_insert=force_insert) | ||
|
||
@classmethod | ||
def public(cls): | ||
return Entry.select().where(Entry.published == True) | ||
|
||
@classmethod | ||
def drafts(cls): | ||
return Entry.select().where(Entry.published == False) | ||
|
||
@classmethod | ||
def search(cls, query): | ||
words = [word.strip() for word in query.split() if word.strip()] | ||
if not words: | ||
# Return an empty query. | ||
return Entry.select().where(Entry.id == 0) | ||
else: | ||
search = ' '.join(words) | ||
|
||
# Query the full-text search index for entries matching the given | ||
# search query, then join the actual Entry data on the matching | ||
# search result. | ||
return (FTSEntry | ||
.select( | ||
FTSEntry, | ||
Entry, | ||
FTSEntry.rank().alias('score')) | ||
.join(Entry, on=(FTSEntry.entry_id == Entry.id).alias('entry')) | ||
.where( | ||
(Entry.published == True) & | ||
(FTSEntry.match(search))) | ||
.order_by(SQL('score').desc())) | ||
|
||
class FTSEntry(FTSModel): | ||
entry_id = IntegerField(Entry) | ||
content = TextField() | ||
|
||
class Meta: | ||
database = database | ||
|
||
def login_required(fn): | ||
@functools.wraps(fn) | ||
def inner(*args, **kwargs): | ||
if session.get('logged_in'): | ||
return fn(*args, **kwargs) | ||
return redirect(url_for('login', next=request.path)) | ||
return inner | ||
|
||
@app.route('/login/', methods=['GET', 'POST']) | ||
def login(): | ||
next_url = request.args.get('next') or request.form.get('next') | ||
if request.method == 'POST' and request.form.get('password'): | ||
password = request.form.get('password') | ||
# TODO: If using a one-way hash, you would also hash the user-submitted | ||
# password and do the comparison on the hashed versions. | ||
if password == app.config['ADMIN_PASSWORD']: | ||
session['logged_in'] = True | ||
session.permanent = True # Use cookie to store session. | ||
flash('You are now logged in.', 'success') | ||
return redirect(next_url or url_for('index')) | ||
else: | ||
flash('Incorrect password.', 'danger') | ||
return render_template('login.html', next_url=next_url) | ||
|
||
@app.route('/logout/', methods=['GET', 'POST']) | ||
def logout(): | ||
if request.method == 'POST': | ||
session.clear() | ||
return redirect(url_for('login')) | ||
return render_template('logout.html') | ||
|
||
@app.route('/') | ||
def index(): | ||
search_query = request.args.get('q') | ||
if search_query: | ||
query = Entry.search(search_query) | ||
else: | ||
query = Entry.public().order_by(Entry.timestamp.desc()) | ||
|
||
# The `object_list` helper will take a base query and then handle | ||
# paginating the results if there are more than 20. For more info see | ||
# the docs: | ||
# http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#object_list | ||
return object_list('index.html', query, search=search_query) | ||
|
||
@app.route('/create/', methods=['GET', 'POST']) | ||
@login_required | ||
def create(): | ||
if request.method == 'POST': | ||
if request.form.get('title') and request.form.get('content'): | ||
entry = Entry.create( | ||
title=request.form['title'], | ||
content=request.form['title'], | ||
published=request.form.get('published') or False) | ||
flash('Entry created successfully.', 'success') | ||
if entry.published: | ||
return redirect(url_for('detail', slug=entry.slug)) | ||
else: | ||
return redirect(url_for('edit', slug=entry.slug)) | ||
else: | ||
flash('Title and Content are required.', 'danger') | ||
return render_template('create.html') | ||
|
||
@app.route('/drafts/') | ||
@login_required | ||
def drafts(): | ||
query = Entry.drafts().order_by(Entry.timestamp.desc()) | ||
return object_list('index.html', query) | ||
|
||
@app.route('/<slug>/') | ||
def detail(slug): | ||
if session.get('logged_in'): | ||
query = Entry.select() | ||
else: | ||
query = Entry.public() | ||
entry = get_object_or_404(query, Entry.slug == slug) | ||
return render_template('detail.html', entry=entry) | ||
|
||
@app.route('/<slug>/edit/', methods=['GET', 'POST']) | ||
@login_required | ||
def edit(slug): | ||
entry = get_object_or_404(Entry, Entry.slug == slug) | ||
if request.method == 'POST': | ||
if request.form.get('title') and request.form.get('content'): | ||
entry.title = request.form['title'] | ||
entry.content = request.form['content'] | ||
entry.published = request.form.get('published') or False | ||
entry.save() | ||
|
||
flash('Entry saved successfully.', 'success') | ||
if entry.published: | ||
return redirect(url_for('detail', slug=entry.slug)) | ||
else: | ||
return redirect(url_for('edit', slug=entry.slug)) | ||
else: | ||
flash('Title and Content are required.', 'danger') | ||
|
||
return render_template('edit.html', entry=entry) | ||
|
||
@app.template_filter('clean_querystring') | ||
def clean_querystring(request_args, *keys_to_remove, **new_values): | ||
# We'll use this template filter in the pagination include. This filter | ||
# will take the current URL and allow us to preserve the arguments in the | ||
# querystring while replacing any that we need to overwrite. For instance | ||
# if your URL is /?q=search+query&page=2 and we want to preserve the search | ||
# term but make a link to page 3, this filter will allow us to do that. | ||
querystring = dict((key, value) for key, value in request_args.items()) | ||
for key in keys_to_remove: | ||
querystring.pop(key, None) | ||
querystring.update(new_values) | ||
return urllib.urlencode(querystring) | ||
|
||
@app.errorhandler(404) | ||
def not_found(exc): | ||
return Response('<h3>Not found</h3>'), 404 | ||
|
||
def main(): | ||
database.create_tables([Entry, FTSEntry], safe=True) | ||
app.run(debug=True) | ||
|
||
if __name__ == '__main__': | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
flask | ||
BeautifulSoup | ||
micawber | ||
pygments | ||
markdown | ||
peewee |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
.highlight { | ||
background: #040400; | ||
color: #FFFFFF; | ||
} | ||
|
||
.highlight span.selection { color: #323232; } | ||
.highlight span.gp { color: #9595FF; } | ||
.highlight span.vi { color: #9595FF; } | ||
.highlight span.kn { color: #00C0D1; } | ||
.highlight span.cp { color: #AEE674; } | ||
.highlight span.caret { color: #FFFFFF; } | ||
.highlight span.no { color: #AEE674; } | ||
.highlight span.s2 { color: #BBFB8D; } | ||
.highlight span.nb { color: #A7FDB2; } | ||
.highlight span.nc { color: #C2ABFF; } | ||
.highlight span.nd { color: #AEE674; } | ||
.highlight span.s { color: #BBFB8D; } | ||
.highlight span.nf { color: #AEE674; } | ||
.highlight span.nx { color: #AEE674; } | ||
.highlight span.kp { color: #00C0D1; } | ||
.highlight span.nt { color: #C2ABFF; } | ||
.highlight span.s1 { color: #BBFB8D; } | ||
.highlight span.bg { color: #040400; } | ||
.highlight span.kt { color: #00C0D1; } | ||
.highlight span.support_function { color: #81B864; } | ||
.highlight span.ow { color: #EBE1B4; } | ||
.highlight span.mf { color: #A1FF24; } | ||
.highlight span.bp { color: #9595FF; } | ||
.highlight span.fg { color: #FFFFFF; } | ||
.highlight span.c1 { color: #3379FF; } | ||
.highlight span.kc { color: #9595FF; } | ||
.highlight span.c { color: #3379FF; } | ||
.highlight span.sx { color: #BBFB8D; } | ||
.highlight span.kd { color: #00C0D1; } | ||
.highlight span.ss { color: #A1FF24; } | ||
.highlight span.sr { color: #BBFB8D; } | ||
.highlight span.mo { color: #A1FF24; } | ||
.highlight span.mi { color: #A1FF24; } | ||
.highlight span.mh { color: #A1FF24; } | ||
.highlight span.o { color: #EBE1B4; } | ||
.highlight span.si { color: #DA96A3; } | ||
.highlight span.sh { color: #BBFB8D; } | ||
.highlight span.na { color: #AEE674; } | ||
.highlight span.sc { color: #BBFB8D; } | ||
.highlight span.k { color: #00C0D1; } | ||
.highlight span.se { color: #DA96A3; } | ||
.highlight span.sd { color: #54F79C; } |
Binary file not shown.
Oops, something went wrong.