Skip to content

Commit

Permalink
Adding blog example app.
Browse files Browse the repository at this point in the history
  • Loading branch information
coleifer committed Mar 11, 2015
1 parent 0e6dd57 commit 85abe94
Show file tree
Hide file tree
Showing 19 changed files with 787 additions and 0 deletions.
265 changes: 265 additions & 0 deletions examples/blog/app.py
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()
6 changes: 6 additions & 0 deletions examples/blog/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
flask
BeautifulSoup
micawber
pygments
markdown
peewee
1 change: 1 addition & 0 deletions examples/blog/static/css/blog.min.css

Large diffs are not rendered by default.

47 changes: 47 additions & 0 deletions examples/blog/static/css/hilite.css
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.
Loading

0 comments on commit 85abe94

Please sign in to comment.