Adds models and various dependencies; first pass on twitter oAuth support, login.
authordsc <david.schoonover@gmail.com>
Sat, 24 Dec 2011 14:40:54 +0000 (06:40 -0800)
committerdsc <david.schoonover@gmail.com>
Sat, 24 Dec 2011 14:40:54 +0000 (06:40 -0800)
README.md
crisishaiku/__init__.py
crisishaiku/cli/__init__.py [deleted file]
crisishaiku/cli/pathtype.py [deleted file]
crisishaiku/models.py [new file with mode: 0644]
crisishaiku/routes.py [new file with mode: 0644]
etc/flask.cfg
setup.py
static/img/fcic-cover.jpg [moved from public/img/fcic-cover.jpg with 100% similarity]

index f7886d2..7fb2c46 100644 (file)
--- 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.
+
+
index 6fb6235..507b9f5 100644 (file)
@@ -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 (file)
index 3a1213c..0000000
+++ /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 (file)
index 9ff03e5..0000000
+++ /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 (file)
index 0000000..1e1d56b
--- /dev/null
@@ -0,0 +1,127 @@
+#!/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'))
+# )
+# usertags = db.Table('tags',
+#     Column('user_id',   Integer, ForeignKey('user.id')),
+#     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='haikus')
+    start     = Column(Integer)
+    line_no   = Column(Integer)
+
+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'))
+
+class UserTag(db.Model):
+    
+
+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())
+    
+    banned    = Column(Boolean)
+    verified  = Column(Boolean)
+    
+    # OAuth & OpenID accounts
+    accounts = Column(JSONData())
+    
+    # We can store the verification crap elsewhere
+    # verification_token = Column(String(60))
+    
+    # tags = relationship('UserTag')
+    
+    
+    
+    def __init__(self, username, email):
+        self.username = username
+        self.email = email
+    
+    def __repr__(self):
+        return '<User %r, %r>' % (self.username, self.email)
+
diff --git a/crisishaiku/routes.py b/crisishaiku/routes.py
new file mode 100644 (file)
index 0000000..8886645
--- /dev/null
@@ -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)
+
index 610212a..721af2c 100644 (file)
@@ -1,2 +1,4 @@
 SQLALCHEMY_DATABASE_URI = 'sqlite:///var/haikus.db'
+TWITTER_CONSUMER_KEY    = 'tQww8eCQbbtF12hHPTJA',
+TWITTER_CONSUMER_SECRET = 'OE9AIRTkxWTBJU88PJjqGI4soC6k0pUOtrRur0Q8d4w',
 
index 50bf0c1..6be6077 100644 (file)
--- 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',
Simple merge