├── .gitignore ├── README.md ├── app ├── app.py ├── debug.py ├── filters.py ├── rapid.py ├── simple.py └── templates │ ├── articles.json │ ├── data.json │ ├── dates.jinja2.html │ ├── index.jinja2.html │ ├── layout.jinja2.html │ ├── render.py │ ├── seconds.jinja2.html │ ├── submit.jinja2.html │ ├── success.jinja2.html │ └── util.jinja2.html ├── deployment ├── nginx │ └── sites-enabled │ │ └── hacknode1 └── supervisor │ └── conf.d │ └── hacknode1.conf ├── dev-requirements.txt ├── fabfile.py ├── notebooks ├── 01-Servers.ipynb ├── 02-Templates.ipynb ├── 03-Flask.ipynb ├── 04-Contexts.ipynb ├── 05-Mongo.ipynb └── serve_notebooks.sh ├── prod-requirements.txt ├── requirements.txt ├── serve.sh └── static ├── css ├── img │ ├── glyphicons-halflings-white.png │ └── glyphicons-halflings.png ├── lib │ ├── bootstrap-responsive.css │ └── bootstrap.css └── main.css ├── index.html ├── js ├── animation.js ├── cookies.js ├── firstuser.js ├── hnfake.js ├── lib │ ├── bootstrap.js │ └── jquery.js └── main.js ├── preview ├── README.md ├── codemirror │ ├── codemirror.css │ ├── codemirror.js │ ├── css.js │ ├── htmlmixed.js │ ├── javascript.js │ └── xml.js ├── emmet.js └── index.html └── submit.html /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | *.pyc 10 | 11 | pids 12 | logs 13 | results 14 | 15 | npm-debug.log 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | rapid-web 2 | ========= 3 | 4 | Rapid Web Prototyping with Lightweight Tools (code that goes with the slides). 5 | 6 | The code in this repository is neatly organized into a number of git tags. 7 | 8 | You can [browse them on Github here](https://github.com/amontalenti/rapid-web/tags). They are also described below. 9 | 10 | 11 | 12 | v0.1-init 13 | --------- 14 | [diff](https://github.com/amontalenti/rapid-web/compare/v0.1-init...v0.2-static) | [code](https://github.com/amontalenti/rapid-web/tree/v0.1-init) 15 | 16 | Your project still needs to be initialized with a virtualenv. 17 | 18 | v0.2-static 19 | ----------- 20 | [diff](https://github.com/amontalenti/rapid-web/compare/v0.2-static...v0.3-links) | [code](https://github.com/amontalenti/rapid-web/tree/v0.2-static) 21 | 22 | You will have a static, jQuery and Bootstrap enabled HTML template in static/index.html. 23 | 24 | v0.3-links 25 | ---------- 26 | [diff](https://github.com/amontalenti/rapid-web/compare/v0.3-links...v0.4-anim) | [code](https://github.com/amontalenti/rapid-web/tree/v0.3-links) 27 | 28 | Use Bootstrap's table and label components to build up the basic user interface. 29 | 30 | v0.4-anim 31 | --------- 32 | [diff](https://github.com/amontalenti/rapid-web/compare/v0.4-anim...v0.5-modal) | [code](https://github.com/amontalenti/rapid-web/tree/v0.4-anim) 33 | 34 | You will add some basic jQuery code for implementing a frontpage animation. 35 | 36 | v0.5-modal 37 | ---------- 38 | [diff](https://github.com/amontalenti/rapid-web/compare/v0.5-modal...v0.6-dynamic) | [code](https://github.com/amontalenti/rapid-web/tree/v0.5-modal) 39 | 40 | Use Bootstrap JavaScript component to add a modal dialog. 41 | 42 | v0.6-dynamic 43 | ------------ 44 | [diff](https://github.com/amontalenti/rapid-web/compare/v0.6-dynamic...v0.7-submit) | [code](https://github.com/amontalenti/rapid-web/tree/v0.6-dynamic) 45 | 46 | Add a slightest bit of dynamism via a public JSON-P Hacker News API. 47 | 48 | v0.7-submit 49 | ----------- 50 | [diff](https://github.com/amontalenti/rapid-web/compare/v0.7-submit...v0.8-clickable) | [code](https://github.com/amontalenti/rapid-web/tree/v0.7-submit) 51 | 52 | Add a form for submitting new stories with Bootstrap. 53 | 54 | v0.8-clickable 55 | -------------- 56 | [diff](https://github.com/amontalenti/rapid-web/compare/v0.8-clickable...v0.9-flask) | [code](https://github.com/amontalenti/rapid-web/tree/v0.8-clickable) 57 | 58 | Clickable prototype now complete; now to add a Python server! 59 | 60 | v0.9-flask 61 | ---------- 62 | [diff](https://github.com/amontalenti/rapid-web/compare/v0.9-flask...v1.0-app) | [code](https://github.com/amontalenti/rapid-web/tree/v0.9-flask) 63 | 64 | Skeletal Flask application working with stub view functions. 65 | 66 | v1.0-app 67 | -------- 68 | [diff](https://github.com/amontalenti/rapid-web/compare/v1.0-app...v1.1-jinja) | [code](https://github.com/amontalenti/rapid-web/tree/v1.0-app) 69 | 70 | Example Jinja2 templates bringing the app together. 71 | 72 | v1.1-jinja 73 | ---------- 74 | [diff](https://github.com/amontalenti/rapid-web/compare/v1.1-jinja...v1.2-formflow) | [code](https://github.com/amontalenti/rapid-web/tree/v1.1-jinja) 75 | 76 | Jinja2 templates upgraded using Bootstrap markup and scripts from prototype. 77 | 78 | v1.2-formflow 79 | ------------- 80 | [diff](https://github.com/amontalenti/rapid-web/compare/v1.2-formflow...v1.3-filters) | [code](https://github.com/amontalenti/rapid-web/tree/v1.2-formflow) 81 | 82 | Beginnings of a form submission and multi-page flow. 83 | 84 | v1.3-filters 85 | ------------ 86 | [diff](https://github.com/amontalenti/rapid-web/compare/v1.3-filters...v1.4-validation) | [code](https://github.com/amontalenti/rapid-web/tree/v1.3-filters) 87 | 88 | Simple and complex Jinja2 filters for use in templates. 89 | 90 | v1.4-validation 91 | --------------- 92 | [diff](https://github.com/amontalenti/rapid-web/compare/v1.4-validation...v1.5-sort) | [code](https://github.com/amontalenti/rapid-web/tree/v1.4-validation) 93 | 94 | Basic form validation logic in the web app. 95 | 96 | v1.5-sort 97 | --------- 98 | [diff](https://github.com/amontalenti/rapid-web/compare/v1.5-sort...v1.6-clicktrack) | [code](https://github.com/amontalenti/rapid-web/tree/v1.5-sort) 99 | 100 | Utilizes Jinja2 filters to sort results in index page. 101 | 102 | v1.6-clicktrack 103 | --------------- 104 | [diff](https://github.com/amontalenti/rapid-web/compare/v1.6-clicktrack...v1.7-fabric) | [code](https://github.com/amontalenti/rapid-web/tree/v1.6-clicktrack) 105 | 106 | Click tracking using a new Flask route, redirect, and Jinja2 macro. 107 | 108 | v1.7-fabric 109 | ----------- 110 | [diff](https://github.com/amontalenti/rapid-web/compare/v1.7-fabric...v1.8-server) | [code](https://github.com/amontalenti/rapid-web/tree/v1.7-fabric) 111 | 112 | A first fabfile for automating server management and deployment. 113 | 114 | v1.8-server 115 | ----------- 116 | [diff](https://github.com/amontalenti/rapid-web/compare/v1.8-server...v1.9-mongo) | [code](https://github.com/amontalenti/rapid-web/tree/v1.8-server) 117 | 118 | A modified fabfile for dealing with a real-world deployment environment. 119 | 120 | v1.9-mongo 121 | ---------- 122 | [diff](https://github.com/amontalenti/rapid-web/compare/v1.9-mongo...v2.0-fin) | [code](https://github.com/amontalenti/rapid-web/tree/v1.9-mongo) 123 | 124 | Add MongoDB query layer to the application for storing articles. 125 | 126 | v2.0-fin 127 | -------- 128 | [code](https://github.com/amontalenti/rapid-web/tree/v2.0-fin) 129 | 130 | Last bits to get app ready for shipping! 131 | -------------------------------------------------------------------------------- /app/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, request, redirect, abort 2 | from flask.ext.script import Manager 3 | from rapid import (top_articles, search_articles, insert_article, validate_submission, track_click) 4 | from filters import human_date 5 | 6 | app = Flask(__name__, static_folder="../static", static_url_path="/static") 7 | app.debug = True 8 | app.add_template_filter(human_date) 9 | 10 | @app.route('/') 11 | def index(): 12 | articles = top_articles() 13 | return render_template('index.jinja2.html', 14 | rows=articles, 15 | page_links="active") 16 | 17 | @app.route('/search/') 18 | def search(query): 19 | articles = search_articles(query) 20 | return render_template('index.jinja2.html', 21 | query=query, 22 | articles=articles) 23 | 24 | @app.route('/submit/', methods=["GET", "POST"]) 25 | def submit(): 26 | if request.method == "POST": 27 | return do_submit() 28 | else: 29 | return render_template('submit.jinja2.html', 30 | page_submit="active") 31 | 32 | def do_submit(): 33 | form = request.form 34 | submission = dict( 35 | title=form["title"], 36 | link=form["link"] 37 | ) 38 | valid, errors = validate_submission(submission) 39 | if valid: 40 | article = insert_article(submission) 41 | return render_template("success.jinja2.html", 42 | page_submit="active") 43 | else: 44 | return render_template('submit.jinja2.html', 45 | page_submit="active", 46 | errors=errors) 47 | 48 | @app.route('/click/') 49 | def click(): 50 | url = request.args["url"] 51 | track_click(url) 52 | return redirect(url) 53 | 54 | manager = Manager(app) 55 | 56 | if __name__ == "__main__": 57 | manager.run() 58 | -------------------------------------------------------------------------------- /app/debug.py: -------------------------------------------------------------------------------- 1 | from werkzeug.wrappers import Request, Response 2 | from werkzeug.debug import DebuggedApplication 3 | 4 | @Request.application 5 | def app(request): 6 | raise ValueError("testing debugger") 7 | return Response("hello, world!") 8 | 9 | app = DebuggedApplication(app, evalex=True) 10 | 11 | from werkzeug.serving import run_simple 12 | run_simple("localhost", 4000, app) 13 | -------------------------------------------------------------------------------- /app/filters.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | 3 | def val_ago(value, unit="unit"): 4 | if value == 1: 5 | return "{} {} ago".format(value, unit) 6 | else: 7 | return "{} {}s ago".format(value, unit) 8 | 9 | def human_date(dateval, nowfunc=dt.datetime.now): 10 | now = nowfunc() 11 | delta = now - dateval 12 | days = delta.days 13 | if days == 0: 14 | seconds = delta.seconds 15 | minutes = seconds / 60 16 | hours = minutes / 60 17 | if hours > 0: 18 | return val_ago(hours, unit="hour") 19 | if minutes > 0: 20 | return val_ago(minutes, unit="minute") 21 | return val_ago(seconds, unit="second") 22 | elif 0 < days < 7: 23 | return val_ago(days, unit="day") 24 | else: 25 | return dateval.strftime("%m/%d/%Y") 26 | 27 | if __name__ == "__main__": 28 | jan1 = dt.datetime(2013, 1, 1) 29 | def test_case(expected, **kwargs): 30 | val = jan1 - dt.timedelta(**kwargs) 31 | human = human_date(val, nowfunc=lambda: jan1) 32 | assert human == expected, human 33 | test_case("1 day ago", days=1) 34 | test_case("2 days ago", days=2) 35 | test_case("5 seconds ago", seconds=5) 36 | test_case("2 minutes ago", seconds=60*2) 37 | test_case("3 hours ago", seconds=60*60*3) 38 | test_case("12/25/2012", days=7) 39 | -------------------------------------------------------------------------------- /app/rapid.py: -------------------------------------------------------------------------------- 1 | from urllib2 import urlopen, URLError 2 | import datetime as dt 3 | from pymongo import MongoClient 4 | 5 | TEAM_NAME = "hacknode1" 6 | 7 | def get_collection(): 8 | return MongoClient()[TEAM_NAME].articles 9 | 10 | def top_articles(): 11 | coll = get_collection() 12 | articles = coll.find() 13 | return list(articles) 14 | 15 | def search_articles(query): 16 | print "Searching ->", query 17 | articles = coll.find({"title": 18 | {"$regex": query} 19 | }) 20 | return list(articles) 21 | 22 | def insert_article(article): 23 | coll = get_collection() 24 | existing = coll.find_one({"link": article["link"]}) 25 | if existing is not None: 26 | print "Found existing, explicit upvoting ->", existing 27 | coll.update({"link": existing["link"]}, 28 | {"$inc": 29 | {"score": 5} 30 | }) 31 | return True 32 | else: 33 | article["score"] = 0 34 | article["date"] = dt.datetime.now() 35 | print "Inserting ->", article 36 | coll.insert(article) 37 | return True 38 | 39 | def track_click(url): 40 | coll = get_collection() 41 | print "Tracking ->", url 42 | coll.update({"link": url}, 43 | {"$inc": 44 | {"score": 1} 45 | }) 46 | return True 47 | 48 | def validate_submission(params): 49 | errors = {} 50 | def err(id, msg): 51 | errors[id] = msg 52 | title = params["title"] 53 | title = title.strip() 54 | if len(title) < 2: 55 | err("title", "title must be > 2 characters") 56 | if len(title) > 150: 57 | err("title", "title may not be > 150 characters") 58 | link = params["link"] 59 | link = link.strip() 60 | try: 61 | opened = urlopen(link) 62 | link = opened.geturl() 63 | except (URLError, ValueError): 64 | err("link", "link could not be reached") 65 | if len(errors) > 0: 66 | return (False, errors) 67 | else: 68 | return (True, errors) 69 | -------------------------------------------------------------------------------- /app/simple.py: -------------------------------------------------------------------------------- 1 | from filters import val_ago, human_date 2 | from flask import render_template, Flask 3 | import datetime as dt 4 | 5 | app = Flask(__name__) 6 | 7 | def top_articles(): 8 | articles = [ 9 | {"title": "Google", "score": 150, "link": "http://google.com"}, 10 | {"title": "Yahoo", "score": 75, "link": "http://yahoo.com"}, 11 | {"title": "Bing", "score": 50, "link": "http://bing.com"} 12 | ] 13 | return articles 14 | 15 | @app.route('/') 16 | def index(): 17 | articles = top_articles() 18 | return render_template("index.jinja2.html", rows=articles) 19 | 20 | @app.template_filter() 21 | def seconds_ago(val): 22 | return val_ago(val, unit="second") 23 | 24 | @app.route('/experiment') 25 | def experiment(): 26 | return render_template('seconds.jinja2.html', 27 | seconds=range(60)) 28 | 29 | app.add_template_filter(human_date) 30 | 31 | @app.route('/datetest') 32 | def datetest(): 33 | now = dt.datetime.now() 34 | deltas = [ 35 | dt.timedelta(seconds=5), 36 | dt.timedelta(seconds=60*60), 37 | dt.timedelta(days=5), 38 | dt.timedelta(days=60) 39 | ] 40 | dates = [(now - delta) 41 | for delta in deltas] 42 | return render_template('dates.jinja2.html', 43 | dates=dates) 44 | 45 | if __name__ == "__main__": 46 | app.run(debug=True) 47 | -------------------------------------------------------------------------------- /app/templates/articles.json: -------------------------------------------------------------------------------- 1 | { "rows": [ 2 | {"title": "Google", "score": 150, "link": "http://google.com"}, 3 | {"title": "Yahoo", "score": 75, "link": "http://yahoo.com"}, 4 | {"title": "Bing", "score": 50, "link": "http://bing.com"}, 5 | {"title": "Google", "score": 150, "link": "http://google.com"}, 6 | {"title": "Yahoo", "score": 75, "link": "http://yahoo.com"}, 7 | {"title": "Bing", "score": 50, "link": "http://bing.com"}, 8 | {"title": "Google", "score": 150, "link": "http://google.com"}, 9 | {"title": "Yahoo", "score": 75, "link": "http://yahoo.com"}, 10 | {"title": "Bing", "score": 50, "link": "http://bing.com"} 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /app/templates/data.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /app/templates/dates.jinja2.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /app/templates/index.jinja2.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.jinja2.html' %} 2 | 3 | {% block title %}Latest News{% endblock %} 4 | 5 | {% block css %} 6 | {{ link_tag('main') }} 7 | {% endblock %} 8 | 9 | {% block body %} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% for row in rows|sort(attribute="date", reverse=True) %} 20 | 21 | 22 | 23 | 24 | 25 | {% endfor %} 26 | 27 |
ScoreLinkPublished
{{ row.score }}{{ tracked_link(row.title, row.link) }}{{ row.date|human_date }}
28 | {% endblock %} 29 | 30 | {% block js %} 31 | {{ script_tag('hnfake') }} 32 | {{ script_tag('cookies') }} 33 | {{ script_tag('animation') }} 34 | {{ script_tag('firstuser') }} 35 | {{ script_tag('main') }} 36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /app/templates/layout.jinja2.html: -------------------------------------------------------------------------------- 1 | {% from 'util.jinja2.html' import link_tag, script_tag, tracked_link %} 2 | 3 | 4 | 5 | 6 | (RN) {% block title %}{% endblock %} 7 | 8 | {{ link_tag('lib/bootstrap') }} 9 | {{ link_tag('lib/bootstrap-responsive') }} 10 | 11 | {% block css %} 12 | {% endblock %} 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 34 | 35 | {% block body %} 36 | {% endblock %} 37 | 38 | 39 |
40 | 41 | {{ script_tag('lib/jquery') }} 42 | {{ script_tag('lib/bootstrap') }} 43 | 44 | 45 | {% block js %} 46 | {% endblock %} 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/templates/render.py: -------------------------------------------------------------------------------- 1 | # Global list of available format parsers on your system 2 | # mapped to the callable/Exception to parse a string into a dict 3 | formats = {} 4 | 5 | class MalformedJSON(Exception): pass 6 | 7 | # json - simplejson or packaged json as a fallback 8 | try: 9 | import simplejson 10 | formats['json'] = (simplejson.loads, simplejson.decoder.JSONDecodeError, MalformedJSON) 11 | except ImportError: 12 | try: 13 | import json 14 | formats['json'] = (json.loads, ValueError, MalformedJSON) 15 | except ImportError: 16 | pass 17 | 18 | import os 19 | import sys 20 | from optparse import OptionParser 21 | 22 | from jinja2 import Environment, FileSystemLoader 23 | 24 | def cli(opts, args): 25 | if args[1] == '-': 26 | data = sys.stdin.read() 27 | else: 28 | data = open(os.path.join(os.getcwd(), os.path.expanduser(args[1]))).read() 29 | 30 | try: 31 | data = formats['json'][0](data) 32 | except formats['json'][1]: 33 | raise formats['json'][2](u'%s ...' % data[:60]) 34 | sys.exit(1) 35 | 36 | env = Environment(loader=FileSystemLoader(os.getcwd())) 37 | sys.stdout.write(env.get_template(args[0]).render(data)) 38 | sys.exit(0) 39 | 40 | 41 | def main(): 42 | default_format = 'json' 43 | if default_format not in formats: 44 | default_format = sorted(formats.keys())[0] 45 | 46 | parser = OptionParser(usage="usage: %prog [options] ") 47 | opts, args = parser.parse_args() 48 | 49 | if len(args) == 0: 50 | parser.print_help() 51 | sys.exit(1) 52 | 53 | # Without the second argv, assume they want to read from stdin 54 | if len(args) == 1: 55 | args.append('-') 56 | 57 | cli(opts, args) 58 | 59 | if __name__ == "__main__": 60 | main() 61 | -------------------------------------------------------------------------------- /app/templates/seconds.jinja2.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /app/templates/submit.jinja2.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.jinja2.html' %} 2 | 3 | {% block title %}Submit News{% endblock %} 4 | 5 | {% block css %} 6 | {{ link_tag('main') }} 7 | {% endblock %} 8 | 9 | {% macro error(name) -%} 10 | {% if errors and errors[name] -%} 11 | error 12 | {% endif -%} 13 | {% endmacro -%} 14 | 15 | {% macro errorhelp(name) -%} 16 | {% if errors and errors[name] -%} 17 | {{ errors[name] }} 18 | {% endif -%} 19 | {% endmacro -%} 20 | 21 | {% macro input(name, desc, type='text', placeholder=None) -%} 22 |
23 | 24 |
25 | 29 | {{ errorhelp(name) }} 30 |
31 |
32 | {%- endmacro %} 33 | 34 | {% macro button(name) -%} 35 | 36 | {%- endmacro %} 37 | 38 | {% block body %} 39 |
40 |

Submit a new article!

41 | 42 |
43 |
44 | {{ input("link", "Link", placeholder="http://...") }} 45 | {{ input("title", "Title", placeholder="headline or description") }} 46 |
47 | {{ button("Submit") }} 48 |
49 |
50 |
51 | {% endblock %} 52 | 53 | {% block js %} 54 | {{ script_tag('submit') }} 55 | {% endblock %} 56 | -------------------------------------------------------------------------------- /app/templates/success.jinja2.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.jinja2.html' %} 2 | 3 | {% block title %}Success!{% endblock %} 4 | 5 | {% block css %} 6 | {{ link_tag('main') }} 7 | {% endblock %} 8 | 9 | {% block body %} 10 |
11 |

Thanks!

12 |

Your submission has been uploaded and will hopefully receive votes shortly.

13 |
14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /app/templates/util.jinja2.html: -------------------------------------------------------------------------------- 1 | {%- macro link_tag(location) -%} 2 | 3 | {%- endmacro -%} 4 | 5 | {%- macro script_tag(location) -%} 6 | 7 | {%- endmacro -%} 8 | 9 | {%- macro tracked_link(title, url) -%} 10 | {{ title }} 11 | {%- endmacro -%} 12 | -------------------------------------------------------------------------------- /deployment/nginx/sites-enabled/hacknode1: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name hacknode1.alephpoint.com; 4 | location / { 5 | try_files $uri @hacknode1; 6 | } 7 | location @hacknode1 { 8 | include uwsgi_params; 9 | uwsgi_pass unix:/tmp/uwsgi-hacknode1.sock; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /deployment/supervisor/conf.d/hacknode1.conf: -------------------------------------------------------------------------------- 1 | [program:hacknode1] 2 | command=uwsgi --plugins=http,python -s /tmp/uwsgi-hacknode1.sock --file /home/shared/servers/hacknode1/app/app.py --callable app -H /home/shared/servers/hacknode1/rapid-env --chmod-socket 666 3 | directory=/home/shared/servers/hacknode1/app 4 | autostart=true 5 | autorestart=true 6 | stdout_logfile=/home/shared/logs/hacknode1.log 7 | redirect_stderr=true 8 | stopsignal=QUIT 9 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | # for live code updates 2 | livereload 3 | # for ipython notebook 4 | tornado 5 | pyzmq 6 | # for deployment 7 | Fabric 8 | -------------------------------------------------------------------------------- /fabfile.py: -------------------------------------------------------------------------------- 1 | from fabric.api import * 2 | from fabric.contrib.project import rsync_project 3 | 4 | env.use_ssh_config = True 5 | env.hosts = ["shared@hacknode"] 6 | 7 | TEAM_NAME = "hacknode1" 8 | 9 | @task 10 | def list_home(): 11 | """List files in home directory.""" 12 | run("ls -lha") 13 | 14 | @task 15 | def list_servers(): 16 | """List deployment servers.""" 17 | run("ls servers") 18 | 19 | def virtualenv_run(cmd): 20 | run("source rapid-env/bin/activate && {}".format(cmd)) 21 | 22 | @task 23 | def setup_virtualenv(): 24 | """Set up virtualenv on remote machine.""" 25 | with cd("servers/" + TEAM_NAME): 26 | run("virtualenv rapid-env") 27 | virtualenv_run("pip install -r requirements.txt") 28 | 29 | @task 30 | def run_devserver(): 31 | """Run the dev Flask server on remote machine.""" 32 | with cd("servers/" + TEAM_NAME): 33 | virtualenv_run("cd app && python app.py runserver --host=0.0.0.0 --port=8000") 34 | 35 | @task 36 | def deploy(): 37 | """Deploy project remotely.""" 38 | run("mkdir -p servers") 39 | rsync_project(remote_dir="servers/" + TEAM_NAME, 40 | local_dir="./", 41 | exclude=("*.pyc", ".git", "rapid-env", "activate")) 42 | 43 | def supervisor_run(cmd): 44 | sudo("supervisorctl {}".format(cmd), shell=False) 45 | 46 | @task 47 | def restart(): 48 | """Restart supervisor service.""" 49 | supervisor_run("restart {}".format(TEAM_NAME)) 50 | run("sleep 1") 51 | supervisor_run("tail -800 {}".format(TEAM_NAME)) 52 | 53 | -------------------------------------------------------------------------------- /notebooks/01-Servers.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "name": "01-Servers" 4 | }, 5 | "nbformat": 3, 6 | "nbformat_minor": 0, 7 | "worksheets": [ 8 | { 9 | "cells": [ 10 | { 11 | "cell_type": "heading", 12 | "level": 2, 13 | "metadata": {}, 14 | "source": [ 15 | "Werkzeug Development Server" 16 | ] 17 | }, 18 | { 19 | "cell_type": "code", 20 | "collapsed": false, 21 | "input": [ 22 | "HOST=\"0.0.0.0\"\n", 23 | "PORT=4001\n", 24 | "\n", 25 | "from werkzeug.wrappers import Request, Response\n", 26 | "\n", 27 | "@Request.application\n", 28 | "def app(request):\n", 29 | " print request.path\n", 30 | " print request.headers\n", 31 | " return Response(\"hello, world!\")\n", 32 | "\n", 33 | "from werkzeug.serving import run_simple\n", 34 | "run_simple(HOST, PORT, app)" 35 | ], 36 | "language": "python", 37 | "metadata": {}, 38 | "outputs": [] 39 | }, 40 | { 41 | "cell_type": "markdown", 42 | "metadata": {}, 43 | "source": [ 44 | "After running the above, visit http://hacknode.alephpoint.com on your specified port." 45 | ] 46 | }, 47 | { 48 | "cell_type": "heading", 49 | "level": 2, 50 | "metadata": {}, 51 | "source": [ 52 | "Enable Web-based Debugging" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "collapsed": true, 58 | "input": [ 59 | "HOST=\"0.0.0.0\"\n", 60 | "PORT=4001\n", 61 | "\n", 62 | "from werkzeug.wrappers import Request, Response\n", 63 | "from werkzeug.debug import DebuggedApplication\n", 64 | "\n", 65 | "@Request.application\n", 66 | "def app(request):\n", 67 | " raise ValueError(\"testing debugger\")\n", 68 | " return Response(\"hello, world!\")\n", 69 | "\n", 70 | "app = DebuggedApplication(app, evalex=True)\n", 71 | "\n", 72 | "from werkzeug.serving import run_simple\n", 73 | "run_simple(HOST, PORT, app)" 74 | ], 75 | "language": "python", 76 | "metadata": {}, 77 | "outputs": [] 78 | }, 79 | { 80 | "cell_type": "markdown", 81 | "metadata": {}, 82 | "source": [ 83 | "After running the above, visit http://hacknode.alephpoint.com on your specified port." 84 | ] 85 | } 86 | ], 87 | "metadata": {} 88 | } 89 | ] 90 | } -------------------------------------------------------------------------------- /notebooks/02-Templates.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "name": "02-Templates" 4 | }, 5 | "nbformat": 3, 6 | "nbformat_minor": 0, 7 | "worksheets": [ 8 | { 9 | "cells": [ 10 | { 11 | "cell_type": "heading", 12 | "level": 2, 13 | "metadata": {}, 14 | "source": [ 15 | "Jinja2 Filesystem Loader" 16 | ] 17 | }, 18 | { 19 | "cell_type": "code", 20 | "collapsed": false, 21 | "input": [ 22 | "from jinja2 import Template\n", 23 | "tmpl = \"\"\"\n", 24 | "\n", 25 | " \n", 26 | " \n", 27 | " \n", 28 | " \n", 29 | " {%- for item in rows %}\n", 30 | " \n", 31 | " \n", 32 | " \n", 33 | " \n", 34 | " {%- endfor %}\n", 35 | "
NumberSquare
{{ item.number }}{{ item.square }}
\n", 36 | "\"\"\"\n", 37 | "squares = [{\"number\": number, \"square\": number*number}\n", 38 | " for number in range(5)]\n", 39 | "squares" 40 | ], 41 | "language": "python", 42 | "metadata": {}, 43 | "outputs": [] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "collapsed": false, 48 | "input": [ 49 | "print Template(tmpl).render(rows=squares)" 50 | ], 51 | "language": "python", 52 | "metadata": {}, 53 | "outputs": [] 54 | } 55 | ], 56 | "metadata": {} 57 | } 58 | ] 59 | } -------------------------------------------------------------------------------- /notebooks/03-Flask.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "name": "03-Flask" 4 | }, 5 | "nbformat": 3, 6 | "nbformat_minor": 0, 7 | "worksheets": [ 8 | { 9 | "cells": [ 10 | { 11 | "cell_type": "heading", 12 | "level": 2, 13 | "metadata": {}, 14 | "source": [ 15 | "\"Data Access Layer\"" 16 | ] 17 | }, 18 | { 19 | "cell_type": "code", 20 | "collapsed": false, 21 | "input": [ 22 | "def top_articles():\n", 23 | " return [\n", 24 | " {\"title\": \"Google\",\n", 25 | " \"link\": \"http://google.com\",\n", 26 | " \"date\": \"just now\"\n", 27 | " },\n", 28 | " {\"title\": \"Yahoo\",\n", 29 | " \"link\": \"http://yahoo.com\",\n", 30 | " \"date\": \"1 minute ago\"\n", 31 | " }\n", 32 | " ]" 33 | ], 34 | "language": "python", 35 | "metadata": {}, 36 | "outputs": [] 37 | }, 38 | { 39 | "cell_type": "heading", 40 | "level": 2, 41 | "metadata": {}, 42 | "source": [ 43 | "Write Out a Template File" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "collapsed": false, 49 | "input": [ 50 | "!mkdir -p templates" 51 | ], 52 | "language": "python", 53 | "metadata": {}, 54 | "outputs": [] 55 | }, 56 | { 57 | "cell_type": "code", 58 | "collapsed": false, 59 | "input": [ 60 | "tmpl = \"\"\"\n", 61 | "\n", 66 | "\"\"\"\n", 67 | "tmpl_file = open(\"templates/rows.jinja2.html\", \"w\")\n", 68 | "tmpl_file.write(tmpl)\n", 69 | "tmpl_file.close()" 70 | ], 71 | "language": "python", 72 | "metadata": {}, 73 | "outputs": [] 74 | }, 75 | { 76 | "cell_type": "code", 77 | "collapsed": false, 78 | "input": [ 79 | "!ls templates/" 80 | ], 81 | "language": "python", 82 | "metadata": {}, 83 | "outputs": [] 84 | }, 85 | { 86 | "cell_type": "code", 87 | "collapsed": false, 88 | "input": [ 89 | "!cat templates/rows.jinja2.html" 90 | ], 91 | "language": "python", 92 | "metadata": {}, 93 | "outputs": [] 94 | }, 95 | { 96 | "cell_type": "heading", 97 | "level": 2, 98 | "metadata": {}, 99 | "source": [ 100 | "Web App Structure" 101 | ] 102 | }, 103 | { 104 | "cell_type": "code", 105 | "collapsed": false, 106 | "input": [ 107 | "from flask import Flask, render_template\n", 108 | "\n", 109 | "app = Flask(\"app\")\n", 110 | "app.debug = True\n", 111 | "\n", 112 | "@app.route('/')\n", 113 | "def index():\n", 114 | " articles = top_articles()\n", 115 | " return render_template(\"rows.jinja2.html\",\n", 116 | " rows=articles)" 117 | ], 118 | "language": "python", 119 | "metadata": {}, 120 | "outputs": [] 121 | }, 122 | { 123 | "cell_type": "code", 124 | "collapsed": false, 125 | "input": [ 126 | "PORT=4001\n", 127 | "app.run(host=\"0.0.0.0\", port=PORT, use_reloader=False)" 128 | ], 129 | "language": "python", 130 | "metadata": {}, 131 | "outputs": [] 132 | } 133 | ], 134 | "metadata": {} 135 | } 136 | ] 137 | } -------------------------------------------------------------------------------- /notebooks/04-Contexts.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "name": "04-Contexts" 4 | }, 5 | "nbformat": 3, 6 | "nbformat_minor": 0, 7 | "worksheets": [ 8 | { 9 | "cells": [ 10 | { 11 | "cell_type": "code", 12 | "collapsed": false, 13 | "input": [ 14 | "from flask import Flask, request, session, g, Response\n", 15 | "\n", 16 | "app = Flask(\"app\")\n", 17 | "app.debug = True\n", 18 | "app.secret_key = \"d34db33f\"\n", 19 | "\n", 20 | "@app.route('/')\n", 21 | "def index():\n", 22 | " # the \"request context\"\n", 23 | " print \"request\", request\n", 24 | " # the \"session\" context\n", 25 | " if \"i\" not in session:\n", 26 | " session[\"i\"] = 0\n", 27 | " else:\n", 28 | " session[\"i\"] += 1\n", 29 | " print \"session\", session\n", 30 | " return Response(\"saved\")\n", 31 | "app.run(host=\"0.0.0.0\", port=4008, use_reloader=False) " 32 | ], 33 | "language": "python", 34 | "metadata": {}, 35 | "outputs": [] 36 | } 37 | ], 38 | "metadata": {} 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /notebooks/05-Mongo.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "name": "05-Mongo" 4 | }, 5 | "nbformat": 3, 6 | "nbformat_minor": 0, 7 | "worksheets": [ 8 | { 9 | "cells": [ 10 | { 11 | "cell_type": "code", 12 | "collapsed": false, 13 | "input": [ 14 | "from pymongo import MongoClient\n", 15 | "\n", 16 | "DB_NAME = \"hacknode1\"\n", 17 | "COLLECTION_NAME = \"articles\"\n", 18 | "\n", 19 | "client = MongoClient()\n", 20 | "db = client[DB_NAME]\n", 21 | "coll = db[COLLECTION_NAME]" 22 | ], 23 | "language": "python", 24 | "metadata": {}, 25 | "outputs": [] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "collapsed": false, 30 | "input": [ 31 | "client" 32 | ], 33 | "language": "python", 34 | "metadata": {}, 35 | "outputs": [] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "collapsed": false, 40 | "input": [ 41 | "db" 42 | ], 43 | "language": "python", 44 | "metadata": {}, 45 | "outputs": [] 46 | }, 47 | { 48 | "cell_type": "code", 49 | "collapsed": false, 50 | "input": [ 51 | "coll" 52 | ], 53 | "language": "python", 54 | "metadata": {}, 55 | "outputs": [] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "collapsed": false, 60 | "input": [ 61 | "coll.remove()" 62 | ], 63 | "language": "python", 64 | "metadata": {}, 65 | "outputs": [] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "collapsed": false, 70 | "input": [ 71 | "coll.insert({\"title\": \"Google\"})" 72 | ], 73 | "language": "python", 74 | "metadata": {}, 75 | "outputs": [] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "collapsed": false, 80 | "input": [ 81 | "coll.find()" 82 | ], 83 | "language": "python", 84 | "metadata": {}, 85 | "outputs": [] 86 | }, 87 | { 88 | "cell_type": "code", 89 | "collapsed": false, 90 | "input": [ 91 | "list(coll.find())" 92 | ], 93 | "language": "python", 94 | "metadata": {}, 95 | "outputs": [] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "collapsed": false, 100 | "input": [ 101 | "coll.remove()" 102 | ], 103 | "language": "python", 104 | "metadata": {}, 105 | "outputs": [] 106 | }, 107 | { 108 | "cell_type": "code", 109 | "collapsed": false, 110 | "input": [ 111 | "coll.insert({\"title\": \"Google\", \"score\": 0})" 112 | ], 113 | "language": "python", 114 | "metadata": {}, 115 | "outputs": [] 116 | }, 117 | { 118 | "cell_type": "code", 119 | "collapsed": false, 120 | "input": [ 121 | "coll.update({\"title\": \"Google\"}, {\"$inc\": {\"score\": 1}})" 122 | ], 123 | "language": "python", 124 | "metadata": {}, 125 | "outputs": [] 126 | }, 127 | { 128 | "cell_type": "code", 129 | "collapsed": false, 130 | "input": [ 131 | "coll.find_one()" 132 | ], 133 | "language": "python", 134 | "metadata": {}, 135 | "outputs": [] 136 | }, 137 | { 138 | "cell_type": "code", 139 | "collapsed": false, 140 | "input": [ 141 | "coll.update({\"title\": \"Google\"}, {\"$inc\": {\"score\": 1}})" 142 | ], 143 | "language": "python", 144 | "metadata": {}, 145 | "outputs": [] 146 | }, 147 | { 148 | "cell_type": "code", 149 | "collapsed": false, 150 | "input": [ 151 | "coll.find_one()" 152 | ], 153 | "language": "python", 154 | "metadata": {}, 155 | "outputs": [] 156 | }, 157 | { 158 | "cell_type": "code", 159 | "collapsed": false, 160 | "input": [ 161 | "doc = coll.find_one()" 162 | ], 163 | "language": "python", 164 | "metadata": {}, 165 | "outputs": [] 166 | }, 167 | { 168 | "cell_type": "code", 169 | "collapsed": false, 170 | "input": [ 171 | "print doc[\"score\"], doc[\"title\"]" 172 | ], 173 | "language": "python", 174 | "metadata": {}, 175 | "outputs": [] 176 | }, 177 | { 178 | "cell_type": "code", 179 | "collapsed": false, 180 | "input": [ 181 | "for i in range(1, 4):\n", 182 | " coll.insert({\"title\": \"Article {}\".format(i), \"score\": i})\n", 183 | "coll.count()" 184 | ], 185 | "language": "python", 186 | "metadata": {}, 187 | "outputs": [] 188 | }, 189 | { 190 | "cell_type": "code", 191 | "collapsed": false, 192 | "input": [ 193 | "for doc in coll.find():\n", 194 | " print doc[\"title\"]" 195 | ], 196 | "language": "python", 197 | "metadata": {}, 198 | "outputs": [] 199 | }, 200 | { 201 | "cell_type": "code", 202 | "collapsed": false, 203 | "input": [ 204 | "coll.remove()" 205 | ], 206 | "language": "python", 207 | "metadata": {}, 208 | "outputs": [] 209 | }, 210 | { 211 | "cell_type": "code", 212 | "collapsed": false, 213 | "input": [ 214 | "coll.count()" 215 | ], 216 | "language": "python", 217 | "metadata": {}, 218 | "outputs": [] 219 | } 220 | ], 221 | "metadata": {} 222 | } 223 | ] 224 | } -------------------------------------------------------------------------------- /notebooks/serve_notebooks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | [[ -z "$1" ]] && echo "must specify port" && exit 1 3 | # requires an activation script in directory above 4 | source ../activate 5 | # makes a sandbox folder 6 | mkdir -p $1 7 | # copies notebooks over 8 | cp *.ipynb $1/ 9 | # goes into it 10 | cd $1 11 | # runs ipython notebook on a public IP w/ specified port 12 | ipython notebook --ip=* --port=$1 --pprint --browser=/bin/true 13 | -------------------------------------------------------------------------------- /prod-requirements.txt: -------------------------------------------------------------------------------- 1 | pymongo 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ipython 2 | Flask 3 | Flask-Script 4 | -------------------------------------------------------------------------------- /serve.sh: -------------------------------------------------------------------------------- 1 | cd static 2 | livereload -p 8000 3 | -------------------------------------------------------------------------------- /static/css/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amontalenti/rapid-web/719063e5099f649465edeb747ea7ca38eb06cb90/static/css/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /static/css/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amontalenti/rapid-web/719063e5099f649465edeb747ea7ca38eb06cb90/static/css/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /static/css/lib/bootstrap-responsive.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.2.2 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */ 10 | 11 | @-ms-viewport { 12 | width: device-width; 13 | } 14 | 15 | .clearfix { 16 | *zoom: 1; 17 | } 18 | 19 | .clearfix:before, 20 | .clearfix:after { 21 | display: table; 22 | line-height: 0; 23 | content: ""; 24 | } 25 | 26 | .clearfix:after { 27 | clear: both; 28 | } 29 | 30 | .hide-text { 31 | font: 0/0 a; 32 | color: transparent; 33 | text-shadow: none; 34 | background-color: transparent; 35 | border: 0; 36 | } 37 | 38 | .input-block-level { 39 | display: block; 40 | width: 100%; 41 | min-height: 30px; 42 | -webkit-box-sizing: border-box; 43 | -moz-box-sizing: border-box; 44 | box-sizing: border-box; 45 | } 46 | 47 | .hidden { 48 | display: none; 49 | visibility: hidden; 50 | } 51 | 52 | .visible-phone { 53 | display: none !important; 54 | } 55 | 56 | .visible-tablet { 57 | display: none !important; 58 | } 59 | 60 | .hidden-desktop { 61 | display: none !important; 62 | } 63 | 64 | .visible-desktop { 65 | display: inherit !important; 66 | } 67 | 68 | @media (min-width: 768px) and (max-width: 979px) { 69 | .hidden-desktop { 70 | display: inherit !important; 71 | } 72 | .visible-desktop { 73 | display: none !important ; 74 | } 75 | .visible-tablet { 76 | display: inherit !important; 77 | } 78 | .hidden-tablet { 79 | display: none !important; 80 | } 81 | } 82 | 83 | @media (max-width: 767px) { 84 | .hidden-desktop { 85 | display: inherit !important; 86 | } 87 | .visible-desktop { 88 | display: none !important; 89 | } 90 | .visible-phone { 91 | display: inherit !important; 92 | } 93 | .hidden-phone { 94 | display: none !important; 95 | } 96 | } 97 | 98 | @media (min-width: 1200px) { 99 | .row { 100 | margin-left: -30px; 101 | *zoom: 1; 102 | } 103 | .row:before, 104 | .row:after { 105 | display: table; 106 | line-height: 0; 107 | content: ""; 108 | } 109 | .row:after { 110 | clear: both; 111 | } 112 | [class*="span"] { 113 | float: left; 114 | min-height: 1px; 115 | margin-left: 30px; 116 | } 117 | .container, 118 | .navbar-static-top .container, 119 | .navbar-fixed-top .container, 120 | .navbar-fixed-bottom .container { 121 | width: 1170px; 122 | } 123 | .span12 { 124 | width: 1170px; 125 | } 126 | .span11 { 127 | width: 1070px; 128 | } 129 | .span10 { 130 | width: 970px; 131 | } 132 | .span9 { 133 | width: 870px; 134 | } 135 | .span8 { 136 | width: 770px; 137 | } 138 | .span7 { 139 | width: 670px; 140 | } 141 | .span6 { 142 | width: 570px; 143 | } 144 | .span5 { 145 | width: 470px; 146 | } 147 | .span4 { 148 | width: 370px; 149 | } 150 | .span3 { 151 | width: 270px; 152 | } 153 | .span2 { 154 | width: 170px; 155 | } 156 | .span1 { 157 | width: 70px; 158 | } 159 | .offset12 { 160 | margin-left: 1230px; 161 | } 162 | .offset11 { 163 | margin-left: 1130px; 164 | } 165 | .offset10 { 166 | margin-left: 1030px; 167 | } 168 | .offset9 { 169 | margin-left: 930px; 170 | } 171 | .offset8 { 172 | margin-left: 830px; 173 | } 174 | .offset7 { 175 | margin-left: 730px; 176 | } 177 | .offset6 { 178 | margin-left: 630px; 179 | } 180 | .offset5 { 181 | margin-left: 530px; 182 | } 183 | .offset4 { 184 | margin-left: 430px; 185 | } 186 | .offset3 { 187 | margin-left: 330px; 188 | } 189 | .offset2 { 190 | margin-left: 230px; 191 | } 192 | .offset1 { 193 | margin-left: 130px; 194 | } 195 | .row-fluid { 196 | width: 100%; 197 | *zoom: 1; 198 | } 199 | .row-fluid:before, 200 | .row-fluid:after { 201 | display: table; 202 | line-height: 0; 203 | content: ""; 204 | } 205 | .row-fluid:after { 206 | clear: both; 207 | } 208 | .row-fluid [class*="span"] { 209 | display: block; 210 | float: left; 211 | width: 100%; 212 | min-height: 30px; 213 | margin-left: 2.564102564102564%; 214 | *margin-left: 2.5109110747408616%; 215 | -webkit-box-sizing: border-box; 216 | -moz-box-sizing: border-box; 217 | box-sizing: border-box; 218 | } 219 | .row-fluid [class*="span"]:first-child { 220 | margin-left: 0; 221 | } 222 | .row-fluid .controls-row [class*="span"] + [class*="span"] { 223 | margin-left: 2.564102564102564%; 224 | } 225 | .row-fluid .span12 { 226 | width: 100%; 227 | *width: 99.94680851063829%; 228 | } 229 | .row-fluid .span11 { 230 | width: 91.45299145299145%; 231 | *width: 91.39979996362975%; 232 | } 233 | .row-fluid .span10 { 234 | width: 82.90598290598291%; 235 | *width: 82.8527914166212%; 236 | } 237 | .row-fluid .span9 { 238 | width: 74.35897435897436%; 239 | *width: 74.30578286961266%; 240 | } 241 | .row-fluid .span8 { 242 | width: 65.81196581196582%; 243 | *width: 65.75877432260411%; 244 | } 245 | .row-fluid .span7 { 246 | width: 57.26495726495726%; 247 | *width: 57.21176577559556%; 248 | } 249 | .row-fluid .span6 { 250 | width: 48.717948717948715%; 251 | *width: 48.664757228587014%; 252 | } 253 | .row-fluid .span5 { 254 | width: 40.17094017094017%; 255 | *width: 40.11774868157847%; 256 | } 257 | .row-fluid .span4 { 258 | width: 31.623931623931625%; 259 | *width: 31.570740134569924%; 260 | } 261 | .row-fluid .span3 { 262 | width: 23.076923076923077%; 263 | *width: 23.023731587561375%; 264 | } 265 | .row-fluid .span2 { 266 | width: 14.52991452991453%; 267 | *width: 14.476723040552828%; 268 | } 269 | .row-fluid .span1 { 270 | width: 5.982905982905983%; 271 | *width: 5.929714493544281%; 272 | } 273 | .row-fluid .offset12 { 274 | margin-left: 105.12820512820512%; 275 | *margin-left: 105.02182214948171%; 276 | } 277 | .row-fluid .offset12:first-child { 278 | margin-left: 102.56410256410257%; 279 | *margin-left: 102.45771958537915%; 280 | } 281 | .row-fluid .offset11 { 282 | margin-left: 96.58119658119658%; 283 | *margin-left: 96.47481360247316%; 284 | } 285 | .row-fluid .offset11:first-child { 286 | margin-left: 94.01709401709402%; 287 | *margin-left: 93.91071103837061%; 288 | } 289 | .row-fluid .offset10 { 290 | margin-left: 88.03418803418803%; 291 | *margin-left: 87.92780505546462%; 292 | } 293 | .row-fluid .offset10:first-child { 294 | margin-left: 85.47008547008548%; 295 | *margin-left: 85.36370249136206%; 296 | } 297 | .row-fluid .offset9 { 298 | margin-left: 79.48717948717949%; 299 | *margin-left: 79.38079650845607%; 300 | } 301 | .row-fluid .offset9:first-child { 302 | margin-left: 76.92307692307693%; 303 | *margin-left: 76.81669394435352%; 304 | } 305 | .row-fluid .offset8 { 306 | margin-left: 70.94017094017094%; 307 | *margin-left: 70.83378796144753%; 308 | } 309 | .row-fluid .offset8:first-child { 310 | margin-left: 68.37606837606839%; 311 | *margin-left: 68.26968539734497%; 312 | } 313 | .row-fluid .offset7 { 314 | margin-left: 62.393162393162385%; 315 | *margin-left: 62.28677941443899%; 316 | } 317 | .row-fluid .offset7:first-child { 318 | margin-left: 59.82905982905982%; 319 | *margin-left: 59.72267685033642%; 320 | } 321 | .row-fluid .offset6 { 322 | margin-left: 53.84615384615384%; 323 | *margin-left: 53.739770867430444%; 324 | } 325 | .row-fluid .offset6:first-child { 326 | margin-left: 51.28205128205128%; 327 | *margin-left: 51.175668303327875%; 328 | } 329 | .row-fluid .offset5 { 330 | margin-left: 45.299145299145295%; 331 | *margin-left: 45.1927623204219%; 332 | } 333 | .row-fluid .offset5:first-child { 334 | margin-left: 42.73504273504273%; 335 | *margin-left: 42.62865975631933%; 336 | } 337 | .row-fluid .offset4 { 338 | margin-left: 36.75213675213675%; 339 | *margin-left: 36.645753773413354%; 340 | } 341 | .row-fluid .offset4:first-child { 342 | margin-left: 34.18803418803419%; 343 | *margin-left: 34.081651209310785%; 344 | } 345 | .row-fluid .offset3 { 346 | margin-left: 28.205128205128204%; 347 | *margin-left: 28.0987452264048%; 348 | } 349 | .row-fluid .offset3:first-child { 350 | margin-left: 25.641025641025642%; 351 | *margin-left: 25.53464266230224%; 352 | } 353 | .row-fluid .offset2 { 354 | margin-left: 19.65811965811966%; 355 | *margin-left: 19.551736679396257%; 356 | } 357 | .row-fluid .offset2:first-child { 358 | margin-left: 17.094017094017094%; 359 | *margin-left: 16.98763411529369%; 360 | } 361 | .row-fluid .offset1 { 362 | margin-left: 11.11111111111111%; 363 | *margin-left: 11.004728132387708%; 364 | } 365 | .row-fluid .offset1:first-child { 366 | margin-left: 8.547008547008547%; 367 | *margin-left: 8.440625568285142%; 368 | } 369 | input, 370 | textarea, 371 | .uneditable-input { 372 | margin-left: 0; 373 | } 374 | .controls-row [class*="span"] + [class*="span"] { 375 | margin-left: 30px; 376 | } 377 | input.span12, 378 | textarea.span12, 379 | .uneditable-input.span12 { 380 | width: 1156px; 381 | } 382 | input.span11, 383 | textarea.span11, 384 | .uneditable-input.span11 { 385 | width: 1056px; 386 | } 387 | input.span10, 388 | textarea.span10, 389 | .uneditable-input.span10 { 390 | width: 956px; 391 | } 392 | input.span9, 393 | textarea.span9, 394 | .uneditable-input.span9 { 395 | width: 856px; 396 | } 397 | input.span8, 398 | textarea.span8, 399 | .uneditable-input.span8 { 400 | width: 756px; 401 | } 402 | input.span7, 403 | textarea.span7, 404 | .uneditable-input.span7 { 405 | width: 656px; 406 | } 407 | input.span6, 408 | textarea.span6, 409 | .uneditable-input.span6 { 410 | width: 556px; 411 | } 412 | input.span5, 413 | textarea.span5, 414 | .uneditable-input.span5 { 415 | width: 456px; 416 | } 417 | input.span4, 418 | textarea.span4, 419 | .uneditable-input.span4 { 420 | width: 356px; 421 | } 422 | input.span3, 423 | textarea.span3, 424 | .uneditable-input.span3 { 425 | width: 256px; 426 | } 427 | input.span2, 428 | textarea.span2, 429 | .uneditable-input.span2 { 430 | width: 156px; 431 | } 432 | input.span1, 433 | textarea.span1, 434 | .uneditable-input.span1 { 435 | width: 56px; 436 | } 437 | .thumbnails { 438 | margin-left: -30px; 439 | } 440 | .thumbnails > li { 441 | margin-left: 30px; 442 | } 443 | .row-fluid .thumbnails { 444 | margin-left: 0; 445 | } 446 | } 447 | 448 | @media (min-width: 768px) and (max-width: 979px) { 449 | .row { 450 | margin-left: -20px; 451 | *zoom: 1; 452 | } 453 | .row:before, 454 | .row:after { 455 | display: table; 456 | line-height: 0; 457 | content: ""; 458 | } 459 | .row:after { 460 | clear: both; 461 | } 462 | [class*="span"] { 463 | float: left; 464 | min-height: 1px; 465 | margin-left: 20px; 466 | } 467 | .container, 468 | .navbar-static-top .container, 469 | .navbar-fixed-top .container, 470 | .navbar-fixed-bottom .container { 471 | width: 724px; 472 | } 473 | .span12 { 474 | width: 724px; 475 | } 476 | .span11 { 477 | width: 662px; 478 | } 479 | .span10 { 480 | width: 600px; 481 | } 482 | .span9 { 483 | width: 538px; 484 | } 485 | .span8 { 486 | width: 476px; 487 | } 488 | .span7 { 489 | width: 414px; 490 | } 491 | .span6 { 492 | width: 352px; 493 | } 494 | .span5 { 495 | width: 290px; 496 | } 497 | .span4 { 498 | width: 228px; 499 | } 500 | .span3 { 501 | width: 166px; 502 | } 503 | .span2 { 504 | width: 104px; 505 | } 506 | .span1 { 507 | width: 42px; 508 | } 509 | .offset12 { 510 | margin-left: 764px; 511 | } 512 | .offset11 { 513 | margin-left: 702px; 514 | } 515 | .offset10 { 516 | margin-left: 640px; 517 | } 518 | .offset9 { 519 | margin-left: 578px; 520 | } 521 | .offset8 { 522 | margin-left: 516px; 523 | } 524 | .offset7 { 525 | margin-left: 454px; 526 | } 527 | .offset6 { 528 | margin-left: 392px; 529 | } 530 | .offset5 { 531 | margin-left: 330px; 532 | } 533 | .offset4 { 534 | margin-left: 268px; 535 | } 536 | .offset3 { 537 | margin-left: 206px; 538 | } 539 | .offset2 { 540 | margin-left: 144px; 541 | } 542 | .offset1 { 543 | margin-left: 82px; 544 | } 545 | .row-fluid { 546 | width: 100%; 547 | *zoom: 1; 548 | } 549 | .row-fluid:before, 550 | .row-fluid:after { 551 | display: table; 552 | line-height: 0; 553 | content: ""; 554 | } 555 | .row-fluid:after { 556 | clear: both; 557 | } 558 | .row-fluid [class*="span"] { 559 | display: block; 560 | float: left; 561 | width: 100%; 562 | min-height: 30px; 563 | margin-left: 2.7624309392265194%; 564 | *margin-left: 2.709239449864817%; 565 | -webkit-box-sizing: border-box; 566 | -moz-box-sizing: border-box; 567 | box-sizing: border-box; 568 | } 569 | .row-fluid [class*="span"]:first-child { 570 | margin-left: 0; 571 | } 572 | .row-fluid .controls-row [class*="span"] + [class*="span"] { 573 | margin-left: 2.7624309392265194%; 574 | } 575 | .row-fluid .span12 { 576 | width: 100%; 577 | *width: 99.94680851063829%; 578 | } 579 | .row-fluid .span11 { 580 | width: 91.43646408839778%; 581 | *width: 91.38327259903608%; 582 | } 583 | .row-fluid .span10 { 584 | width: 82.87292817679558%; 585 | *width: 82.81973668743387%; 586 | } 587 | .row-fluid .span9 { 588 | width: 74.30939226519337%; 589 | *width: 74.25620077583166%; 590 | } 591 | .row-fluid .span8 { 592 | width: 65.74585635359117%; 593 | *width: 65.69266486422946%; 594 | } 595 | .row-fluid .span7 { 596 | width: 57.18232044198895%; 597 | *width: 57.12912895262725%; 598 | } 599 | .row-fluid .span6 { 600 | width: 48.61878453038674%; 601 | *width: 48.56559304102504%; 602 | } 603 | .row-fluid .span5 { 604 | width: 40.05524861878453%; 605 | *width: 40.00205712942283%; 606 | } 607 | .row-fluid .span4 { 608 | width: 31.491712707182323%; 609 | *width: 31.43852121782062%; 610 | } 611 | .row-fluid .span3 { 612 | width: 22.92817679558011%; 613 | *width: 22.87498530621841%; 614 | } 615 | .row-fluid .span2 { 616 | width: 14.3646408839779%; 617 | *width: 14.311449394616199%; 618 | } 619 | .row-fluid .span1 { 620 | width: 5.801104972375691%; 621 | *width: 5.747913483013988%; 622 | } 623 | .row-fluid .offset12 { 624 | margin-left: 105.52486187845304%; 625 | *margin-left: 105.41847889972962%; 626 | } 627 | .row-fluid .offset12:first-child { 628 | margin-left: 102.76243093922652%; 629 | *margin-left: 102.6560479605031%; 630 | } 631 | .row-fluid .offset11 { 632 | margin-left: 96.96132596685082%; 633 | *margin-left: 96.8549429881274%; 634 | } 635 | .row-fluid .offset11:first-child { 636 | margin-left: 94.1988950276243%; 637 | *margin-left: 94.09251204890089%; 638 | } 639 | .row-fluid .offset10 { 640 | margin-left: 88.39779005524862%; 641 | *margin-left: 88.2914070765252%; 642 | } 643 | .row-fluid .offset10:first-child { 644 | margin-left: 85.6353591160221%; 645 | *margin-left: 85.52897613729868%; 646 | } 647 | .row-fluid .offset9 { 648 | margin-left: 79.8342541436464%; 649 | *margin-left: 79.72787116492299%; 650 | } 651 | .row-fluid .offset9:first-child { 652 | margin-left: 77.07182320441989%; 653 | *margin-left: 76.96544022569647%; 654 | } 655 | .row-fluid .offset8 { 656 | margin-left: 71.2707182320442%; 657 | *margin-left: 71.16433525332079%; 658 | } 659 | .row-fluid .offset8:first-child { 660 | margin-left: 68.50828729281768%; 661 | *margin-left: 68.40190431409427%; 662 | } 663 | .row-fluid .offset7 { 664 | margin-left: 62.70718232044199%; 665 | *margin-left: 62.600799341718584%; 666 | } 667 | .row-fluid .offset7:first-child { 668 | margin-left: 59.94475138121547%; 669 | *margin-left: 59.838368402492065%; 670 | } 671 | .row-fluid .offset6 { 672 | margin-left: 54.14364640883978%; 673 | *margin-left: 54.037263430116376%; 674 | } 675 | .row-fluid .offset6:first-child { 676 | margin-left: 51.38121546961326%; 677 | *margin-left: 51.27483249088986%; 678 | } 679 | .row-fluid .offset5 { 680 | margin-left: 45.58011049723757%; 681 | *margin-left: 45.47372751851417%; 682 | } 683 | .row-fluid .offset5:first-child { 684 | margin-left: 42.81767955801105%; 685 | *margin-left: 42.71129657928765%; 686 | } 687 | .row-fluid .offset4 { 688 | margin-left: 37.01657458563536%; 689 | *margin-left: 36.91019160691196%; 690 | } 691 | .row-fluid .offset4:first-child { 692 | margin-left: 34.25414364640884%; 693 | *margin-left: 34.14776066768544%; 694 | } 695 | .row-fluid .offset3 { 696 | margin-left: 28.45303867403315%; 697 | *margin-left: 28.346655695309746%; 698 | } 699 | .row-fluid .offset3:first-child { 700 | margin-left: 25.69060773480663%; 701 | *margin-left: 25.584224756083227%; 702 | } 703 | .row-fluid .offset2 { 704 | margin-left: 19.88950276243094%; 705 | *margin-left: 19.783119783707537%; 706 | } 707 | .row-fluid .offset2:first-child { 708 | margin-left: 17.12707182320442%; 709 | *margin-left: 17.02068884448102%; 710 | } 711 | .row-fluid .offset1 { 712 | margin-left: 11.32596685082873%; 713 | *margin-left: 11.219583872105325%; 714 | } 715 | .row-fluid .offset1:first-child { 716 | margin-left: 8.56353591160221%; 717 | *margin-left: 8.457152932878806%; 718 | } 719 | input, 720 | textarea, 721 | .uneditable-input { 722 | margin-left: 0; 723 | } 724 | .controls-row [class*="span"] + [class*="span"] { 725 | margin-left: 20px; 726 | } 727 | input.span12, 728 | textarea.span12, 729 | .uneditable-input.span12 { 730 | width: 710px; 731 | } 732 | input.span11, 733 | textarea.span11, 734 | .uneditable-input.span11 { 735 | width: 648px; 736 | } 737 | input.span10, 738 | textarea.span10, 739 | .uneditable-input.span10 { 740 | width: 586px; 741 | } 742 | input.span9, 743 | textarea.span9, 744 | .uneditable-input.span9 { 745 | width: 524px; 746 | } 747 | input.span8, 748 | textarea.span8, 749 | .uneditable-input.span8 { 750 | width: 462px; 751 | } 752 | input.span7, 753 | textarea.span7, 754 | .uneditable-input.span7 { 755 | width: 400px; 756 | } 757 | input.span6, 758 | textarea.span6, 759 | .uneditable-input.span6 { 760 | width: 338px; 761 | } 762 | input.span5, 763 | textarea.span5, 764 | .uneditable-input.span5 { 765 | width: 276px; 766 | } 767 | input.span4, 768 | textarea.span4, 769 | .uneditable-input.span4 { 770 | width: 214px; 771 | } 772 | input.span3, 773 | textarea.span3, 774 | .uneditable-input.span3 { 775 | width: 152px; 776 | } 777 | input.span2, 778 | textarea.span2, 779 | .uneditable-input.span2 { 780 | width: 90px; 781 | } 782 | input.span1, 783 | textarea.span1, 784 | .uneditable-input.span1 { 785 | width: 28px; 786 | } 787 | } 788 | 789 | @media (max-width: 767px) { 790 | body { 791 | padding-right: 20px; 792 | padding-left: 20px; 793 | } 794 | .navbar-fixed-top, 795 | .navbar-fixed-bottom, 796 | .navbar-static-top { 797 | margin-right: -20px; 798 | margin-left: -20px; 799 | } 800 | .container-fluid { 801 | padding: 0; 802 | } 803 | .dl-horizontal dt { 804 | float: none; 805 | width: auto; 806 | clear: none; 807 | text-align: left; 808 | } 809 | .dl-horizontal dd { 810 | margin-left: 0; 811 | } 812 | .container { 813 | width: auto; 814 | } 815 | .row-fluid { 816 | width: 100%; 817 | } 818 | .row, 819 | .thumbnails { 820 | margin-left: 0; 821 | } 822 | .thumbnails > li { 823 | float: none; 824 | margin-left: 0; 825 | } 826 | [class*="span"], 827 | .uneditable-input[class*="span"], 828 | .row-fluid [class*="span"] { 829 | display: block; 830 | float: none; 831 | width: 100%; 832 | margin-left: 0; 833 | -webkit-box-sizing: border-box; 834 | -moz-box-sizing: border-box; 835 | box-sizing: border-box; 836 | } 837 | .span12, 838 | .row-fluid .span12 { 839 | width: 100%; 840 | -webkit-box-sizing: border-box; 841 | -moz-box-sizing: border-box; 842 | box-sizing: border-box; 843 | } 844 | .row-fluid [class*="offset"]:first-child { 845 | margin-left: 0; 846 | } 847 | .input-large, 848 | .input-xlarge, 849 | .input-xxlarge, 850 | input[class*="span"], 851 | select[class*="span"], 852 | textarea[class*="span"], 853 | .uneditable-input { 854 | display: block; 855 | width: 100%; 856 | min-height: 30px; 857 | -webkit-box-sizing: border-box; 858 | -moz-box-sizing: border-box; 859 | box-sizing: border-box; 860 | } 861 | .input-prepend input, 862 | .input-append input, 863 | .input-prepend input[class*="span"], 864 | .input-append input[class*="span"] { 865 | display: inline-block; 866 | width: auto; 867 | } 868 | .controls-row [class*="span"] + [class*="span"] { 869 | margin-left: 0; 870 | } 871 | .modal { 872 | position: fixed; 873 | top: 20px; 874 | right: 20px; 875 | left: 20px; 876 | width: auto; 877 | margin: 0; 878 | } 879 | .modal.fade { 880 | top: -100px; 881 | } 882 | .modal.fade.in { 883 | top: 20px; 884 | } 885 | } 886 | 887 | @media (max-width: 480px) { 888 | .nav-collapse { 889 | -webkit-transform: translate3d(0, 0, 0); 890 | } 891 | .page-header h1 small { 892 | display: block; 893 | line-height: 20px; 894 | } 895 | input[type="checkbox"], 896 | input[type="radio"] { 897 | border: 1px solid #ccc; 898 | } 899 | .form-horizontal .control-label { 900 | float: none; 901 | width: auto; 902 | padding-top: 0; 903 | text-align: left; 904 | } 905 | .form-horizontal .controls { 906 | margin-left: 0; 907 | } 908 | .form-horizontal .control-list { 909 | padding-top: 0; 910 | } 911 | .form-horizontal .form-actions { 912 | padding-right: 10px; 913 | padding-left: 10px; 914 | } 915 | .media .pull-left, 916 | .media .pull-right { 917 | display: block; 918 | float: none; 919 | margin-bottom: 10px; 920 | } 921 | .media-object { 922 | margin-right: 0; 923 | margin-left: 0; 924 | } 925 | .modal { 926 | top: 10px; 927 | right: 10px; 928 | left: 10px; 929 | } 930 | .modal-header .close { 931 | padding: 10px; 932 | margin: -10px; 933 | } 934 | .carousel-caption { 935 | position: static; 936 | } 937 | } 938 | 939 | @media (max-width: 979px) { 940 | body { 941 | padding-top: 0; 942 | } 943 | .navbar-fixed-top, 944 | .navbar-fixed-bottom { 945 | position: static; 946 | } 947 | .navbar-fixed-top { 948 | margin-bottom: 20px; 949 | } 950 | .navbar-fixed-bottom { 951 | margin-top: 20px; 952 | } 953 | .navbar-fixed-top .navbar-inner, 954 | .navbar-fixed-bottom .navbar-inner { 955 | padding: 5px; 956 | } 957 | .navbar .container { 958 | width: auto; 959 | padding: 0; 960 | } 961 | .navbar .brand { 962 | padding-right: 10px; 963 | padding-left: 10px; 964 | margin: 0 0 0 -5px; 965 | } 966 | .nav-collapse { 967 | clear: both; 968 | } 969 | .nav-collapse .nav { 970 | float: none; 971 | margin: 0 0 10px; 972 | } 973 | .nav-collapse .nav > li { 974 | float: none; 975 | } 976 | .nav-collapse .nav > li > a { 977 | margin-bottom: 2px; 978 | } 979 | .nav-collapse .nav > .divider-vertical { 980 | display: none; 981 | } 982 | .nav-collapse .nav .nav-header { 983 | color: #777777; 984 | text-shadow: none; 985 | } 986 | .nav-collapse .nav > li > a, 987 | .nav-collapse .dropdown-menu a { 988 | padding: 9px 15px; 989 | font-weight: bold; 990 | color: #777777; 991 | -webkit-border-radius: 3px; 992 | -moz-border-radius: 3px; 993 | border-radius: 3px; 994 | } 995 | .nav-collapse .btn { 996 | padding: 4px 10px 4px; 997 | font-weight: normal; 998 | -webkit-border-radius: 4px; 999 | -moz-border-radius: 4px; 1000 | border-radius: 4px; 1001 | } 1002 | .nav-collapse .dropdown-menu li + li a { 1003 | margin-bottom: 2px; 1004 | } 1005 | .nav-collapse .nav > li > a:hover, 1006 | .nav-collapse .dropdown-menu a:hover { 1007 | background-color: #f2f2f2; 1008 | } 1009 | .navbar-inverse .nav-collapse .nav > li > a, 1010 | .navbar-inverse .nav-collapse .dropdown-menu a { 1011 | color: #999999; 1012 | } 1013 | .navbar-inverse .nav-collapse .nav > li > a:hover, 1014 | .navbar-inverse .nav-collapse .dropdown-menu a:hover { 1015 | background-color: #111111; 1016 | } 1017 | .nav-collapse.in .btn-group { 1018 | padding: 0; 1019 | margin-top: 5px; 1020 | } 1021 | .nav-collapse .dropdown-menu { 1022 | position: static; 1023 | top: auto; 1024 | left: auto; 1025 | display: none; 1026 | float: none; 1027 | max-width: none; 1028 | padding: 0; 1029 | margin: 0 15px; 1030 | background-color: transparent; 1031 | border: none; 1032 | -webkit-border-radius: 0; 1033 | -moz-border-radius: 0; 1034 | border-radius: 0; 1035 | -webkit-box-shadow: none; 1036 | -moz-box-shadow: none; 1037 | box-shadow: none; 1038 | } 1039 | .nav-collapse .open > .dropdown-menu { 1040 | display: block; 1041 | } 1042 | .nav-collapse .dropdown-menu:before, 1043 | .nav-collapse .dropdown-menu:after { 1044 | display: none; 1045 | } 1046 | .nav-collapse .dropdown-menu .divider { 1047 | display: none; 1048 | } 1049 | .nav-collapse .nav > li > .dropdown-menu:before, 1050 | .nav-collapse .nav > li > .dropdown-menu:after { 1051 | display: none; 1052 | } 1053 | .nav-collapse .navbar-form, 1054 | .nav-collapse .navbar-search { 1055 | float: none; 1056 | padding: 10px 15px; 1057 | margin: 10px 0; 1058 | border-top: 1px solid #f2f2f2; 1059 | border-bottom: 1px solid #f2f2f2; 1060 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 1061 | -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 1062 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 1063 | } 1064 | .navbar-inverse .nav-collapse .navbar-form, 1065 | .navbar-inverse .nav-collapse .navbar-search { 1066 | border-top-color: #111111; 1067 | border-bottom-color: #111111; 1068 | } 1069 | .navbar .nav-collapse .nav.pull-right { 1070 | float: none; 1071 | margin-left: 0; 1072 | } 1073 | .nav-collapse, 1074 | .nav-collapse.collapse { 1075 | height: 0; 1076 | overflow: hidden; 1077 | } 1078 | .navbar .btn-navbar { 1079 | display: block; 1080 | } 1081 | .navbar-static .navbar-inner { 1082 | padding-right: 10px; 1083 | padding-left: 10px; 1084 | } 1085 | } 1086 | 1087 | @media (min-width: 980px) { 1088 | .nav-collapse.collapse { 1089 | height: auto !important; 1090 | overflow: visible !important; 1091 | } 1092 | } 1093 | -------------------------------------------------------------------------------- /static/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 20px; 3 | padding-bottom: 20px; 4 | } 5 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Rapid News Static 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 30 | 31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 |
ScoreLinkPublished
150The Meditations of Marcus Aurelius3 hours ago
123Under Their Feet1 day ago
82Segway inventor's next endeavor2 days ago
32The Next Supermodel3 days ago
28The popularity of Python programming4 days ago
14Snowfall Interactive Feature2 weeks ago
12Drowning in a sea of prescriptions1 month ago
77 | 78 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /static/js/animation.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | function animateRows() { 3 | // simple animation to fade in all but the top story 4 | var rowNum = 1; 5 | var dur = 500; 6 | $("tbody tr").each(function() { 7 | if (rowNum === 0) { 8 | // skip 1st row 9 | return; 10 | } 11 | // capture current row 12 | var elm = $(this); 13 | // schedule it to fade in 14 | setTimeout(function() { 15 | elm.fadeIn(); 16 | }, dur); 17 | dur += 500; 18 | rowNum += 1; 19 | }); 20 | }; 21 | 22 | RAPID.animateRows = animateRows; 23 | })(); 24 | -------------------------------------------------------------------------------- /static/js/cookies.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | function createCookie(name,value,days) { 4 | if (days) { 5 | var date = new Date(); 6 | date.setTime(date.getTime()+(days*24*60*60*1000)); 7 | var expires = "; expires="+date.toGMTString(); 8 | } 9 | else var expires = ""; 10 | document.cookie = name+"="+value+expires+"; path=/"; 11 | }; 12 | 13 | function readCookie(name) { 14 | var nameEQ = name + "="; 15 | var ca = document.cookie.split(';'); 16 | for(var i=0;i < ca.length;i++) { 17 | var c = ca[i]; 18 | while (c.charAt(0)==' ') c = c.substring(1,c.length); 19 | if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length); 20 | } 21 | return null; 22 | }; 23 | 24 | function eraseCookie(name) { 25 | createCookie(name,"",-1); 26 | }; 27 | 28 | RAPID.createCookie = createCookie; 29 | RAPID.readCookie = readCookie; 30 | RAPID.eraseCookie = eraseCookie; 31 | })(); 32 | -------------------------------------------------------------------------------- /static/js/firstuser.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | function showFirstVisitDialog() { 3 | var cookie = RAPID.readCookie("visited"); 4 | if (cookie === "true") { 5 | // do nothing, user has visited before 6 | return; 7 | } 8 | var modal = $("#first-visit-dialog"); 9 | modal.on("hide", function() { 10 | RAPID.createCookie("visited", "true", 30); 11 | }); 12 | modal.modal(); 13 | }; 14 | 15 | function clearFirstVisit() { 16 | RAPID.eraseCookie("visited"); 17 | }; 18 | 19 | RAPID.showFirstVisitDialog = showFirstVisitDialog; 20 | RAPID.clearFirstVisit = clearFirstVisit; 21 | })(); 22 | -------------------------------------------------------------------------------- /static/js/hnfake.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | function hackernewsFetch(afterFetch) { 3 | var apiroot = "http://hndroidapi.appspot.com"; 4 | var path = "/best/format/json/page/"; 5 | var params = "?appid=RAPID&callback=?"; 6 | var url = [apiroot, path, params].join(""); 7 | 8 | // disable HNFake for now! 9 | return; 10 | 11 | $.getJSON(url, function(data) { 12 | var rows = $("table tr"); 13 | $.each(data.items, function(i, item) { 14 | var row = rows.get(i+1); 15 | if (typeof row !== "undefined") { 16 | row = $(row); 17 | var score = row.find("span.label:first"); 18 | var pubdate = row.find("span.label:last"); 19 | var link = row.find("a"); 20 | link.attr("href", item.url); 21 | link.html(item.title); 22 | score.html(item.score.replace(" points", "")); 23 | pubdate.html(item.time); 24 | } 25 | }); 26 | }); 27 | }; 28 | 29 | RAPID.hackernewsFetch = hackernewsFetch; 30 | })(); 31 | -------------------------------------------------------------------------------- /static/js/lib/bootstrap.js: -------------------------------------------------------------------------------- 1 | /* =================================================== 2 | * bootstrap-transition.js v2.2.2 3 | * http://twitter.github.com/bootstrap/javascript.html#transitions 4 | * =================================================== 5 | * Copyright 2012 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ========================================================== */ 19 | 20 | 21 | !function ($) { 22 | 23 | "use strict"; // jshint ;_; 24 | 25 | 26 | /* CSS TRANSITION SUPPORT (http://www.modernizr.com/) 27 | * ======================================================= */ 28 | 29 | $(function () { 30 | 31 | $.support.transition = (function () { 32 | 33 | var transitionEnd = (function () { 34 | 35 | var el = document.createElement('bootstrap') 36 | , transEndEventNames = { 37 | 'WebkitTransition' : 'webkitTransitionEnd' 38 | , 'MozTransition' : 'transitionend' 39 | , 'OTransition' : 'oTransitionEnd otransitionend' 40 | , 'transition' : 'transitionend' 41 | } 42 | , name 43 | 44 | for (name in transEndEventNames){ 45 | if (el.style[name] !== undefined) { 46 | return transEndEventNames[name] 47 | } 48 | } 49 | 50 | }()) 51 | 52 | return transitionEnd && { 53 | end: transitionEnd 54 | } 55 | 56 | })() 57 | 58 | }) 59 | 60 | }(window.jQuery);/* ========================================================== 61 | * bootstrap-alert.js v2.2.2 62 | * http://twitter.github.com/bootstrap/javascript.html#alerts 63 | * ========================================================== 64 | * Copyright 2012 Twitter, Inc. 65 | * 66 | * Licensed under the Apache License, Version 2.0 (the "License"); 67 | * you may not use this file except in compliance with the License. 68 | * You may obtain a copy of the License at 69 | * 70 | * http://www.apache.org/licenses/LICENSE-2.0 71 | * 72 | * Unless required by applicable law or agreed to in writing, software 73 | * distributed under the License is distributed on an "AS IS" BASIS, 74 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 75 | * See the License for the specific language governing permissions and 76 | * limitations under the License. 77 | * ========================================================== */ 78 | 79 | 80 | !function ($) { 81 | 82 | "use strict"; // jshint ;_; 83 | 84 | 85 | /* ALERT CLASS DEFINITION 86 | * ====================== */ 87 | 88 | var dismiss = '[data-dismiss="alert"]' 89 | , Alert = function (el) { 90 | $(el).on('click', dismiss, this.close) 91 | } 92 | 93 | Alert.prototype.close = function (e) { 94 | var $this = $(this) 95 | , selector = $this.attr('data-target') 96 | , $parent 97 | 98 | if (!selector) { 99 | selector = $this.attr('href') 100 | selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 101 | } 102 | 103 | $parent = $(selector) 104 | 105 | e && e.preventDefault() 106 | 107 | $parent.length || ($parent = $this.hasClass('alert') ? $this : $this.parent()) 108 | 109 | $parent.trigger(e = $.Event('close')) 110 | 111 | if (e.isDefaultPrevented()) return 112 | 113 | $parent.removeClass('in') 114 | 115 | function removeElement() { 116 | $parent 117 | .trigger('closed') 118 | .remove() 119 | } 120 | 121 | $.support.transition && $parent.hasClass('fade') ? 122 | $parent.on($.support.transition.end, removeElement) : 123 | removeElement() 124 | } 125 | 126 | 127 | /* ALERT PLUGIN DEFINITION 128 | * ======================= */ 129 | 130 | var old = $.fn.alert 131 | 132 | $.fn.alert = function (option) { 133 | return this.each(function () { 134 | var $this = $(this) 135 | , data = $this.data('alert') 136 | if (!data) $this.data('alert', (data = new Alert(this))) 137 | if (typeof option == 'string') data[option].call($this) 138 | }) 139 | } 140 | 141 | $.fn.alert.Constructor = Alert 142 | 143 | 144 | /* ALERT NO CONFLICT 145 | * ================= */ 146 | 147 | $.fn.alert.noConflict = function () { 148 | $.fn.alert = old 149 | return this 150 | } 151 | 152 | 153 | /* ALERT DATA-API 154 | * ============== */ 155 | 156 | $(document).on('click.alert.data-api', dismiss, Alert.prototype.close) 157 | 158 | }(window.jQuery);/* ============================================================ 159 | * bootstrap-button.js v2.2.2 160 | * http://twitter.github.com/bootstrap/javascript.html#buttons 161 | * ============================================================ 162 | * Copyright 2012 Twitter, Inc. 163 | * 164 | * Licensed under the Apache License, Version 2.0 (the "License"); 165 | * you may not use this file except in compliance with the License. 166 | * You may obtain a copy of the License at 167 | * 168 | * http://www.apache.org/licenses/LICENSE-2.0 169 | * 170 | * Unless required by applicable law or agreed to in writing, software 171 | * distributed under the License is distributed on an "AS IS" BASIS, 172 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 173 | * See the License for the specific language governing permissions and 174 | * limitations under the License. 175 | * ============================================================ */ 176 | 177 | 178 | !function ($) { 179 | 180 | "use strict"; // jshint ;_; 181 | 182 | 183 | /* BUTTON PUBLIC CLASS DEFINITION 184 | * ============================== */ 185 | 186 | var Button = function (element, options) { 187 | this.$element = $(element) 188 | this.options = $.extend({}, $.fn.button.defaults, options) 189 | } 190 | 191 | Button.prototype.setState = function (state) { 192 | var d = 'disabled' 193 | , $el = this.$element 194 | , data = $el.data() 195 | , val = $el.is('input') ? 'val' : 'html' 196 | 197 | state = state + 'Text' 198 | data.resetText || $el.data('resetText', $el[val]()) 199 | 200 | $el[val](data[state] || this.options[state]) 201 | 202 | // push to event loop to allow forms to submit 203 | setTimeout(function () { 204 | state == 'loadingText' ? 205 | $el.addClass(d).attr(d, d) : 206 | $el.removeClass(d).removeAttr(d) 207 | }, 0) 208 | } 209 | 210 | Button.prototype.toggle = function () { 211 | var $parent = this.$element.closest('[data-toggle="buttons-radio"]') 212 | 213 | $parent && $parent 214 | .find('.active') 215 | .removeClass('active') 216 | 217 | this.$element.toggleClass('active') 218 | } 219 | 220 | 221 | /* BUTTON PLUGIN DEFINITION 222 | * ======================== */ 223 | 224 | var old = $.fn.button 225 | 226 | $.fn.button = function (option) { 227 | return this.each(function () { 228 | var $this = $(this) 229 | , data = $this.data('button') 230 | , options = typeof option == 'object' && option 231 | if (!data) $this.data('button', (data = new Button(this, options))) 232 | if (option == 'toggle') data.toggle() 233 | else if (option) data.setState(option) 234 | }) 235 | } 236 | 237 | $.fn.button.defaults = { 238 | loadingText: 'loading...' 239 | } 240 | 241 | $.fn.button.Constructor = Button 242 | 243 | 244 | /* BUTTON NO CONFLICT 245 | * ================== */ 246 | 247 | $.fn.button.noConflict = function () { 248 | $.fn.button = old 249 | return this 250 | } 251 | 252 | 253 | /* BUTTON DATA-API 254 | * =============== */ 255 | 256 | $(document).on('click.button.data-api', '[data-toggle^=button]', function (e) { 257 | var $btn = $(e.target) 258 | if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') 259 | $btn.button('toggle') 260 | }) 261 | 262 | }(window.jQuery);/* ========================================================== 263 | * bootstrap-carousel.js v2.2.2 264 | * http://twitter.github.com/bootstrap/javascript.html#carousel 265 | * ========================================================== 266 | * Copyright 2012 Twitter, Inc. 267 | * 268 | * Licensed under the Apache License, Version 2.0 (the "License"); 269 | * you may not use this file except in compliance with the License. 270 | * You may obtain a copy of the License at 271 | * 272 | * http://www.apache.org/licenses/LICENSE-2.0 273 | * 274 | * Unless required by applicable law or agreed to in writing, software 275 | * distributed under the License is distributed on an "AS IS" BASIS, 276 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 277 | * See the License for the specific language governing permissions and 278 | * limitations under the License. 279 | * ========================================================== */ 280 | 281 | 282 | !function ($) { 283 | 284 | "use strict"; // jshint ;_; 285 | 286 | 287 | /* CAROUSEL CLASS DEFINITION 288 | * ========================= */ 289 | 290 | var Carousel = function (element, options) { 291 | this.$element = $(element) 292 | this.options = options 293 | this.options.pause == 'hover' && this.$element 294 | .on('mouseenter', $.proxy(this.pause, this)) 295 | .on('mouseleave', $.proxy(this.cycle, this)) 296 | } 297 | 298 | Carousel.prototype = { 299 | 300 | cycle: function (e) { 301 | if (!e) this.paused = false 302 | this.options.interval 303 | && !this.paused 304 | && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) 305 | return this 306 | } 307 | 308 | , to: function (pos) { 309 | var $active = this.$element.find('.item.active') 310 | , children = $active.parent().children() 311 | , activePos = children.index($active) 312 | , that = this 313 | 314 | if (pos > (children.length - 1) || pos < 0) return 315 | 316 | if (this.sliding) { 317 | return this.$element.one('slid', function () { 318 | that.to(pos) 319 | }) 320 | } 321 | 322 | if (activePos == pos) { 323 | return this.pause().cycle() 324 | } 325 | 326 | return this.slide(pos > activePos ? 'next' : 'prev', $(children[pos])) 327 | } 328 | 329 | , pause: function (e) { 330 | if (!e) this.paused = true 331 | if (this.$element.find('.next, .prev').length && $.support.transition.end) { 332 | this.$element.trigger($.support.transition.end) 333 | this.cycle() 334 | } 335 | clearInterval(this.interval) 336 | this.interval = null 337 | return this 338 | } 339 | 340 | , next: function () { 341 | if (this.sliding) return 342 | return this.slide('next') 343 | } 344 | 345 | , prev: function () { 346 | if (this.sliding) return 347 | return this.slide('prev') 348 | } 349 | 350 | , slide: function (type, next) { 351 | var $active = this.$element.find('.item.active') 352 | , $next = next || $active[type]() 353 | , isCycling = this.interval 354 | , direction = type == 'next' ? 'left' : 'right' 355 | , fallback = type == 'next' ? 'first' : 'last' 356 | , that = this 357 | , e 358 | 359 | this.sliding = true 360 | 361 | isCycling && this.pause() 362 | 363 | $next = $next.length ? $next : this.$element.find('.item')[fallback]() 364 | 365 | e = $.Event('slide', { 366 | relatedTarget: $next[0] 367 | }) 368 | 369 | if ($next.hasClass('active')) return 370 | 371 | if ($.support.transition && this.$element.hasClass('slide')) { 372 | this.$element.trigger(e) 373 | if (e.isDefaultPrevented()) return 374 | $next.addClass(type) 375 | $next[0].offsetWidth // force reflow 376 | $active.addClass(direction) 377 | $next.addClass(direction) 378 | this.$element.one($.support.transition.end, function () { 379 | $next.removeClass([type, direction].join(' ')).addClass('active') 380 | $active.removeClass(['active', direction].join(' ')) 381 | that.sliding = false 382 | setTimeout(function () { that.$element.trigger('slid') }, 0) 383 | }) 384 | } else { 385 | this.$element.trigger(e) 386 | if (e.isDefaultPrevented()) return 387 | $active.removeClass('active') 388 | $next.addClass('active') 389 | this.sliding = false 390 | this.$element.trigger('slid') 391 | } 392 | 393 | isCycling && this.cycle() 394 | 395 | return this 396 | } 397 | 398 | } 399 | 400 | 401 | /* CAROUSEL PLUGIN DEFINITION 402 | * ========================== */ 403 | 404 | var old = $.fn.carousel 405 | 406 | $.fn.carousel = function (option) { 407 | return this.each(function () { 408 | var $this = $(this) 409 | , data = $this.data('carousel') 410 | , options = $.extend({}, $.fn.carousel.defaults, typeof option == 'object' && option) 411 | , action = typeof option == 'string' ? option : options.slide 412 | if (!data) $this.data('carousel', (data = new Carousel(this, options))) 413 | if (typeof option == 'number') data.to(option) 414 | else if (action) data[action]() 415 | else if (options.interval) data.cycle() 416 | }) 417 | } 418 | 419 | $.fn.carousel.defaults = { 420 | interval: 5000 421 | , pause: 'hover' 422 | } 423 | 424 | $.fn.carousel.Constructor = Carousel 425 | 426 | 427 | /* CAROUSEL NO CONFLICT 428 | * ==================== */ 429 | 430 | $.fn.carousel.noConflict = function () { 431 | $.fn.carousel = old 432 | return this 433 | } 434 | 435 | /* CAROUSEL DATA-API 436 | * ================= */ 437 | 438 | $(document).on('click.carousel.data-api', '[data-slide]', function (e) { 439 | var $this = $(this), href 440 | , $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 441 | , options = $.extend({}, $target.data(), $this.data()) 442 | $target.carousel(options) 443 | e.preventDefault() 444 | }) 445 | 446 | }(window.jQuery);/* ============================================================= 447 | * bootstrap-collapse.js v2.2.2 448 | * http://twitter.github.com/bootstrap/javascript.html#collapse 449 | * ============================================================= 450 | * Copyright 2012 Twitter, Inc. 451 | * 452 | * Licensed under the Apache License, Version 2.0 (the "License"); 453 | * you may not use this file except in compliance with the License. 454 | * You may obtain a copy of the License at 455 | * 456 | * http://www.apache.org/licenses/LICENSE-2.0 457 | * 458 | * Unless required by applicable law or agreed to in writing, software 459 | * distributed under the License is distributed on an "AS IS" BASIS, 460 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 461 | * See the License for the specific language governing permissions and 462 | * limitations under the License. 463 | * ============================================================ */ 464 | 465 | 466 | !function ($) { 467 | 468 | "use strict"; // jshint ;_; 469 | 470 | 471 | /* COLLAPSE PUBLIC CLASS DEFINITION 472 | * ================================ */ 473 | 474 | var Collapse = function (element, options) { 475 | this.$element = $(element) 476 | this.options = $.extend({}, $.fn.collapse.defaults, options) 477 | 478 | if (this.options.parent) { 479 | this.$parent = $(this.options.parent) 480 | } 481 | 482 | this.options.toggle && this.toggle() 483 | } 484 | 485 | Collapse.prototype = { 486 | 487 | constructor: Collapse 488 | 489 | , dimension: function () { 490 | var hasWidth = this.$element.hasClass('width') 491 | return hasWidth ? 'width' : 'height' 492 | } 493 | 494 | , show: function () { 495 | var dimension 496 | , scroll 497 | , actives 498 | , hasData 499 | 500 | if (this.transitioning) return 501 | 502 | dimension = this.dimension() 503 | scroll = $.camelCase(['scroll', dimension].join('-')) 504 | actives = this.$parent && this.$parent.find('> .accordion-group > .in') 505 | 506 | if (actives && actives.length) { 507 | hasData = actives.data('collapse') 508 | if (hasData && hasData.transitioning) return 509 | actives.collapse('hide') 510 | hasData || actives.data('collapse', null) 511 | } 512 | 513 | this.$element[dimension](0) 514 | this.transition('addClass', $.Event('show'), 'shown') 515 | $.support.transition && this.$element[dimension](this.$element[0][scroll]) 516 | } 517 | 518 | , hide: function () { 519 | var dimension 520 | if (this.transitioning) return 521 | dimension = this.dimension() 522 | this.reset(this.$element[dimension]()) 523 | this.transition('removeClass', $.Event('hide'), 'hidden') 524 | this.$element[dimension](0) 525 | } 526 | 527 | , reset: function (size) { 528 | var dimension = this.dimension() 529 | 530 | this.$element 531 | .removeClass('collapse') 532 | [dimension](size || 'auto') 533 | [0].offsetWidth 534 | 535 | this.$element[size !== null ? 'addClass' : 'removeClass']('collapse') 536 | 537 | return this 538 | } 539 | 540 | , transition: function (method, startEvent, completeEvent) { 541 | var that = this 542 | , complete = function () { 543 | if (startEvent.type == 'show') that.reset() 544 | that.transitioning = 0 545 | that.$element.trigger(completeEvent) 546 | } 547 | 548 | this.$element.trigger(startEvent) 549 | 550 | if (startEvent.isDefaultPrevented()) return 551 | 552 | this.transitioning = 1 553 | 554 | this.$element[method]('in') 555 | 556 | $.support.transition && this.$element.hasClass('collapse') ? 557 | this.$element.one($.support.transition.end, complete) : 558 | complete() 559 | } 560 | 561 | , toggle: function () { 562 | this[this.$element.hasClass('in') ? 'hide' : 'show']() 563 | } 564 | 565 | } 566 | 567 | 568 | /* COLLAPSE PLUGIN DEFINITION 569 | * ========================== */ 570 | 571 | var old = $.fn.collapse 572 | 573 | $.fn.collapse = function (option) { 574 | return this.each(function () { 575 | var $this = $(this) 576 | , data = $this.data('collapse') 577 | , options = typeof option == 'object' && option 578 | if (!data) $this.data('collapse', (data = new Collapse(this, options))) 579 | if (typeof option == 'string') data[option]() 580 | }) 581 | } 582 | 583 | $.fn.collapse.defaults = { 584 | toggle: true 585 | } 586 | 587 | $.fn.collapse.Constructor = Collapse 588 | 589 | 590 | /* COLLAPSE NO CONFLICT 591 | * ==================== */ 592 | 593 | $.fn.collapse.noConflict = function () { 594 | $.fn.collapse = old 595 | return this 596 | } 597 | 598 | 599 | /* COLLAPSE DATA-API 600 | * ================= */ 601 | 602 | $(document).on('click.collapse.data-api', '[data-toggle=collapse]', function (e) { 603 | var $this = $(this), href 604 | , target = $this.attr('data-target') 605 | || e.preventDefault() 606 | || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 607 | , option = $(target).data('collapse') ? 'toggle' : $this.data() 608 | $this[$(target).hasClass('in') ? 'addClass' : 'removeClass']('collapsed') 609 | $(target).collapse(option) 610 | }) 611 | 612 | }(window.jQuery);/* ============================================================ 613 | * bootstrap-dropdown.js v2.2.2 614 | * http://twitter.github.com/bootstrap/javascript.html#dropdowns 615 | * ============================================================ 616 | * Copyright 2012 Twitter, Inc. 617 | * 618 | * Licensed under the Apache License, Version 2.0 (the "License"); 619 | * you may not use this file except in compliance with the License. 620 | * You may obtain a copy of the License at 621 | * 622 | * http://www.apache.org/licenses/LICENSE-2.0 623 | * 624 | * Unless required by applicable law or agreed to in writing, software 625 | * distributed under the License is distributed on an "AS IS" BASIS, 626 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 627 | * See the License for the specific language governing permissions and 628 | * limitations under the License. 629 | * ============================================================ */ 630 | 631 | 632 | !function ($) { 633 | 634 | "use strict"; // jshint ;_; 635 | 636 | 637 | /* DROPDOWN CLASS DEFINITION 638 | * ========================= */ 639 | 640 | var toggle = '[data-toggle=dropdown]' 641 | , Dropdown = function (element) { 642 | var $el = $(element).on('click.dropdown.data-api', this.toggle) 643 | $('html').on('click.dropdown.data-api', function () { 644 | $el.parent().removeClass('open') 645 | }) 646 | } 647 | 648 | Dropdown.prototype = { 649 | 650 | constructor: Dropdown 651 | 652 | , toggle: function (e) { 653 | var $this = $(this) 654 | , $parent 655 | , isActive 656 | 657 | if ($this.is('.disabled, :disabled')) return 658 | 659 | $parent = getParent($this) 660 | 661 | isActive = $parent.hasClass('open') 662 | 663 | clearMenus() 664 | 665 | if (!isActive) { 666 | $parent.toggleClass('open') 667 | } 668 | 669 | $this.focus() 670 | 671 | return false 672 | } 673 | 674 | , keydown: function (e) { 675 | var $this 676 | , $items 677 | , $active 678 | , $parent 679 | , isActive 680 | , index 681 | 682 | if (!/(38|40|27)/.test(e.keyCode)) return 683 | 684 | $this = $(this) 685 | 686 | e.preventDefault() 687 | e.stopPropagation() 688 | 689 | if ($this.is('.disabled, :disabled')) return 690 | 691 | $parent = getParent($this) 692 | 693 | isActive = $parent.hasClass('open') 694 | 695 | if (!isActive || (isActive && e.keyCode == 27)) return $this.click() 696 | 697 | $items = $('[role=menu] li:not(.divider):visible a', $parent) 698 | 699 | if (!$items.length) return 700 | 701 | index = $items.index($items.filter(':focus')) 702 | 703 | if (e.keyCode == 38 && index > 0) index-- // up 704 | if (e.keyCode == 40 && index < $items.length - 1) index++ // down 705 | if (!~index) index = 0 706 | 707 | $items 708 | .eq(index) 709 | .focus() 710 | } 711 | 712 | } 713 | 714 | function clearMenus() { 715 | $(toggle).each(function () { 716 | getParent($(this)).removeClass('open') 717 | }) 718 | } 719 | 720 | function getParent($this) { 721 | var selector = $this.attr('data-target') 722 | , $parent 723 | 724 | if (!selector) { 725 | selector = $this.attr('href') 726 | selector = selector && /#/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 727 | } 728 | 729 | $parent = $(selector) 730 | $parent.length || ($parent = $this.parent()) 731 | 732 | return $parent 733 | } 734 | 735 | 736 | /* DROPDOWN PLUGIN DEFINITION 737 | * ========================== */ 738 | 739 | var old = $.fn.dropdown 740 | 741 | $.fn.dropdown = function (option) { 742 | return this.each(function () { 743 | var $this = $(this) 744 | , data = $this.data('dropdown') 745 | if (!data) $this.data('dropdown', (data = new Dropdown(this))) 746 | if (typeof option == 'string') data[option].call($this) 747 | }) 748 | } 749 | 750 | $.fn.dropdown.Constructor = Dropdown 751 | 752 | 753 | /* DROPDOWN NO CONFLICT 754 | * ==================== */ 755 | 756 | $.fn.dropdown.noConflict = function () { 757 | $.fn.dropdown = old 758 | return this 759 | } 760 | 761 | 762 | /* APPLY TO STANDARD DROPDOWN ELEMENTS 763 | * =================================== */ 764 | 765 | $(document) 766 | .on('click.dropdown.data-api touchstart.dropdown.data-api', clearMenus) 767 | .on('click.dropdown touchstart.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) 768 | .on('touchstart.dropdown.data-api', '.dropdown-menu', function (e) { e.stopPropagation() }) 769 | .on('click.dropdown.data-api touchstart.dropdown.data-api' , toggle, Dropdown.prototype.toggle) 770 | .on('keydown.dropdown.data-api touchstart.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown) 771 | 772 | }(window.jQuery);/* ========================================================= 773 | * bootstrap-modal.js v2.2.2 774 | * http://twitter.github.com/bootstrap/javascript.html#modals 775 | * ========================================================= 776 | * Copyright 2012 Twitter, Inc. 777 | * 778 | * Licensed under the Apache License, Version 2.0 (the "License"); 779 | * you may not use this file except in compliance with the License. 780 | * You may obtain a copy of the License at 781 | * 782 | * http://www.apache.org/licenses/LICENSE-2.0 783 | * 784 | * Unless required by applicable law or agreed to in writing, software 785 | * distributed under the License is distributed on an "AS IS" BASIS, 786 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 787 | * See the License for the specific language governing permissions and 788 | * limitations under the License. 789 | * ========================================================= */ 790 | 791 | 792 | !function ($) { 793 | 794 | "use strict"; // jshint ;_; 795 | 796 | 797 | /* MODAL CLASS DEFINITION 798 | * ====================== */ 799 | 800 | var Modal = function (element, options) { 801 | this.options = options 802 | this.$element = $(element) 803 | .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this)) 804 | this.options.remote && this.$element.find('.modal-body').load(this.options.remote) 805 | } 806 | 807 | Modal.prototype = { 808 | 809 | constructor: Modal 810 | 811 | , toggle: function () { 812 | return this[!this.isShown ? 'show' : 'hide']() 813 | } 814 | 815 | , show: function () { 816 | var that = this 817 | , e = $.Event('show') 818 | 819 | this.$element.trigger(e) 820 | 821 | if (this.isShown || e.isDefaultPrevented()) return 822 | 823 | this.isShown = true 824 | 825 | this.escape() 826 | 827 | this.backdrop(function () { 828 | var transition = $.support.transition && that.$element.hasClass('fade') 829 | 830 | if (!that.$element.parent().length) { 831 | that.$element.appendTo(document.body) //don't move modals dom position 832 | } 833 | 834 | that.$element 835 | .show() 836 | 837 | if (transition) { 838 | that.$element[0].offsetWidth // force reflow 839 | } 840 | 841 | that.$element 842 | .addClass('in') 843 | .attr('aria-hidden', false) 844 | 845 | that.enforceFocus() 846 | 847 | transition ? 848 | that.$element.one($.support.transition.end, function () { that.$element.focus().trigger('shown') }) : 849 | that.$element.focus().trigger('shown') 850 | 851 | }) 852 | } 853 | 854 | , hide: function (e) { 855 | e && e.preventDefault() 856 | 857 | var that = this 858 | 859 | e = $.Event('hide') 860 | 861 | this.$element.trigger(e) 862 | 863 | if (!this.isShown || e.isDefaultPrevented()) return 864 | 865 | this.isShown = false 866 | 867 | this.escape() 868 | 869 | $(document).off('focusin.modal') 870 | 871 | this.$element 872 | .removeClass('in') 873 | .attr('aria-hidden', true) 874 | 875 | $.support.transition && this.$element.hasClass('fade') ? 876 | this.hideWithTransition() : 877 | this.hideModal() 878 | } 879 | 880 | , enforceFocus: function () { 881 | var that = this 882 | $(document).on('focusin.modal', function (e) { 883 | if (that.$element[0] !== e.target && !that.$element.has(e.target).length) { 884 | that.$element.focus() 885 | } 886 | }) 887 | } 888 | 889 | , escape: function () { 890 | var that = this 891 | if (this.isShown && this.options.keyboard) { 892 | this.$element.on('keyup.dismiss.modal', function ( e ) { 893 | e.which == 27 && that.hide() 894 | }) 895 | } else if (!this.isShown) { 896 | this.$element.off('keyup.dismiss.modal') 897 | } 898 | } 899 | 900 | , hideWithTransition: function () { 901 | var that = this 902 | , timeout = setTimeout(function () { 903 | that.$element.off($.support.transition.end) 904 | that.hideModal() 905 | }, 500) 906 | 907 | this.$element.one($.support.transition.end, function () { 908 | clearTimeout(timeout) 909 | that.hideModal() 910 | }) 911 | } 912 | 913 | , hideModal: function (that) { 914 | this.$element 915 | .hide() 916 | .trigger('hidden') 917 | 918 | this.backdrop() 919 | } 920 | 921 | , removeBackdrop: function () { 922 | this.$backdrop.remove() 923 | this.$backdrop = null 924 | } 925 | 926 | , backdrop: function (callback) { 927 | var that = this 928 | , animate = this.$element.hasClass('fade') ? 'fade' : '' 929 | 930 | if (this.isShown && this.options.backdrop) { 931 | var doAnimate = $.support.transition && animate 932 | 933 | this.$backdrop = $('