Permalink
Browse files

Initial checkin

  • Loading branch information...
0 parents commit c2c294fc033a5fd92f4efdfd8b58c3bbd5c3e7a9 @coleifer committed Sep 16, 2011
Showing with 2,758 additions and 0 deletions.
  1. +19 −0 LICENSE
  2. +3 −0 MANIFEST.in
  3. +19 −0 README.rst
  4. +267 −0 example/app.py
  5. +9 −0 example/config.py
  6. +5 −0 example/requirements.txt
  7. +2 −0 example/run_example.sh
  8. +16 −0 example/static/style.css
  9. +12 −0 example/templates/admin/notes.html
  10. +6 −0 example/templates/admin/user_stats.html
  11. +21 −0 example/templates/base.html
  12. +13 −0 example/templates/create.html
  13. +7 −0 example/templates/homepage.html
  14. +2 −0 example/templates/includes/message.html
  15. +6 −0 example/templates/includes/pagination.html
  16. +19 −0 example/templates/join.html
  17. +12 −0 example/templates/private_messages.html
  18. +12 −0 example/templates/public_messages.html
  19. +27 −0 example/templates/user_detail.html
  20. +12 −0 example/templates/user_followers.html
  21. +12 −0 example/templates/user_following.html
  22. +12 −0 example/templates/user_list.html
  23. +1 −0 flaskext/__init__.py
  24. +364 −0 flaskext/admin.py
  25. +184 −0 flaskext/auth.py
  26. +54 −0 flaskext/db.py
  27. +2 −0 flaskext/exceptions.py
  28. +299 −0 flaskext/rest.py
  29. +93 −0 flaskext/serializer.py
  30. +491 −0 flaskext/static/css/admin.css
  31. BIN flaskext/static/img/icon-no.gif
  32. BIN flaskext/static/img/icon-yes.gif
  33. BIN flaskext/static/img/icon_addlink.gif
  34. BIN flaskext/static/img/icon_changelink.gif
  35. BIN flaskext/static/img/icon_deletelink.gif
  36. +69 −0 flaskext/static/js/admin.js
  37. +110 −0 flaskext/static/js/events.js
  38. +154 −0 flaskext/static/js/jquery.min.js
  39. +28 −0 flaskext/static/js/modernizr-1.5.min.js
  40. +82 −0 flaskext/templates/admin/base.html
  41. +8 −0 flaskext/templates/admin/includes/pagination.html
  42. +28 −0 flaskext/templates/admin/index.html
  43. +21 −0 flaskext/templates/admin/models/add.html
  44. +5 −0 flaskext/templates/admin/models/base.html
  45. +14 −0 flaskext/templates/admin/models/delete.html
  46. +21 −0 flaskext/templates/admin/models/edit.html
  47. +102 −0 flaskext/templates/admin/models/index.html
  48. +6 −0 flaskext/templates/admin/panels/base.html
  49. +7 −0 flaskext/templates/admin/panels/default.html
  50. +15 −0 flaskext/templates/auth/login.html
  51. +11 −0 flaskext/templates/macros/forms.html
  52. +48 −0 flaskext/utils.py
  53. +28 −0 setup.py
19 LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2011 Charles Leifer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
@@ -0,0 +1,3 @@
+include LICENSE
+include README.rst
+recursive-include example *
@@ -0,0 +1,19 @@
+flask-peewee
+============
+
+provides a layer of integration between the `flask <http://flask.pocoo.org/>`_
+web framework and the `peewee orm <http://charlesleifer.com/docs/peewee/>`_.
+
+batteries included:
+
+* admin interface
+* authentication
+* rest api
+
+requirements:
+
+* `flask <https://github.com/mitsuhiko/flask>`_
+* `peewee <https://github.com/coleifer/peewee>`_
+* `wtforms <https://bitbucket.org/simplecodes/wtforms>`_
+* `wtforms-peewee <https://github.com/coleifer/wtf-peewee>`_
+* python 2.5 or greater
@@ -0,0 +1,267 @@
+import datetime
+
+from flask import Flask, request, redirect, url_for, render_template, flash
+from hashlib import md5, sha1
+
+from peewee import *
+
+# flask-peewee bindings
+from flaskext.admin import Admin, ModelAdmin, AdminPanel
+from flaskext.auth import Auth
+from flaskext.db import Peewee
+from flaskext.rest import RestAPI, RestResource, UserAuthentication
+from flaskext.utils import get_object_or_404, object_list
+
+
+app = Flask(__name__)
+app.config.from_object('config.Configuration')
+
+db = Peewee(app)
+
+# model definitions
+class User(db.Model):
+ username = CharField()
+ password = CharField()
+ email = CharField()
+ join_date = DateTimeField(default=datetime.datetime.now)
+ active = BooleanField(default=True)
+ admin = BooleanField(default=False)
+
+ def __unicode__(self):
+ return self.username
+
+ def set_password(self, password):
+ self.password = sha1(password).hexdigest()
+
+ def following(self):
+ return User.select().join(
+ Relationship, on='to_user_id'
+ ).where(from_user=self).order_by('username')
+
+ def followers(self):
+ return User.select().join(
+ Relationship
+ ).where(to_user=self).order_by('username')
+
+ def is_following(self, user):
+ return Relationship.filter(
+ from_user=self,
+ to_user=user
+ ).exists()
+
+ def gravatar_url(self, size=80):
+ return 'http://www.gravatar.com/avatar/%s?d=identicon&s=%d' % \
+ (md5(self.email.strip().lower().encode('utf-8')).hexdigest(), size)
+
+
+class Relationship(db.Model):
+ from_user = ForeignKeyField(User, related_name='relationships')
+ to_user = ForeignKeyField(User, related_name='related_to')
+
+ def __unicode__(self):
+ return 'Relationship from %s to %s' % (self.from_user, self.to_user)
+
+
+class Message(db.Model):
+ user = ForeignKeyField(User)
+ content = TextField()
+ pub_date = DateTimeField(default=datetime.datetime.now)
+
+ def __unicode__(self):
+ return '%s: %s' % (self.user, self.content)
+
+
+class Note(db.Model):
+ user = ForeignKeyField(User)
+ message = TextField()
+ created_date = DateTimeField(default=datetime.datetime.now)
+
+class NotePanel(AdminPanel):
+ template_name = 'admin/notes.html'
+
+ def get_urls(self):
+ return (
+ ('/create/', self.create),
+ )
+
+ def create(self):
+ if request.method == 'POST':
+ if request.form.get('message'):
+ Note.create(
+ user=auth.get_logged_in_user(),
+ message=request.form['message'],
+ )
+ next = request.form.get('next') or self.dashboard_url()
+ return redirect(next)
+
+ def get_context(self):
+ return {
+ 'note_list': Note.select().order_by(('created_date', 'desc')).paginate(1, 3)
+ }
+
+class UserStatsPanel(AdminPanel):
+ template_name = 'admin/user_stats.html'
+
+ def get_context(self):
+ last_week = datetime.datetime.now() - datetime.timedelta(days=7)
+ signups_this_week = User.filter(join_date__gt=last_week).count()
+ messages_this_week = Message.filter(pub_date__gt=last_week).count()
+ return {
+ 'signups': signups_this_week,
+ 'messages': messages_this_week,
+ }
+
+
+auth = Auth(app, db, user_model=User)
+admin = Admin(app, db, auth)
+
+
+class MessageAdmin(ModelAdmin):
+ columns = ('user', 'content', 'pub_date',)
+
+class NoteAdmin(ModelAdmin):
+ columns = ('user', 'message', 'created_date',)
+
+
+auth.register_admin(admin)
+admin.register(Relationship)
+admin.register(Message, MessageAdmin)
+admin.register(Note, NoteAdmin)
+admin.register_panel('Notes', NotePanel)
+admin.register_panel('User stats', UserStatsPanel)
+
+# rest api stuff
+rest_auth = UserAuthentication(auth)
+api = RestAPI(app, default_auth=rest_auth)
+
+class UserResource(RestResource):
+ exclude = ('password',)
+
+api.register(Message)
+api.register(Relationship)
+api.register(User, UserResource)
+
+# utils
+def create_tables():
+ User.create_table()
+ Relationship.create_table()
+ Message.create_table()
+ Note.create_table()
+
+
+# custom filters
+@app.template_filter('is_following')
+def is_following(from_user, to_user):
+ return from_user.is_following(to_user)
+
+
+# views
+@app.route('/')
+def homepage():
+ if auth.get_logged_in_user():
+ return private_timeline()
+ else:
+ return public_timeline()
+
+@app.route('/private/')
+@auth.login_required
+def private_timeline():
+ user = auth.get_logged_in_user()
+
+ messages = Message.select().where(
+ user__in=user.following()
+ ).order_by(('pub_date', 'desc'))
+
+ return object_list('private_messages.html', messages, 'message_list')
+
+@app.route('/public/')
+def public_timeline():
+ messages = Message.select().order_by(('pub_date', 'desc'))
+ return object_list('public_messages.html', messages, 'message_list')
+
+@app.route('/join/', methods=['GET', 'POST'])
+def join():
+ if request.method == 'POST' and request.form['username']:
+ try:
+ user = User.get(username=request.form['username'])
+ flash('That username is already taken')
+ except User.DoesNotExist:
+ user = User(
+ username=request.form['username'],
+ email=request.form['email'],
+ join_date=datetime.datetime.now()
+ )
+ user.set_password(request.form['password'])
+ user.save()
+
+ auth.login_user(user)
+ return redirect(url_for('homepage'))
+
+ return render_template('join.html')
+
+@app.route('/following/')
+@auth.login_required
+def following():
+ user = auth.get_logged_in_user()
+ return object_list('user_following.html', user.following(), 'user_list')
+
+@app.route('/followers/')
+@auth.login_required
+def followers():
+ user = auth.get_logged_in_user()
+ return object_list('user_followers.html', user.followers(), 'user_list')
+
+@app.route('/users/')
+def user_list():
+ users = User.select().order_by('username')
+ return object_list('user_list.html', users, 'user_list')
+
+@app.route('/users/<username>/')
+def user_detail(username):
+ user = get_object_or_404(User, username=username)
+ messages = user.message_set.order_by(('pub_date', 'desc'))
+ return object_list('user_detail.html', messages, 'message_list', person=user)
+
+@app.route('/users/<username>/follow/', methods=['POST'])
+@auth.login_required
+def user_follow(username):
+ user = get_object_or_404(User, username=username)
+ Relationship.get_or_create(
+ from_user=auth.get_logged_in_user(),
+ to_user=user,
+ )
+ flash('You are now following %s' % user.username)
+ return redirect(url_for('user_detail', username=user.username))
+
+@app.route('/users/<username>/unfollow/', methods=['POST'])
+@auth.login_required
+def user_unfollow(username):
+ user = get_object_or_404(User, username=username)
+ Relationship.delete().where(
+ from_user=auth.get_logged_in_user(),
+ to_user=user,
+ ).execute()
+ flash('You are no longer following %s' % user.username)
+ return redirect(url_for('user_detail', username=user.username))
+
+@app.route('/create/', methods=['GET', 'POST'])
+@auth.login_required
+def create():
+ user = auth.get_logged_in_user()
+ if request.method == 'POST' and request.form['content']:
+ message = Message.create(
+ user=user,
+ content=request.form['content'],
+ )
+ flash('Your message has been created')
+ return redirect(url_for('user_detail', username=user.username))
+
+ return render_template('create.html')
+
+admin.setup()
+api.setup()
+
+
+# allow running from the command line
+if __name__ == '__main__':
+ app.run()
@@ -0,0 +1,9 @@
+# config
+
+class Configuration(object):
+ DATABASE = {
+ 'name': 'example.db',
+ 'engine': 'peewee.SqliteDatabase',
+ }
+ DEBUG = True
+ SECRET_KEY = 'shhhh'
@@ -0,0 +1,5 @@
+flask
+werkzeug
+jinja2
+gunicorn
+-e git+git@github.com:coleifer/peewee.git#egg=peewee
@@ -0,0 +1,2 @@
+#!/bin/sh
+python app.py
@@ -0,0 +1,16 @@
+body { font-family: sans-serif; background: #eee; }
+a, h1, h2 { color: #377BA8; }
+h1, h2 { font-family: 'Georgia', serif; margin: 0; }
+h1 { border-bottom: 2px solid #eee; }
+h2 { font-size: 1.2em; }
+
+.page { margin: 2em auto; width: 35em; border: 5px solid #ccc;
+ padding: 0.8em; background: white; }
+.page ul { list-style-type: none; }
+.page li { clear: both; }
+.metanav { text-align: right; font-size: 0.8em; padding: 0.3em;
+ margin-bottom: 1em; background: #fafafa; }
+.flash { background: #CEE5F5; padding: 0.5em;
+ border: 1px solid #AACBE2; }
+.avatar { display: block; float: left; margin: 0 10px 0 0; }
+.message-content { min-height: 80px; }
@@ -0,0 +1,12 @@
+{% extends "admin/panels/default.html" %}
+
+{% block panel_content %}
+ {% for note in note_list %}
+ <p>{{ note.user.username }}: {{ note.message }}</p>
+ {% endfor %}
+ <form method="post" action="{{ url_for(panel.get_url_name('create')) }}">
+ <input type="hidden" value="{{ request.url }}" />
+ <p><textarea name="message"></textarea></p>
+ <p><button type="submit" class="small">Save</button></p>
+ </form>
+{% endblock %}
@@ -0,0 +1,6 @@
+{% extends "admin/panels/default.html" %}
+
+{% block panel_content %}
+ <p>New users this week: {{ signups }}</p>
+ <p>Messages this week: {{ messages }}</p>
+{% endblock %}
@@ -0,0 +1,21 @@
+<!doctype html>
+<title>Tweepee</title>
+<link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css') }}">
+<div class=page>
+ <h1><a href="{{ url_for('homepage') }}">Tweepee</a></h1>
+ <div class=metanav>
+ {% if not session.logged_in %}
+ <a href="{{ url_for('auth.login') }}">log in</a>
+ <a href="{{ url_for('join') }}">join</a>
+ {% else %}
+ <a href="{{ url_for('public_timeline') }}">public timeline</a>
+ <a href="{{ url_for('create') }}">create</a>
+ <a href="{{ url_for('auth.logout') }}">log out</a>
+ {% endif %}
+ </div>
+ {% for message in get_flashed_messages() %}
+ <div class=flash>{{ message }}</div>
+ {% endfor %}
+ <h2>{% block content_title %}{% endblock %}</h2>
+ {% block content %}{% endblock %}
+</div>
Oops, something went wrong.

0 comments on commit c2c294f

Please sign in to comment.