A website dedicated to the poetry of financial disaster~
-## Install
+## Installation
+
+### Install & Setup
First up, `virtualenv`:
pip install -e .
+Now that we've got all the deps, we'll want to create the database.
+
+ >>> from crisishaiku import app, db, User
+ >>> db.create_all()
+
+You'll see it blorp out some SQL. Let's make sure it worked.
+
+ >>> u = User('leigh', 'trouble@crisishaiku.com')
+ >>> db.session.add(u)
+ >>> db.session.commit()
+
+Yet more SQL, and hopefully no error message. Hooray!
+
+Finally, run the server:
+
+ bin/server
+
+And point your browser at http://localhost:5000/ -- you should see the request
+blow by. Hello, world!
+
+Oh, and finally: as a convenience, `bin/server` will attempt to activate the
+virtualenv if one isn't already active (as obviously I find that obnoxious
+and always forget).
+
-## Dev Install
+### Haiku Finder Install
**OPTIONAL**
DATA_DIR = path('data')
STATE_FILE = 'state.json'
SYLLABLE_FILE = 'syllables.json'
-REPORT_FILE = DATA_DIR/'report/fcir.txt'
+REPORT_FILE = DATA_DIR/'report/docs/fcir.txt'
OUT_DIR = DATA_DIR/'haikus'
OUTFILE_OVERLAP = 'haikus.txt'
-#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" crisishaiku.com -- a website dedicated to the poetry of financial disaster
"""
VERSION = tuple(map(int, __version__.split('.')))
+import os
+from path import path
from flask import (Flask,
request, session, g, redirect, url_for,
abort, render_template, flash, )
from flaskext.bcrypt import Bcrypt
from flaskext.oauth import OAuth
+HERE = path(__file__).dirname().dirname().abspath()
-app = Flask('crisishaiku')
-### Config
-# Load from our base config file
-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)
-# render .jade files using pyjade
+app = Flask('crisishaiku',
+ instance_relative_config=True, instance_path=HERE/'var')
+
+
+# Load Config
+mode = os.environ.get('CRISISHAIKU_MODE', 'dev').lower()
+config_obj = 'DevelopmentConfig'
+if mode in ('prod', 'production'):
+ config_obj = 'ProductionConfig'
+elif mode in ('test', 'testing', 'unittest'):
+ config_obj = 'UnitTestConfig'
+app.config.from_object('crisishaiku.config.'+config_obj)
+
+# Load any overrides from the file specified in the env var CRISISHAIKU_SETTINGS
+app.config.from_envvar('CRISISHAIKU_SETTINGS', silent=True)
+
+# Render .jade files using pyjade
app.jinja_env.add_extension('pyjade.ext.jinja.PyJadeExtension')
+
db = SQLAlchemy(app)
crypt = Bcrypt(app)
-
oauth = OAuth()
twitter = oauth.remote_app('twitter',
base_url = 'https://api.twitter.com/1/',
)
-import crisishaiku.models
-import crisishaiku.views
+import crisishaiku.model
+from crisishaiku.model import *
+import crisishaiku.view
--- /dev/null
+# -*- coding: utf-8 -*-
+
+class Config(object):
+ MODE = None
+ DEBUG = False
+ TESTING = False
+ TWITTER_CONSUMER_KEY = 'tQww8eCQbbtF12hHPTJA'
+ TWITTER_CONSUMER_SECRET = 'OE9AIRTkxWTBJU88PJjqGI4soC6k0pUOtrRur0Q8d4w'
+ SQLALCHEMY_ECHO = False
+ SQLALCHEMY_DATABASE_URI = 'sqlite://:memory:'
+
+
+class UnitTestConfig(Config):
+ MODE = 'unittest'
+ TESTING = True
+
+
+class DevelopmentConfig(Config):
+ MODE = 'development'
+ DEBUG = True
+ SQLALCHEMY_ECHO = True
+ SQLALCHEMY_DATABASE_URI = 'sqlite:///../var/haiku.db'
+
+
+class ProductionConfig(Config):
+ MODE = 'prod'
+ SQLALCHEMY_DATABASE_URI = 'mysql://crisishaiku@localhost/crisishaiku'
+
-#!/usr/bin/env python
# -*- coding: utf-8 -*-
+__all__ = ('Section', 'Haiku', 'User',)
+# __all__ = ('Section', 'Haiku', 'User', 'Target', 'Comment', 'Like', 'Tag',)
-# __all__
from sqlalchemy import (
Table, Column, ForeignKey,
from sqlalchemy.orm import relationship, backref, aliased
import anyjson as json
-from crisishaiku import db
-
-# pw_hash = crypt.generate_password_hash('hunter2')
-# crypt.check_password_hash(pw_hash, 'hunter2') # returns True
+from crisishaiku import db, crypt
### Helpers
value = json.loads(value)
return value
-class TimestampMixin(object):
- ctime = Column(DateTime, default=func.now())
- mtime = Column(DateTime, default=func.now())
### Model Classes
-class User(TimestampMixin, db.Model):
+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_id = Column(Integer, ForeignKey('section.id'))
+ children = relationship('Section', lazy='dynamic', backref=backref('parent', lazy='dynamic', remote_side=[id]))
+ haikus = relationship('Haiku', lazy='dynamic', backref=backref('context', lazy='dynamic'))
+
+
+class Haiku(db.Model):
+ id = Column(Integer, primary_key=True)
+ text = Column(Text())
+ start = Column(Integer) # character index into the section where this haiku begins
+ context_id = Column(Integer, ForeignKey('section.id'))
+
+
+class User(db.Model):
id = Column(Integer, primary_key=True)
username = Column(String(30), unique=True)
email = Column(String(120), unique=True)
pwhash = Column(String(60))
+ ctime = Column(DateTime, default=func.now())
+ mtime = Column(DateTime, default=func.now())
last_seen = Column(DateTime, default=func.now())
+
verified = Column(Boolean, default=False)
banned = Column(Boolean, default=False)
deleted = Column(Boolean, default=False)
accounts = Column(JSONData())
# likes = relationship('Like', lazy='dynamic', backref=backref('user', lazy='dynamic'))
- comments = relationship('Comment', lazy='dynamic', backref=backref('author', lazy='dynamic'))
+ # comments = relationship('Comment', lazy='dynamic', backref=backref('author', lazy='dynamic'))
# per-user tag tracking disabled for now
## tags = relationship('UserTag', backref=backref('user', lazy='dynamic'))
self.username = username
self.email = email
+ @property
+ def password(self):
+ return self.pwhash
+
+ @password.setter
+ def password(self, pw):
+ self.pwhash = crypt.generate_password_hash(pw)
+
+ def checkPassword(self, pw):
+ return crypt.check_password_hash(self.pwhash, pw)
+
+
+ ### OAuth
+
+ def addAccount(self, type, data):
+ "Associate an account (after authentication & authorization has taken place)."
+ accounts = self.accounts or {}
+ accounts[type] = data
+ self.accounts = accounts.copy() # copy to ensure SQLAlchemy picks up the change
+
+ def removeAccount(self, type):
+ "Removes an associated account."
+ accounts = self.accounts
+ if not accounts or type not in accounts:
+ return
+ del accounts[type]
+ self.accounts = accounts.copy() # copy to ensure SQLAlchemy picks up the change
+
+
def __repr__(self):
return '<User %r, %r>' % (self.username, self.email)
-# 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_id = Column(Integer, ForeignKey('section.id'))
-# children = relationship('Section', lazy='dynamic', backref=backref('parent', lazy='dynamic'))
-# haikus = relationship('Haiku', lazy='dynamic', backref=backref('context', lazy='dynamic'))
-
-
-# class Haiku(db.Model):
-# id = Column(Integer, primary_key=True)
-# text = Column(Text())
-# start = Column(Integer) # character index into the section where this haiku begins
-# context_id = Column(Integer, ForeignKey('section.id'))
-
-
-class Comment(TimestampMixin, db.Model):
- id = Column(Integer, primary_key=True)
- # target = relationship('Target', lazy='dynamic')
- author_id = Column(Integer, ForeignKey('user.id'))
- text = Column(Text())
- approved = Column(Boolean, default=False) # spam filter approval
- deleted = Column(Boolean, default=False)
-
-
-
-
# class Target(db.Model):
# "A polymorphic pointer to a Comment, Haiku, or Section."
# id = Column(Integer, primary_key=True)
# return self.section or self.haiku or self.comment
+# class Comment(db.Model):
+# id = Column(Integer, primary_key=True)
+# # target = relationship('Target', lazy='dynamic')
+# author_id = Column(Integer, ForeignKey('user.id'))
+# text = Column(Text())
+# ctime = Column(DateTime, default=func.now())
+# mtime = Column(DateTime, default=func.now())
+# approved = Column(Boolean, default=False) # spam filter approval
+# deleted = Column(Boolean, default=False)
+
+
# likes = db.Table('likes',
# Column('user_id', Integer, ForeignKey('user.id')),
# Column('target_id', Integer, ForeignKey('target.id')),
--- /dev/null
+html, body, div, span, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+abbr, address, cite, code, del, dfn, em, img, ins, kbd, q, samp,
+small, strong, sub, sup, var, b, i, dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, figcaption, figure,
+footer, header, hgroup, menu, nav, section, summary,
+time, mark, audio, video {
+ margin:0; padding:0;
+ border: 0;
+ font-size: 100%;
+ font: inherit;
+ vertical-align: baseline; }
+
+article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section {
+ display: block; }
+
+html { color:#000; background:#fff; width:100%; height:100%; overflow-y: scroll; }
+body { position:absolute; width:100%; top:0; left:0; }
+
+address,caption,cite,code,dfn,em,strong,th,var,optgroup { font-style:inherit; font-weight:inherit; }
+fieldset,img { border:0; }
+caption,th { text-align:left; }
+caption { margin-bottom:.5em; text-align:center; }
+
+h1,h2,h3,h4,h5,h6 { font-size:100%; font-weight:normal; }
+h1,h2,h3,h4,h5,h6,strong { font-weight:bold; }
+h1 { font-size:250%; }
+h2 { font-size:150%; }
+h3 { font-size:125%; }
+h4 { font-size:110%; }
+h1,h2,h3 { margin:0.5em 0 1em; }
+
+
+ins { background-color: #ff9; color: #000; text-decoration: none; }
+mark { background-color: #ff9; color: #000; font-style: italic; font-weight: bold; }
+del { text-decoration: line-through; }
+abbr[title], dfn[title] { border-bottom: 1px dotted; cursor: help; }
+abbr,acronym { border:0; font-variant:normal; border-bottom:1px dotted #000; cursor:help; }
+em, i { font-style:italic; }
+small { font-size: 85%; }
+strong, b, th { font-weight: bold; }
+u { text-decoration:underline; }
+
+/* Set sub, sup without affecting line-height: gist.github.com/413930 */
+sub, sup { font-size: 75%; line-height: 0; position: relative; }
+sup { top: -0.5em; }
+sub { bottom: -0.25em; }
+
+
+blockquote, q { quotes: none; }
+blockquote:before, blockquote:after,
+q:before, q:after { content: ''; content: none; }
+hr { display: block; height: 1px; border: 0; border-top: 1px solid #ccc; margin: 1em 0; padding: 0; }
+
+
+/** Font normalization inspired by YUI Library's fonts.css: developer.yahoo.com/yui/ */
+//body { font:13px/1.231 sans-serif; *font-size:small; } /* Hack retained to preserve specificity */
+body { font:16px/1.231 sans-serif; *font-size:small; } /* Hack retained to preserve specificity */
+select, input, textarea, button { font:99% sans-serif; }
+
+/* Normalize monospace sizing: en.wikipedia.org/wiki/MediaWiki_talk:Common.css/Archive_11#Teletype_style_fix_for_Chrome */
+pre, code, kbd, samp { font-family: monospace, sans-serif; }
+/* www.pathf.com/blogs/2008/05/formatting-quoted-code-in-blog-posts-css21-white-space-pre-wrap/ */
+pre {
+ white-space: pre;
+ white-space: pre-wrap; /* css-3 */
+ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
+ white-space: -pre-wrap; /* Opera 4-6 */
+ white-space: -o-pre-wrap; /* Opera 7 */
+
+ word-wrap: break-word;
+ padding: 15px; }
+
+p,fieldset,table,pre { margin-bottom:1em; }
+
+
+/* Accessible focus treatment: people.opera.com/patrickl/experiments/keyboard/test */
+a:hover, a:active { outline: none; }
+
+
+/* Lists */
+
+ul,ol,dl,blockquote { margin:0.5em; }
+ol,ul,dl { margin-left:1.0em; }
+li { list-style:none; }
+ol { list-style-type: decimal; }
+ol li { list-style:decimal inside; }
+ul li { list-style:disc inside; }
+dl dd { margin-left:0.5em; }
+
+/* Remove margins for navigation lists */
+nav ul, nav li { margin: 0; list-style:none; list-style-image: none; }
+
+
+/* Forms */
+
+input,select { vertical-align: middle; }
+input,button,textarea,select,optgroup,option { font-family:inherit; font-size:inherit; font-style:inherit; font-weight:inherit; }
+input,button,textarea,select { *font-size:100%; }
+input[type=text],input[type=password],textarea { width:12.25em; *width:11.9em; }
+textarea { overflow: auto; } /* www.sitepoint.com/blogs/2010/08/20/ie-remove-textarea-scrollbars/ */
+
+legend { color:#000; }
+.ie6 legend, .ie7 legend { margin-left: -7px; }
+
+/* Align checkboxes, radios, text inputs with their label by: Thierry Koblentz tjkdesign.com/ez-css/css/base.css */
+input[type="radio"] { vertical-align: text-bottom; }
+input[type="checkbox"] { vertical-align: bottom; }
+.ie7 input[type="checkbox"] { vertical-align: baseline; }
+.ie6 input { vertical-align: text-bottom; }
+
+/* Hand cursor on clickable input elements */
+a[href], input[type='button'], input[type='submit'], input[type='image'], label[for], select, button, .pointer { cursor: pointer; }
+
+
+/* Webkit browsers add a 2px margin outside the chrome of form elements */
+button, input, select, textarea { margin: 0; }
+
+/* Colors for form validity */
+input:valid, textarea:valid { }
+input:invalid, textarea:invalid {
+ border-radius: 1px; -moz-box-shadow: 0px 0px 5px red; -webkit-box-shadow: 0px 0px 5px red; box-shadow: 0px 0px 5px red;
+}
+.no-boxshadow input:invalid, .no-boxshadow textarea:invalid { background-color: #f0dddd; }
+
+/* Make buttons play nice in IE:
+ www.viget.com/inspire/styling-the-button-element-in-internet-explorer/ */
+button { width: auto; overflow: visible; }
+
+
+/* Tables */
+
+table { border-collapse:collapse; border-spacing:0; }
+th,td { padding:.5em; }
+th { font-weight:bold; text-align:center; }
+td { vertical-align: top; }
+
+
+/* Misc */
+
+/* These selection declarations have to be separate
+ No text-shadow: twitter.com/miketaylr/status/12228805301
+ Also: hot pink! */
+::-moz-selection{ background: #FF5E99; color:#fff; text-shadow: none; }
+::selection { background:#FF5E99; color:#fff; text-shadow: none; }
+
+/* j.mp/webkit-tap-highlight-color */
+a:link { -webkit-tap-highlight-color: #FF5E99; }
+
+/* Bicubic resizing for non-native sized IMG:
+ code.flickr.com/blog/2008/11/12/on-ui-quality-the-little-things-client-side-image-resizing/ */
+.ie7 img { -ms-interpolation-mode: bicubic; }
+
+
+/* Minimal Basic Classes */
+
+.clearer { clear:both !important; float:none !important; margin:0 !important; padding:0 !important; }
+.rounded { border-radius:1em; -moz-border-radius:1em; -webkit-border-radius:1em; }
--- /dev/null
+html,body,div,span,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,abbr,address,cite,code,del,dfn,em,img,ins,kbd,q,samp,small,strong,sub,sup,var,b,i,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,figcaption,figure,footer,header,hgroup,menu,nav,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline;}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block;}html{color:#000;background:#fff;width:100%;height:100%;overflow-y:scroll;}body{position:absolute;width:100%;top:0;left:0;}address,caption,cite,code,dfn,em,strong,th,var,optgroup{font-style:inherit;font-weight:inherit;}fieldset,img{border:0;}caption,th{text-align:left;}caption{margin-bottom:.5em;text-align:center;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}h1,h2,h3,h4,h5,h6,strong{font-weight:bold;}h1{font-size:250%;}h2{font-size:150%;}h3{font-size:125%;}h4{font-size:110%;}h1,h2,h3{margin:.5em 0 1em;}ins{background-color:#ff9;color:#000;text-decoration:none;}mark{background-color:#ff9;color:#000;font-style:italic;font-weight:bold;}del{text-decoration:line-through;}abbr[title],dfn[title]{border-bottom:1px dotted;cursor:help;}abbr,acronym{border:0;font-variant:normal;border-bottom:1px dotted #000;cursor:help;}em{font-style:italic;}small{font-size:85%;}strong,th{font-weight:bold;}sub,sup{font-size:75%;line-height:0;position:relative;}sup{top:-0.5em;}sub{bottom:-0.25em;}blockquote,q{quotes:none;}blockquote:before,blockquote:after,q:before,q:after{content:'';content:none;}hr{display:block;height:1px;border:0;border-top:1px solid #ccc;margin:1em 0;padding:0;}//body{font:13px/1.231 sans-serif;*font-size:small;}body{font:16px/1.231 sans-serif;*font-size:small;}select,input,textarea,button{font:99% sans-serif;}pre,code,kbd,samp{font-family:monospace,sans-serif;}pre{white-space:pre;white-space:pre-wrap;word-wrap:break-word;padding:15px;}p,fieldset,table,pre{margin-bottom:1em;}a:hover,a:active{outline:none;}ul,ol,dl,blockquote{margin:1em;}ol,ul,dl{margin-left:2em;}li{list-style:none;}ol{list-style-type:decimal;}ol li{list-style:decimal inside;}ul li{list-style:disc inside;}dl dd{margin-left:1em;}nav ul,nav li{margin:0;list-style:none;list-style-image:none;}input,select{vertical-align:middle;}input,button,textarea,select,optgroup,option{font-family:inherit;font-size:inherit;font-style:inherit;font-weight:inherit;}input,button,textarea,select{*font-size:100%;}input[type=text],input[type=password],textarea{width:12.25em;*width:11.9em;}textarea{overflow:auto;}legend{color:#000;}.ie6 legend,.ie7 legend{margin-left:-7px;}input[type="radio"]{vertical-align:text-bottom;}input[type="checkbox"]{vertical-align:bottom;}.ie7 input[type="checkbox"]{vertical-align:baseline;}.ie6 input{vertical-align:text-bottom;}label,input[type="button"],input[type="submit"],input[type="image"],button{cursor:pointer;}button,input,select,textarea{margin:0;}input:invalid,textarea:invalid{border-radius:1px;-moz-box-shadow:0 0 5px red;-webkit-box-shadow:0 0 5px red;box-shadow:0 0 5px red;}.no-boxshadow input:invalid,.no-boxshadow textarea:invalid{background-color:#f0dddd;}button{width:auto;overflow:visible;}table{border-collapse:collapse;border-spacing:0;}th,td{padding:.5em;}th{font-weight:bold;text-align:center;}td{vertical-align:top;}::-moz-selection{background:#FF5E99;color:#fff;text-shadow:none;}::selection{background:#FF5E99;color:#fff;text-shadow:none;}a:link{-webkit-tap-highlight-color:#FF5E99;}.ie7 img{-ms-interpolation-mode:bicubic;}.clearer{clear:both!important;float:none!important;margin:0!important;padding:0!important;}.rounded{border-radius:1em;-moz-border-radius:1em;-webkit-border-radius:1em;}
\ No newline at end of file
--- /dev/null
+// Backbone.js 0.5.3
+// (c) 2010 Jeremy Ashkenas, DocumentCloud Inc.
+// Backbone may be freely distributed under the MIT license.
+// For all details and documentation:
+// http://documentcloud.github.com/backbone
+(function(){var h=this,p=h.Backbone,e;e=typeof exports!=="undefined"?exports:h.Backbone={};e.VERSION="0.5.3";var f=h._;if(!f&&typeof require!=="undefined")f=require("underscore")._;var g=h.jQuery||h.Zepto;e.noConflict=function(){h.Backbone=p;return this};e.emulateHTTP=!1;e.emulateJSON=!1;e.Events={bind:function(a,b,c){var d=this._callbacks||(this._callbacks={});(d[a]||(d[a]=[])).push([b,c]);return this},unbind:function(a,b){var c;if(a){if(c=this._callbacks)if(b){c=c[a];if(!c)return this;for(var d=
+0,e=c.length;d<e;d++)if(c[d]&&b===c[d][0]){c[d]=null;break}}else c[a]=[]}else this._callbacks={};return this},trigger:function(a){var b,c,d,e,f=2;if(!(c=this._callbacks))return this;for(;f--;)if(b=f?a:"all",b=c[b])for(var g=0,h=b.length;g<h;g++)(d=b[g])?(e=f?Array.prototype.slice.call(arguments,1):arguments,d[0].apply(d[1]||this,e)):(b.splice(g,1),g--,h--);return this}};e.Model=function(a,b){var c;a||(a={});if(c=this.defaults)f.isFunction(c)&&(c=c.call(this)),a=f.extend({},c,a);this.attributes={};
+this._escapedAttributes={};this.cid=f.uniqueId("c");this.set(a,{silent:!0});this._changed=!1;this._previousAttributes=f.clone(this.attributes);if(b&&b.collection)this.collection=b.collection;this.initialize(a,b)};f.extend(e.Model.prototype,e.Events,{_previousAttributes:null,_changed:!1,idAttribute:"id",initialize:function(){},toJSON:function(){return f.clone(this.attributes)},get:function(a){return this.attributes[a]},escape:function(a){var b;if(b=this._escapedAttributes[a])return b;b=this.attributes[a];
+return this._escapedAttributes[a]=(b==null?"":""+b).replace(/&(?!\w+;|#\d+;|#x[\da-f]+;)/gi,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\//g,"/")},has:function(a){return this.attributes[a]!=null},set:function(a,b){b||(b={});if(!a)return this;if(a.attributes)a=a.attributes;var c=this.attributes,d=this._escapedAttributes;if(!b.silent&&this.validate&&!this._performValidation(a,b))return!1;if(this.idAttribute in a)this.id=a[this.idAttribute];
+var e=this._changing;this._changing=!0;for(var g in a){var h=a[g];if(!f.isEqual(c[g],h))c[g]=h,delete d[g],this._changed=!0,b.silent||this.trigger("change:"+g,this,h,b)}!e&&!b.silent&&this._changed&&this.change(b);this._changing=!1;return this},unset:function(a,b){if(!(a in this.attributes))return this;b||(b={});var c={};c[a]=void 0;if(!b.silent&&this.validate&&!this._performValidation(c,b))return!1;delete this.attributes[a];delete this._escapedAttributes[a];a==this.idAttribute&&delete this.id;this._changed=
+!0;b.silent||(this.trigger("change:"+a,this,void 0,b),this.change(b));return this},clear:function(a){a||(a={});var b,c=this.attributes,d={};for(b in c)d[b]=void 0;if(!a.silent&&this.validate&&!this._performValidation(d,a))return!1;this.attributes={};this._escapedAttributes={};this._changed=!0;if(!a.silent){for(b in c)this.trigger("change:"+b,this,void 0,a);this.change(a)}return this},fetch:function(a){a||(a={});var b=this,c=a.success;a.success=function(d,e,f){if(!b.set(b.parse(d,f),a))return!1;c&&
+c(b,d)};a.error=i(a.error,b,a);return(this.sync||e.sync).call(this,"read",this,a)},save:function(a,b){b||(b={});if(a&&!this.set(a,b))return!1;var c=this,d=b.success;b.success=function(a,e,f){if(!c.set(c.parse(a,f),b))return!1;d&&d(c,a,f)};b.error=i(b.error,c,b);var f=this.isNew()?"create":"update";return(this.sync||e.sync).call(this,f,this,b)},destroy:function(a){a||(a={});if(this.isNew())return this.trigger("destroy",this,this.collection,a);var b=this,c=a.success;a.success=function(d){b.trigger("destroy",
+b,b.collection,a);c&&c(b,d)};a.error=i(a.error,b,a);return(this.sync||e.sync).call(this,"delete",this,a)},url:function(){var a=k(this.collection)||this.urlRoot||l();if(this.isNew())return a;return a+(a.charAt(a.length-1)=="/"?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this)},isNew:function(){return this.id==null},change:function(a){this.trigger("change",this,a);this._previousAttributes=f.clone(this.attributes);this._changed=!1},hasChanged:function(a){if(a)return this._previousAttributes[a]!=
+this.attributes[a];return this._changed},changedAttributes:function(a){a||(a=this.attributes);var b=this._previousAttributes,c=!1,d;for(d in a)f.isEqual(b[d],a[d])||(c=c||{},c[d]=a[d]);return c},previous:function(a){if(!a||!this._previousAttributes)return null;return this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},_performValidation:function(a,b){var c=this.validate(a);if(c)return b.error?b.error(this,c,b):this.trigger("error",this,c,b),!1;return!0}});
+e.Collection=function(a,b){b||(b={});if(b.comparator)this.comparator=b.comparator;f.bindAll(this,"_onModelEvent","_removeReference");this._reset();a&&this.reset(a,{silent:!0});this.initialize.apply(this,arguments)};f.extend(e.Collection.prototype,e.Events,{model:e.Model,initialize:function(){},toJSON:function(){return this.map(function(a){return a.toJSON()})},add:function(a,b){if(f.isArray(a))for(var c=0,d=a.length;c<d;c++)this._add(a[c],b);else this._add(a,b);return this},remove:function(a,b){if(f.isArray(a))for(var c=
+0,d=a.length;c<d;c++)this._remove(a[c],b);else this._remove(a,b);return this},get:function(a){if(a==null)return null;return this._byId[a.id!=null?a.id:a]},getByCid:function(a){return a&&this._byCid[a.cid||a]},at:function(a){return this.models[a]},sort:function(a){a||(a={});if(!this.comparator)throw Error("Cannot sort a set without a comparator");this.models=this.sortBy(this.comparator);a.silent||this.trigger("reset",this,a);return this},pluck:function(a){return f.map(this.models,function(b){return b.get(a)})},
+reset:function(a,b){a||(a=[]);b||(b={});this.each(this._removeReference);this._reset();this.add(a,{silent:!0});b.silent||this.trigger("reset",this,b);return this},fetch:function(a){a||(a={});var b=this,c=a.success;a.success=function(d,f,e){b[a.add?"add":"reset"](b.parse(d,e),a);c&&c(b,d)};a.error=i(a.error,b,a);return(this.sync||e.sync).call(this,"read",this,a)},create:function(a,b){var c=this;b||(b={});a=this._prepareModel(a,b);if(!a)return!1;var d=b.success;b.success=function(a,e,f){c.add(a,b);
+d&&d(a,e,f)};a.save(null,b);return a},parse:function(a){return a},chain:function(){return f(this.models).chain()},_reset:function(){this.length=0;this.models=[];this._byId={};this._byCid={}},_prepareModel:function(a,b){if(a instanceof e.Model){if(!a.collection)a.collection=this}else{var c=a;a=new this.model(c,{collection:this});a.validate&&!a._performValidation(c,b)&&(a=!1)}return a},_add:function(a,b){b||(b={});a=this._prepareModel(a,b);if(!a)return!1;var c=this.getByCid(a);if(c)throw Error(["Can't add the same model to a set twice",
+c.id]);this._byId[a.id]=a;this._byCid[a.cid]=a;this.models.splice(b.at!=null?b.at:this.comparator?this.sortedIndex(a,this.comparator):this.length,0,a);a.bind("all",this._onModelEvent);this.length++;b.silent||a.trigger("add",a,this,b);return a},_remove:function(a,b){b||(b={});a=this.getByCid(a)||this.get(a);if(!a)return null;delete this._byId[a.id];delete this._byCid[a.cid];this.models.splice(this.indexOf(a),1);this.length--;b.silent||a.trigger("remove",a,this,b);this._removeReference(a);return a},
+_removeReference:function(a){this==a.collection&&delete a.collection;a.unbind("all",this._onModelEvent)},_onModelEvent:function(a,b,c,d){(a=="add"||a=="remove")&&c!=this||(a=="destroy"&&this._remove(b,d),b&&a==="change:"+b.idAttribute&&(delete this._byId[b.previous(b.idAttribute)],this._byId[b.id]=b),this.trigger.apply(this,arguments))}});f.each(["forEach","each","map","reduce","reduceRight","find","detect","filter","select","reject","every","all","some","any","include","contains","invoke","max",
+"min","sortBy","sortedIndex","toArray","size","first","rest","last","without","indexOf","lastIndexOf","isEmpty","groupBy"],function(a){e.Collection.prototype[a]=function(){return f[a].apply(f,[this.models].concat(f.toArray(arguments)))}});e.Router=function(a){a||(a={});if(a.routes)this.routes=a.routes;this._bindRoutes();this.initialize.apply(this,arguments)};var q=/:([\w\d]+)/g,r=/\*([\w\d]+)/g,s=/[-[\]{}()+?.,\\^$|#\s]/g;f.extend(e.Router.prototype,e.Events,{initialize:function(){},route:function(a,
+b,c){e.history||(e.history=new e.History);f.isRegExp(a)||(a=this._routeToRegExp(a));e.history.route(a,f.bind(function(d){d=this._extractParameters(a,d);c.apply(this,d);this.trigger.apply(this,["route:"+b].concat(d))},this))},navigate:function(a,b){e.history.navigate(a,b)},_bindRoutes:function(){if(this.routes){var a=[],b;for(b in this.routes)a.unshift([b,this.routes[b]]);b=0;for(var c=a.length;b<c;b++)this.route(a[b][0],a[b][1],this[a[b][1]])}},_routeToRegExp:function(a){a=a.replace(s,"\\$&").replace(q,
+"([^/]*)").replace(r,"(.*?)");return RegExp("^"+a+"$")},_extractParameters:function(a,b){return a.exec(b).slice(1)}});e.History=function(){this.handlers=[];f.bindAll(this,"checkUrl")};var j=/^#*/,t=/msie [\w.]+/,m=!1;f.extend(e.History.prototype,{interval:50,getFragment:function(a,b){if(a==null)if(this._hasPushState||b){a=window.location.pathname;var c=window.location.search;c&&(a+=c);a.indexOf(this.options.root)==0&&(a=a.substr(this.options.root.length))}else a=window.location.hash;return decodeURIComponent(a.replace(j,
+""))},start:function(a){if(m)throw Error("Backbone.history has already been started");this.options=f.extend({},{root:"/"},this.options,a);this._wantsPushState=!!this.options.pushState;this._hasPushState=!(!this.options.pushState||!window.history||!window.history.pushState);a=this.getFragment();var b=document.documentMode;if(b=t.exec(navigator.userAgent.toLowerCase())&&(!b||b<=7))this.iframe=g('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo("body")[0].contentWindow,this.navigate(a);
+this._hasPushState?g(window).bind("popstate",this.checkUrl):"onhashchange"in window&&!b?g(window).bind("hashchange",this.checkUrl):setInterval(this.checkUrl,this.interval);this.fragment=a;m=!0;a=window.location;b=a.pathname==this.options.root;if(this._wantsPushState&&!this._hasPushState&&!b)return this.fragment=this.getFragment(null,!0),window.location.replace(this.options.root+"#"+this.fragment),!0;else if(this._wantsPushState&&this._hasPushState&&b&&a.hash)this.fragment=a.hash.replace(j,""),window.history.replaceState({},
+document.title,a.protocol+"//"+a.host+this.options.root+this.fragment);if(!this.options.silent)return this.loadUrl()},route:function(a,b){this.handlers.unshift({route:a,callback:b})},checkUrl:function(){var a=this.getFragment();a==this.fragment&&this.iframe&&(a=this.getFragment(this.iframe.location.hash));if(a==this.fragment||a==decodeURIComponent(this.fragment))return!1;this.iframe&&this.navigate(a);this.loadUrl()||this.loadUrl(window.location.hash)},loadUrl:function(a){var b=this.fragment=this.getFragment(a);
+return f.any(this.handlers,function(a){if(a.route.test(b))return a.callback(b),!0})},navigate:function(a,b){var c=(a||"").replace(j,"");if(!(this.fragment==c||this.fragment==decodeURIComponent(c))){if(this._hasPushState){var d=window.location;c.indexOf(this.options.root)!=0&&(c=this.options.root+c);this.fragment=c;window.history.pushState({},document.title,d.protocol+"//"+d.host+c)}else if(window.location.hash=this.fragment=c,this.iframe&&c!=this.getFragment(this.iframe.location.hash))this.iframe.document.open().close(),
+this.iframe.location.hash=c;b&&this.loadUrl(a)}}});e.View=function(a){this.cid=f.uniqueId("view");this._configure(a||{});this._ensureElement();this.delegateEvents();this.initialize.apply(this,arguments)};var u=/^(\S+)\s*(.*)$/,n=["model","collection","el","id","attributes","className","tagName"];f.extend(e.View.prototype,e.Events,{tagName:"div",$:function(a){return g(a,this.el)},initialize:function(){},render:function(){return this},remove:function(){g(this.el).remove();return this},make:function(a,
+b,c){a=document.createElement(a);b&&g(a).attr(b);c&&g(a).html(c);return a},delegateEvents:function(a){if(a||(a=this.events))for(var b in f.isFunction(a)&&(a=a.call(this)),g(this.el).unbind(".delegateEvents"+this.cid),a){var c=this[a[b]];if(!c)throw Error('Event "'+a[b]+'" does not exist');var d=b.match(u),e=d[1];d=d[2];c=f.bind(c,this);e+=".delegateEvents"+this.cid;d===""?g(this.el).bind(e,c):g(this.el).delegate(d,e,c)}},_configure:function(a){this.options&&(a=f.extend({},this.options,a));for(var b=
+0,c=n.length;b<c;b++){var d=n[b];a[d]&&(this[d]=a[d])}this.options=a},_ensureElement:function(){if(this.el){if(f.isString(this.el))this.el=g(this.el).get(0)}else{var a=this.attributes||{};if(this.id)a.id=this.id;if(this.className)a["class"]=this.className;this.el=this.make(this.tagName,a)}}});e.Model.extend=e.Collection.extend=e.Router.extend=e.View.extend=function(a,b){var c=v(this,a,b);c.extend=this.extend;return c};var w={create:"POST",update:"PUT","delete":"DELETE",read:"GET"};e.sync=function(a,
+b,c){var d=w[a];c=f.extend({type:d,dataType:"json"},c);if(!c.url)c.url=k(b)||l();if(!c.data&&b&&(a=="create"||a=="update"))c.contentType="application/json",c.data=JSON.stringify(b.toJSON());if(e.emulateJSON)c.contentType="application/x-www-form-urlencoded",c.data=c.data?{model:c.data}:{};if(e.emulateHTTP&&(d==="PUT"||d==="DELETE")){if(e.emulateJSON)c.data._method=d;c.type="POST";c.beforeSend=function(a){a.setRequestHeader("X-HTTP-Method-Override",d)}}if(c.type!=="GET"&&!e.emulateJSON)c.processData=
+!1;return g.ajax(c)};var o=function(){},v=function(a,b,c){var d;d=b&&b.hasOwnProperty("constructor")?b.constructor:function(){return a.apply(this,arguments)};f.extend(d,a);o.prototype=a.prototype;d.prototype=new o;b&&f.extend(d.prototype,b);c&&f.extend(d,c);d.prototype.constructor=d;d.__super__=a.prototype;return d},k=function(a){if(!a||!a.url)return null;return f.isFunction(a.url)?a.url():a.url},l=function(){throw Error('A "url" property or function must be specified');},i=function(a,b,c){return function(d){a?
+a(b,d,c):b.trigger("error",b,d,c)}}}).call(this);
--- /dev/null
+// Backbone.js 0.5.3
+// (c) 2010 Jeremy Ashkenas, DocumentCloud Inc.
+// Backbone may be freely distributed under the MIT license.
+// For all details and documentation:
+// http://documentcloud.github.com/backbone
+
+(function(){
+
+ // Initial Setup
+ // -------------
+
+ // Save a reference to the global object.
+ var root = this;
+
+ // Save the previous value of the `Backbone` variable.
+ var previousBackbone = root.Backbone;
+
+ // The top-level namespace. All public Backbone classes and modules will
+ // be attached to this. Exported for both CommonJS and the browser.
+ var Backbone;
+ if (typeof exports !== 'undefined') {
+ Backbone = exports;
+ } else {
+ Backbone = root.Backbone = {};
+ }
+
+ // Current version of the library. Keep in sync with `package.json`.
+ Backbone.VERSION = '0.5.3';
+
+ // Require Underscore, if we're on the server, and it's not already present.
+ var _ = root._;
+ if (!_ && (typeof require !== 'undefined')) _ = require('underscore')._;
+
+ // For Backbone's purposes, jQuery or Zepto owns the `$` variable.
+ var $ = root.jQuery || root.Zepto;
+
+ // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
+ // to its previous owner. Returns a reference to this Backbone object.
+ Backbone.noConflict = function() {
+ root.Backbone = previousBackbone;
+ return this;
+ };
+
+ // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option will
+ // fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and set a
+ // `X-Http-Method-Override` header.
+ Backbone.emulateHTTP = false;
+
+ // Turn on `emulateJSON` to support legacy servers that can't deal with direct
+ // `application/json` requests ... will encode the body as
+ // `application/x-www-form-urlencoded` instead and will send the model in a
+ // form param named `model`.
+ Backbone.emulateJSON = false;
+
+ // Backbone.Events
+ // -----------------
+
+ // A module that can be mixed in to *any object* in order to provide it with
+ // custom events. You may `bind` or `unbind` a callback function to an event;
+ // `trigger`-ing an event fires all callbacks in succession.
+ //
+ // var object = {};
+ // _.extend(object, Backbone.Events);
+ // object.bind('expand', function(){ alert('expanded'); });
+ // object.trigger('expand');
+ //
+ Backbone.Events = {
+
+ // Bind an event, specified by a string name, `ev`, to a `callback` function.
+ // Passing `"all"` will bind the callback to all events fired.
+ bind : function(ev, callback, context) {
+ var calls = this._callbacks || (this._callbacks = {});
+ var list = calls[ev] || (calls[ev] = []);
+ list.push([callback, context]);
+ return this;
+ },
+
+ // Remove one or many callbacks. If `callback` is null, removes all
+ // callbacks for the event. If `ev` is null, removes all bound callbacks
+ // for all events.
+ unbind : function(ev, callback) {
+ var calls;
+ if (!ev) {
+ this._callbacks = {};
+ } else if (calls = this._callbacks) {
+ if (!callback) {
+ calls[ev] = [];
+ } else {
+ var list = calls[ev];
+ if (!list) return this;
+ for (var i = 0, l = list.length; i < l; i++) {
+ &n