From: dsc Date: Sat, 24 Dec 2011 14:40:54 +0000 (-0800) Subject: Adds models and various dependencies; first pass on twitter oAuth support, login. X-Git-Url: http://git.less.ly:3516/?a=commitdiff_plain;h=86f8365867ace6c45fc2c1896aae16d831d23556;p=crisishaiku.git Adds models and various dependencies; first pass on twitter oAuth support, login. --- diff --git a/README.md b/README.md index f7886d2..7fb2c46 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ Finally, let's fix the missing dict (which I dug up from somewhere). cp etc/hyph_en_US.dic .env/lib/python2.7/site-packages/hyphen/ + ## Features ### Pages @@ -64,8 +65,29 @@ Finally, let's fix the missing dict (which I dug up from somewhere). - Zips of the haikus, report - Source on GitHub +### Models + +- User +- Haiku +- Like +- Text +- Comment + + +## Debugging Notes + +When running `ipython` under `virtualenv`, you can activate the environment from within the interpreter: + +````python +execfile('.env/bin/activate_this.py', dict(__file__='.env/bin/activate_this.py')) +```` + +Which resolves problems with the env's `site-packages` dir not getting picked up recursively. + ## Notes - Did you know that the Financial Crisis Inquiry Report increased the US Gross National Haiku Quotient by 1.8%, the largest single increase every affected by a congressional report? - Split out Haiku-finder into its own package? That'd be neat. + + diff --git a/crisishaiku/__init__.py b/crisishaiku/__init__.py index 6fb6235..507b9f5 100644 --- a/crisishaiku/__init__.py +++ b/crisishaiku/__init__.py @@ -11,19 +11,35 @@ from flask import (Flask, request, session, g, redirect, url_for, abort, render_template, flash, ) from flaskext.sqlalchemy import SQLAlchemy +from flaskext.bcrypt import Bcrypt +from flaskext.oauth import OAuth app = Flask('crisishaiku') ### Config -# Load from any ALL_CAPS variables in this file -app.config.from_object('crisishaiku') # Load from our base config file -app.config.from_pyfile('etc/flask.cfg') +app.config.from_pyfile('../etc/flask.cfg') # Load any overrides from the file specified in the env var FLASK_SETTINGS app.config.from_envvar('FLASK_SETTINGS', silent=True) db = SQLAlchemy(app) +crypt = Bcrypt(app) +# pw_hash = crypt.generate_password_hash('hunter2') +# crypt.check_password_hash(pw_hash, 'hunter2') # returns True +oauth = OAuth() +twitter = oauth.remote_app('twitter', + base_url = 'https://api.twitter.com/1/', + request_token_url = 'https://api.twitter.com/oauth/request_token', + access_token_url = 'https://api.twitter.com/oauth/access_token', + authorize_url = 'https://api.twitter.com/oauth/authenticate', # /autorize if we wanted to read/write + consumer_key = app.config['TWITTER_CONSUMER_KEY'], + consumer_secret = app.config['TWITTER_CONSUMER_SECRET'] +) + + +# import crisishaiku.models +# import crisishaiku.routes diff --git a/crisishaiku/cli/__init__.py b/crisishaiku/cli/__init__.py deleted file mode 100644 index 3a1213c..0000000 --- a/crisishaiku/cli/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - diff --git a/crisishaiku/cli/pathtype.py b/crisishaiku/cli/pathtype.py deleted file mode 100644 index 9ff03e5..0000000 --- a/crisishaiku/cli/pathtype.py +++ /dev/null @@ -1,148 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import sys, codecs, locale -import argparse -from path import path - - -__all__ = ('FileType', 'PathType', 'DirectoryType', 'PathTypeError',) - - -class PathTypeError(TypeError): - """ TypeError that provides `path` and `type` attributes tracking expectations. """ - - def __init__(self, message, filepath, pathtype): - super(PathTypeError, self).__init__(message, filepath, pathtype) - self.message = message - self.path = filepath - self.type = pathtype - - - -class FileType(argparse.FileType): - """Factory for creating file object types - - Instances of FileType are typically passed as type= arguments to the - ArgumentParser add_argument() method. - - Keyword Arguments: - - mode='r' -- A string indicating how the file is to be opened. Accepts the - same values as the builtin open() function. - - encoding=None -- The file's encoding. None is treated as per the `codecs` - module (as bytes). - - errors='strict' -- Error handling as defined in the `codecs` module: - 'strict', 'ignore', 'replace', 'xmlcharrefreplace', 'backslashreplace' - - bufsize=-1 -- The file's desired buffer size. Accepts the same values as - the builtin open() function. - """ - - def __init__(self, mode='r', encoding=None, errors='strict', bufsize=-1): - self._mode = mode - self._encoding = encoding - self._errors = errors - self._bufsize = bufsize - - def __call__(self, f): - mode = self._mode - enc = self._encoding - - # the special path "-" means sys.std{in,out} - if f == '-': - if 'r' in mode: - f = '/dev/stdin' - enc = enc or sys.stdin.encoding or locale.getpreferredencoding().lower() - elif 'w' in mode: - f = '/dev/stdout' - enc = enc or sys.stdout.encoding or locale.getpreferredencoding().lower() - else: - msg = _('argument "-" with mode %r') % mode - raise ValueError(msg) - - # all other paths are used as ... paths - try: - return codecs.open( f, mode=mode, encoding=enc or None, - errors=self._errors, buffering=self._bufsize ) - except IOError as e: - message = _("can't open '%s': %s") - raise ArgumentTypeError(message % (f, e)) - - def __repr__(self): - args = self._mode, self._encoding, self._errors, self._bufsize - args_str = ', '.join(repr(arg) for arg in args if arg != -1) - return '%s(%s)' % (type(self).__name__, args_str) - - - -class PathType(object): - """ Factory for validating a path and wrapping it as a `path`. - - Keyword Arguments: - - base=u'' -- Base path to resolve the passed path from. - - mustExist=False -- Validate directory exists, raising OSError otherwise. - - expand=True -- Expand the path. - - abspath=False -- Resolve the absolute path. - """ - base = u'' - mustExist = True - expand = True - abspath = False - - - def __init__(self, base=u'', mustExist=True, expand=True, abspath=False): - self.base = path(base) - self.mustExist = mustExist - self.expand = expand - self.abspath = abspath - - - def checkExists(self, p): - if self.mustExist and not p.exists(): - raise OSError(2, 'No such file or directory', p) - return p - - def __call__(self, p): - p = self.base/p - if self.expand: - p = p.expand() - if self.abspath(): - p = p.abspath() - return self.checkExists(p) - - - def __repr__(self): - return "%s(%s)" % ( type(self).__name__, - ', '.join( '%s=%r' % (k,v) for k,v in self.__dict__.items() if not k[0] == '_' ) ) - - - -class DirectoryType(PathType): - """ Factory for validating a directory path and wrapping it as a `path`. - """ - mkdirs = True - - - def __init__(self, base=u'', mkdirs=True, mustExist=False, expand=True, abspath=False): - """ Factory for validating a directory path and wrapping it as a `path`. If a given - path is not a directory, TypeError is raised. - - Keyword Arguments: - - base=u'' -- Base path to resolve the passed path from. - - mkdirs=True -- If directory does not exist, make it and all intermediary - directories. - - mustExist=False -- Validate directory exists, raising OSError otherwise. - - expand=True -- Expand the path. - - abspath=False -- Resolve the absolute path. - """ - super(DirectoryType, self).__init__(base, mustExist, expand, abspath) - self.mkdirs = mkdirs - - - def checkExists(self, p): - if self.mkdirs and not p.exists(): - p.makedirs() - if p.exists() and not p.isdir(): - raise PathTypeError('Path is not a directory', p, self) - return super(PathType, self).checkExists(p) - - - diff --git a/crisishaiku/models.py b/crisishaiku/models.py new file mode 100644 index 0000000..8080b22 --- /dev/null +++ b/crisishaiku/models.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from sqlalchemy import ( + Table, Column, ForeignKey, + Boolean, Enum, Binary, PickleType, + Integer, SmallInteger, BigInteger, Float, + String, Text, Unicode, UnicodeText, + DateTime, Date, Time, + func, ) +from sqlalchemy.orm import relationship, backref, aliased +from sqlalchemy.types import TypeDecorator, VARCHAR +from sqlalchemy.ext.associationproxy import association_proxy +import anyjson as json + +from crisishaiku import db + + + +class JSONData(TypeDecorator): + "Type representing serialized JSON data." + impl = VARCHAR + + def process_bind_param(self, value, dialect): + if value is not None: + value = json.dumps(value) + return value + + def process_result_value(self, value, dialect): + if value is not None: + value = json.loads(value) + return value + +class TimestampMixin(object): + ctime = Column(DateTime, default=func.now()) + mtime = Column(DateTime, default=func.now()) + + +# join tables + +# likes = db.Table('likes', +# Column('user_id', Integer, ForeignKey('user.id')), +# Column('target_id', Integer, ForeignKey('target.id')), +# Column('ctime', DateTime, default=func.now()), +# ) + +# tags2haikus = db.Table('tags', +# Column('tag_id', Integer, ForeignKey('tag.id')), +# Column('haiku_id', Integer, ForeignKey('haiku.id')) +# ) + + +class Target(db.Model): + "A polymorphic pointer to a Comment, Haiku, or Section." + id = Column(Integer, primary_key=True) + +class Section(db.Model): + "A chunk of the Report. May represent a chapter/section node, or a paragraph of text." + id = Column(Integer, primary_key=True) + title = Column(String(200)) + text = Column(Text()) + parent = relationship('Section', backref=backref('children', lazy='dynamic')) + +class Haiku(db.Model): + id = Column(Integer, primary_key=True) + text = Column(Text()) + context = relationship('Section', backref=backref('haikus' lazy='dynamic')) + start = Column(Integer) # character index into the section where this haiku begins + +class Like(db.Model): + id = Column(Integer, primary_key=True) + user = relationship('User', backref=backref('likes', lazy='dynamic')) + target = relationship('Target') + ctime = Column(DateTime, default=func.now()) + +class Comment(TimestampMixin, db.Model): + id = Column(Integer, primary_key=True) + target = relationship('Target') + author = relationship('User', backref=backref('comments', lazy='dynamic')) + text = Column(Text()) + approved = Column(Boolean) # spam filter approval + deleted = Column(Boolean) + +class Tag(db.Model): + id = Column(Integer, primary_key=True) + name = Column(String(120), unique=True) + haikus = relationship('Haiku', secondary=tags2haikus, backref=backref('tags', lazy='dynamic')) + +# user_tags = db.Table('user_tags', +# Column('user_id', Integer, ForeignKey('user.id')), +# Column('tag_id', Integer, ForeignKey('tag.id')), +# Column('haiku_id', Integer, ForeignKey('haiku.id')), +# ) + +class User(TimestampMixin, db.Model): + id = Column(Integer, primary_key=True) + username = Column(String(30), unique=True) + email = Column(String(120), unique=True) + pwhash = Column(String(60)) + + last_seen = Column(DateTime, default=func.now()) + verified = Column(Boolean) + banned = Column(Boolean) + deleted = Column(Boolean) + + # OAuth & OpenID accounts + accounts = Column(JSONData()) + + # We can store the verification crap elsewhere + # verification_token = Column(String(60)) + + # comments + # likes + # tags = relationship('UserTag') + + + def __init__(self, username, email): + self.username = username + self.email = email + + def __repr__(self): + return '' % (self.username, self.email) + diff --git a/crisishaiku/routes.py b/crisishaiku/routes.py new file mode 100644 index 0000000..8886645 --- /dev/null +++ b/crisishaiku/routes.py @@ -0,0 +1,37 @@ +from flask import ( + request, session, url_for, redirect, abort, + render_template, g, flash, send_from_directory, ) +from crisishaiku import app, db, crypt, twitter, models +from path import path + +STATIC = path(app.root_path)/'static' + + +@app.route('/favicon.ico') +def favicon(): + return send_from_directory(STATIC, 'favicon.ico', mimetype='image/vnd.microsoft.icon') + + +@app.route('/login') +def login(): + return twitter.authorize(callback=url_for('oauth_authorized', + next=request.args.get('next') or request.referrer or None)) + + +@app.route('/oauth-authorized') +@twitter.authorized_handler +def oauth_authorized(res): + next_url = request.args.get('next') or url_for('index') + if res is None: + flash(u'You denied the request to sign in.') + return redirect(next_url) + + session['twitter_token'] = ( + res['oauth_token'], + res['oauth_token_secret'] + ) + session['twitter_user'] = res['screen_name'] + + flash('You were signed in as %s' % res['screen_name']) + return redirect(next_url) + diff --git a/etc/flask.cfg b/etc/flask.cfg index 610212a..721af2c 100644 --- a/etc/flask.cfg +++ b/etc/flask.cfg @@ -1,2 +1,4 @@ SQLALCHEMY_DATABASE_URI = 'sqlite:///var/haikus.db' +TWITTER_CONSUMER_KEY = 'tQww8eCQbbtF12hHPTJA', +TWITTER_CONSUMER_SECRET = 'OE9AIRTkxWTBJU88PJjqGI4soC6k0pUOtrRur0Q8d4w', diff --git a/setup.py b/setup.py index 50bf0c1..6be6077 100644 --- a/setup.py +++ b/setup.py @@ -30,13 +30,20 @@ setup( install_requires = [ # 'PyHyphen >= 1.0beta1', + 'path >= 2.2', 'bunch >= 1.0', 'jsonlib2 >= 1.5.2', 'anyjson >= 0.3.1', 'PyYAML >= 3.10', + 'py-bcrypt >= 0.2', + 'SQLAlchemy >= 0.7.4', 'Flask >= 0.8', 'Flask-SQLAlchemy >= 0.15', + 'Flask-Bcrypt >= 0.5.2', + 'Flask-Admin >= 0.3.0', + 'Flask-OAuth >= 0.11', + 'Flask-Openid >= 1.0.1', 'pyjade >= 0.6', 'pystache >= 0.3.1',