├── src ├── models │ ├── __init__.py │ ├── models.py │ ├── loadcsv.py │ ├── quickstart.py │ └── calendarapi.py ├── templates │ ├── login.html │ ├── finishoauth.html │ ├── buttons.html │ ├── dbdisplay.html │ ├── bars.html │ ├── calendar.html │ ├── base.html │ └── index.html ├── db │ ├── events.sqlite3 │ ├── events_all.sqlite3 │ ├── events_all_2014.sqlite3 │ └── schema.sql ├── static │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.svg │ ├── css │ │ ├── calendar.css │ │ ├── bars.css │ │ ├── cover.css │ │ ├── d3.tip.v0.6.3.js │ │ ├── bootstrap-theme.min.css │ │ ├── bootstrap-theme.css.map │ │ └── bootstrap-theme.css │ ├── calendar.js │ ├── bars.js │ └── js │ │ └── bootstrap.min.js ├── client_secrets.json └── webapp.py ├── Procfile ├── .gitignore ├── package.json ├── gruntfile.js ├── ANALYSIS.md ├── requirements.txt ├── MOTIVATION.md └── README.md /src/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: python webapp.py 2 | -------------------------------------------------------------------------------- /src/templates/login.html: -------------------------------------------------------------------------------- 1 | {% for calendar in calendar_list %} 2 | {{ calendar }} 3 | {% endfor %} -------------------------------------------------------------------------------- /src/db/events.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robert8138/flask-google-calendar-api-project/HEAD/src/db/events.sqlite3 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | client_secret.json 2 | pip-selfcheck.json 3 | events.csv 4 | *.pyc 5 | bin/ 6 | include/ 7 | lib/ 8 | node_modules/ -------------------------------------------------------------------------------- /src/db/events_all.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robert8138/flask-google-calendar-api-project/HEAD/src/db/events_all.sqlite3 -------------------------------------------------------------------------------- /src/db/events_all_2014.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robert8138/flask-google-calendar-api-project/HEAD/src/db/events_all_2014.sqlite3 -------------------------------------------------------------------------------- /src/static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robert8138/flask-google-calendar-api-project/HEAD/src/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /src/static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robert8138/flask-google-calendar-api-project/HEAD/src/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "livereloadTest", 3 | "version": "0.1.0", 4 | "devDependencies": { 5 | "grunt": "~0.4.2", 6 | "grunt-contrib-watch": "~0.5.3" 7 | } 8 | } -------------------------------------------------------------------------------- /src/static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robert8138/flask-google-calendar-api-project/HEAD/src/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /src/db/schema.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS events; 2 | CREATE TABLE events ( 3 | id INTEGER PRIMARY KEY AUTOINCREMENT, 4 | date TEXT, 5 | duration REAL, 6 | event_type TEXT, 7 | event_name TEXT 8 | ); 9 | -------------------------------------------------------------------------------- /src/templates/finishoauth.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} OAuth authentication success {% endblock %} 4 | 5 | {% block content %} 6 | 7 | OAuth authentication was successful! 8 | 9 | {% endblock %} -------------------------------------------------------------------------------- /src/templates/buttons.html: -------------------------------------------------------------------------------- 1 | {% for calendar in calendarMap %} 2 | 3 | 6 | 7 | {% endfor %} -------------------------------------------------------------------------------- /src/static/css/calendar.css: -------------------------------------------------------------------------------- 1 | .day { 2 | fill: #fff; 3 | stroke: #ccc; 4 | } 5 | 6 | .month { 7 | fill: none; 8 | stroke: #000; 9 | stroke-width: 2px; 10 | } 11 | 12 | .heatmap { 13 | fill: #2b8cbe; 14 | } 15 | 16 | .calendar { 17 | display: block; 18 | margin: auto; 19 | } -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.initConfig({ 3 | pkg: grunt.file.readJSON('package.json'), 4 | watch: { 5 | src: { 6 | files: ['src/templates/*.html', 'src/static/css/*.css'], 7 | options: { livereload: true } 8 | } 9 | } 10 | }); 11 | grunt.loadNpmTasks('grunt-contrib-watch'); 12 | }; -------------------------------------------------------------------------------- /ANALYSIS.md: -------------------------------------------------------------------------------- 1 | # A list of Analysis Questions About My Calendar: 2 | 3 | * How much time do I spend on Continuous Improvement (CI)? On the weekdays? On the weekends? 4 | * How are the patterns different from day time and night time, especially on the weekends? 5 | * How much time do I spend on Self Reflection, Reading, and Writing? 6 | * How much Vacation Days Do I take a Year? 7 | * How much time do I spend working while in Graduate School v.s. Working in the Industry? 8 | -------------------------------------------------------------------------------- /src/client_secrets.json: -------------------------------------------------------------------------------- 1 | { 2 | "installed": { 3 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 4 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 5 | "client_id": "314221214870-o3arqdr7ohjjbf7cguijsoelpej0ol5k.apps.googleusercontent.com", 6 | "client_secret": "NzxDBF4dbANc3nDI-HQhcb0g", 7 | "redirect_uris": [ 8 | "urn:ietf:wg:oauth:2.0:oob", 9 | "http://localhost" 10 | ], 11 | "token_uri": "https://accounts.google.com/o/oauth2/token" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/templates/dbdisplay.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} Events {% endblock %} 4 | 5 | {% block content %} 6 | 7 | 8 | {% for event in events %} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {% endfor %} 17 |
{{ event.date }}{{ event.duration }}{{ event.event_type }}{{ event.event_name }}
18 | 19 | {% endblock %} -------------------------------------------------------------------------------- /src/templates/bars.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} Bar View {% endblock %} 4 | 5 | {% block content %} 6 | 7 | {% with calendarMap=calendarMap %} 8 | {% include "buttons.html" %} 9 | {% endwith %} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% endblock %} -------------------------------------------------------------------------------- /src/templates/calendar.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} Calendar View {% endblock %} 4 | 5 | {% block content %} 6 | 7 | {% with calendarMap=calendarMap %} 8 | {% include "buttons.html" %} 9 | {% endwith %} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% endblock %} -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==0.10.1 2 | Flask-Cors==2.1.0 3 | Flask-SQLAlchemy==2.0 4 | Jinja2==2.8 5 | MarkupSafe==0.23 6 | Pattern==2.6 7 | Pygments==1.6 8 | SQLAlchemy==1.0.8 9 | Sphinx==1.2.3 10 | Werkzeug==0.10.4 11 | backports.ssl-match-hostname==3.4.0.2 12 | cdk==1.0.8 13 | certifi==14.05.14 14 | distribute==0.6.45 15 | docopt==0.6.2 16 | google-api-python-client==1.4.1 17 | httplib2==0.9.1 18 | ipython==1.1.0 19 | itsdangerous==0.24 20 | oauth2client==1.5.0 21 | pyasn1==0.1.8 22 | pyasn1-modules==0.0.7 23 | python-dateutil==2.2 24 | pytz==2014.7 25 | pyzmq==14.4.0 26 | readline==6.2.4.1 27 | requests==2.7.0 28 | rsa==3.2 29 | simplejson==3.8.0 30 | six==1.9.0 31 | tornado==4.0.2 32 | uritemplate==0.6 33 | vboxapi==1.0 34 | vertica-python==0.3.0 35 | virtualenv==1.10.1 36 | wheel==0.24.0 37 | wsgiref==0.1.2 38 | -------------------------------------------------------------------------------- /src/models/models.py: -------------------------------------------------------------------------------- 1 | from flask.ext.sqlalchemy import SQLAlchemy 2 | from sqlalchemy import Column, Integer, String, Date, Float 3 | 4 | db = SQLAlchemy() 5 | 6 | class Events(db.Model): 7 | __tablename__ = 'events_full' 8 | id = Column(Integer, primary_key=True) 9 | date = Column(Date) 10 | duration = Column(Float) 11 | event_type = Column(String) 12 | event_name = Column(String) 13 | 14 | def __init__(self, date, duration, event_type, event_name): 15 | self.date = date 16 | self.duration = duration 17 | self.event_type = event_type 18 | self.event_name = event_name 19 | 20 | def __repr__(self): 21 | return '' % (self.id) 22 | 23 | @property 24 | def serialize(self): 25 | '''return as a json object so we can use it in RESTful API''' 26 | return {'id': self.id, 27 | 'date': self.date.strftime("%Y-%m-%d"), 28 | 'duration': self.duration, 29 | 'event_type': self.event_type, 30 | 'event_name': self.event_name } 31 | 32 | -------------------------------------------------------------------------------- /src/static/css/bars.css: -------------------------------------------------------------------------------- 1 | .bar { 2 | fill: orange; 3 | } 4 | 5 | .bar:hover { 6 | fill: #2ca25f; 7 | } 8 | 9 | .xaxis path, 10 | .xaxis line { 11 | fill: none; 12 | stroke: black; 13 | shape-rendering: crispEdges; 14 | } 15 | .yaxis path, 16 | .yaxis line { 17 | fill: none; 18 | stroke: black; 19 | shape-rendering: crispEdges; 20 | } 21 | 22 | .spanClass 23 | { 24 | display:block; 25 | } 26 | 27 | .d3-tip { 28 | line-height: 1; 29 | font-weight: bold; 30 | padding: 12px; 31 | background: rgba(0, 0, 0, 0.8); 32 | color: #fff; 33 | border-radius: 2px; 34 | } 35 | 36 | /* Creates a small triangle extender for the tooltip */ 37 | .d3-tip:after { 38 | box-sizing: border-box; 39 | display: inline; 40 | font-size: 10px; 41 | width: 100%; 42 | line-height: 1; 43 | color: rgba(0, 0, 0, 0.8); 44 | content: "\25BC"; 45 | position: absolute; 46 | text-align: center; 47 | } 48 | 49 | /* Style northward tooltips differently */ 50 | .d3-tip.n:after { 51 | margin: -1px 0 0 0; 52 | top: 100%; 53 | left: 0; 54 | } -------------------------------------------------------------------------------- /src/models/loadcsv.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String, Date, Float 2 | from sqlalchemy import create_engine, MetaData, Table 3 | from sqlalchemy.orm import mapper, create_session 4 | import datetime 5 | import csv 6 | 7 | def load_csv_to_db(CSV_FILE): 8 | 9 | #CSV_FILE = 'events.csv' 10 | engine = create_engine('sqlite:///db/events_all_2014.sqlite3') # memory-only database 11 | 12 | table = None 13 | metadata = MetaData(bind=engine) 14 | 15 | 16 | with open(CSV_FILE) as f: 17 | # assume first line is header 18 | cf = csv.DictReader(f, 19 | fieldnames = ('date', 'duration', 'event_type', 'event_name'), 20 | delimiter=',') 21 | print "Ready to write into db..................................." 22 | for row in cf: 23 | if table is None: 24 | # create the table 25 | table = Table('events_full', metadata, 26 | Column('id', Integer, primary_key=True), 27 | Column('date', Date), 28 | Column('duration', Float), 29 | Column('event_type', String), 30 | Column('event_name', String) 31 | ) 32 | table.create() 33 | # insert data into the table 34 | date = row['date'] 35 | duration = row['duration'] 36 | event_type = row['event_type'] 37 | event_name = row['event_name'].replace(',', '') 38 | year, month, day = [int(elem) for elem in date.split('-')] 39 | date = datetime.date(year, month, day) 40 | table.insert().values(date = date, 41 | duration = duration, 42 | event_type = event_type, 43 | event_name = event_name).execute() -------------------------------------------------------------------------------- /src/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {% block title %}{% endblock %} 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 40 |
41 | {% block content %} 42 | {% endblock %} 43 |
44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Welcome to Calendar Aanalyzer 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 |
26 | 27 |
28 | 29 |
30 |
31 |

Flask

32 | 38 |
39 |
40 | 41 |
42 |

Analyze your Google Calendar

43 |

This Flask App analyze how you spent your time by querying Google Calendar API and build beautiful d3 graphs

44 |

45 | Get Started 46 |

47 |
48 | 49 |
50 |
51 |

Flask template for Bootstrap, by @_rchang.

52 |
53 |
54 | 55 |
56 | 57 |
58 | 59 |
60 | 61 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/models/quickstart.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import httplib2 3 | import os 4 | 5 | from apiclient import discovery 6 | import oauth2client 7 | from oauth2client import client 8 | from oauth2client import tools 9 | 10 | import datetime 11 | 12 | try: 13 | import argparse 14 | flags = argparse.ArgumentParser(parents=[tools.argparser]).parse_args() 15 | except ImportError: 16 | flags = None 17 | 18 | SCOPES = 'https://www.googleapis.com/auth/calendar.readonly' 19 | CLIENT_SECRET_FILE = 'client_secret.json' 20 | APPLICATION_NAME = 'Google Calendar API Quickstart' 21 | 22 | 23 | def get_credentials(): 24 | """Gets valid user credentials from storage. 25 | 26 | If nothing has been stored, or if the stored credentials are invalid, 27 | the OAuth2 flow is completed to obtain the new credentials. 28 | 29 | Returns: 30 | Credentials, the obtained credential. 31 | """ 32 | home_dir = os.path.expanduser('~') 33 | credential_dir = os.path.join(home_dir, '.credentials') 34 | if not os.path.exists(credential_dir): 35 | os.makedirs(credential_dir) 36 | credential_path = os.path.join(credential_dir, 37 | 'calendar-quickstart.json') 38 | 39 | store = oauth2client.file.Storage(credential_path) 40 | credentials = store.get() 41 | if not credentials or credentials.invalid: 42 | flow = client.flow_from_clientsecrets(CLIENT_SECRET_FILE, SCOPES) 43 | flow.user_agent = APPLICATION_NAME 44 | if flags: 45 | credentials = tools.run_flow(flow, store, flags) 46 | else: # Needed only for compatability with Python 2.6 47 | credentials = tools.run(flow, store) 48 | print('Storing credentials to ' + credential_path) 49 | return credentials 50 | 51 | def main(): 52 | """Shows basic usage of the Google Calendar API. 53 | 54 | Creates a Google Calendar API service object and outputs a list of the next 55 | 10 events on the user's calendar. 56 | """ 57 | credentials = get_credentials() 58 | http = credentials.authorize(httplib2.Http()) 59 | service = discovery.build('calendar', 'v3', http=http) 60 | 61 | now = datetime.datetime.utcnow().isoformat() + 'Z' # 'Z' indicates UTC time 62 | print('Getting the upcoming 10 events') 63 | eventsResult = service.events().list( 64 | calendarId='primary', timeMin=now, maxResults=10, singleEvents=True, 65 | orderBy='startTime').execute() 66 | events = eventsResult.get('items', []) 67 | 68 | if not events: 69 | print('No upcoming events found.') 70 | for event in events: 71 | start = event['start'].get('dateTime', event['start'].get('date')) 72 | print(start, event['summary']) 73 | 74 | 75 | if __name__ == '__main__': 76 | main() -------------------------------------------------------------------------------- /src/static/calendar.js: -------------------------------------------------------------------------------- 1 | $.each(window.calendarMap, function(k, v) { 2 | d3.select("[id='" + k + "']").on("click", function() { make_calendar(k); }); 3 | }); 4 | 5 | var width = 1000, 6 | height = 150, 7 | cellSize = 15; 8 | 9 | var percent = d3.format(".1%"), 10 | format = d3.time.format("%Y-%m-%d"); 11 | 12 | // Set up the skeleton by year 13 | var svg = d3.select("body").selectAll("svg") 14 | .data(d3.range(2013, 2016)) 15 | .enter().append("svg") 16 | .attr("width", width) 17 | .attr("height", height) 18 | .attr("class", "calendar") 19 | .append("g") 20 | .attr("transform", "translate(" + ((width - cellSize * 53) / 2) + "," + (height - cellSize * 7 - 1) + ")"); 21 | 22 | // Set up the YEAR text 23 | svg.append("text") 24 | .attr("transform", "translate(-6," + cellSize * 3.5 + ")rotate(-90)") 25 | .style("text-anchor", "middle") 26 | .text(function(d) { return d; }); 27 | 28 | // Set up each day for plotting 29 | var rect = svg.selectAll(".day") 30 | .data(function(d) { return d3.time.days(new Date(d, 0, 1), new Date(d + 1, 0, 1)); }) 31 | .enter().append("rect") 32 | .attr("class", "day") 33 | .attr("width", cellSize) 34 | .attr("height", cellSize) 35 | .attr("x", function(d) { return d3.time.weekOfYear(d) * cellSize; }) 36 | .attr("y", function(d) { return d.getDay() * cellSize; }) 37 | .datum(format); 38 | 39 | // Add title/date to each cell 40 | rect.append("title") 41 | .text(function(d) { return d; }); 42 | 43 | // Set up each month for plotting 44 | svg.selectAll(".month") 45 | .data(function(d) { return d3.time.months(new Date(d, 0, 1), new Date(d + 1, 0, 1)); }) 46 | .enter().append("path") 47 | .attr("class", "month") 48 | .attr("d", monthPath); 49 | 50 | // Some voodoo magic that I don't understand about paths 51 | function monthPath(t0) { 52 | var t1 = new Date(t0.getFullYear(), t0.getMonth() + 1, 0), 53 | d0 = t0.getDay(), 54 | w0 = d3.time.weekOfYear(t0), 55 | d1 = t1.getDay(), 56 | w1 = d3.time.weekOfYear(t1); 57 | return "M" + (w0 + 1) * cellSize + "," + d0 * cellSize 58 | + "H" + w0 * cellSize + "V" + 7 * cellSize 59 | + "H" + w1 * cellSize + "V" + (d1 + 1) * cellSize 60 | + "H" + (w1 + 1) * cellSize + "V" + 0 61 | + "H" + (w0 + 1) * cellSize + "Z"; 62 | } 63 | 64 | function make_calendar(event_type) { 65 | 66 | url = "http://127.0.0.1:5000/api/".concat(event_type) 67 | d3.json(url, function(error, data) { 68 | 69 | console.log(event_type.concat("is loaded!")) 70 | 71 | var dataset = data['json_list'] 72 | 73 | var dataset_munged = d3.nest() 74 | .key(function(d) { return format(new Date(d.date)); }) 75 | .rollup(function(events) { return d3.sum(events, function(d) { 76 | return d.duration|| 0; }); }) 77 | .map(dataset); 78 | 79 | var colors = ["#ffffd9","#edf8b1","#c7e9b4", 80 | "#7fcdbb","#41b6c4","#1d91c0", 81 | "#225ea8","#253494","#081d58"] 82 | var colorScale = d3.scale.quantize() 83 | // .domain([0, d3.max(dataset_munged, function (d) { return d.value; })]) 84 | .domain([0, 200]) //TODO 85 | .range(colors); 86 | 87 | rect 88 | .classed("day", true) 89 | .style("fill", function(d) { return colorScale(dataset_munged[d]); }) 90 | 91 | }) 92 | } -------------------------------------------------------------------------------- /src/static/css/cover.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Globals 3 | */ 4 | 5 | /* Links */ 6 | a, 7 | a:focus, 8 | a:hover { 9 | color: #fff; 10 | } 11 | 12 | /* Custom default button */ 13 | .btn-default, 14 | .btn-default:hover, 15 | .btn-default:focus { 16 | color: white; 17 | text-shadow: none; /* Prevent inheritence from `body` */ 18 | background-color: #fff; 19 | border: 1px solid #fff; 20 | } 21 | 22 | 23 | /* 24 | * Base structure 25 | */ 26 | 27 | html, 28 | body { 29 | height: 100%; 30 | background-color: #fdae6b; 31 | } 32 | body { 33 | color: #fff; 34 | text-align: center; 35 | text-shadow: 0 1px 3px rgba(0,0,0,.5); 36 | } 37 | 38 | /* Extra markup and styles for table-esque vertical and horizontal centering */ 39 | .site-wrapper { 40 | display: table; 41 | width: 100%; 42 | height: 100%; /* For at least Firefox */ 43 | min-height: 100%; 44 | -webkit-box-shadow: inset 0 0 100px rgba(0,0,0,.5); 45 | box-shadow: inset 0 0 100px rgba(0,0,0,.5); 46 | } 47 | .site-wrapper-inner { 48 | display: table-cell; 49 | vertical-align: top; 50 | } 51 | .cover-container { 52 | margin-right: auto; 53 | margin-left: auto; 54 | } 55 | 56 | /* Padding for spacing */ 57 | .inner { 58 | padding: 30px; 59 | } 60 | 61 | 62 | /* 63 | * Header 64 | */ 65 | .masthead-brand { 66 | margin-top: 10px; 67 | margin-bottom: 10px; 68 | } 69 | 70 | .masthead-nav > li { 71 | display: inline-block; 72 | } 73 | .masthead-nav > li + li { 74 | margin-left: 20px; 75 | } 76 | .masthead-nav > li > a { 77 | padding-right: 0; 78 | padding-left: 0; 79 | font-size: 16px; 80 | font-weight: bold; 81 | color: #fff; /* IE8 proofing */ 82 | color: rgba(255,255,255,.75); 83 | border-bottom: 2px solid transparent; 84 | } 85 | .masthead-nav > li > a:hover, 86 | .masthead-nav > li > a:focus { 87 | background-color: transparent; 88 | border-bottom-color: #a9a9a9; 89 | border-bottom-color: rgba(255,255,255,.25); 90 | } 91 | .masthead-nav > .active > a, 92 | .masthead-nav > .active > a:hover, 93 | .masthead-nav > .active > a:focus { 94 | color: #fff; 95 | border-bottom-color: #fff; 96 | } 97 | 98 | @media (min-width: 768px) { 99 | .masthead-brand { 100 | float: left; 101 | } 102 | .masthead-nav { 103 | float: right; 104 | } 105 | } 106 | 107 | 108 | /* 109 | * Cover 110 | */ 111 | 112 | .cover { 113 | padding: 0 20px; 114 | } 115 | .cover .btn-lg { 116 | padding: 10px 20px; 117 | font-weight: bold; 118 | } 119 | 120 | 121 | /* 122 | * Footer 123 | */ 124 | 125 | .mastfoot { 126 | color: #999; /* IE8 proofing */ 127 | color: rgba(255,255,255,.5); 128 | } 129 | 130 | 131 | /* 132 | * Affix and center 133 | */ 134 | 135 | @media (min-width: 768px) { 136 | /* Pull out the header and footer */ 137 | .masthead { 138 | position: fixed; 139 | top: 0; 140 | } 141 | .mastfoot { 142 | position: fixed; 143 | bottom: 0; 144 | } 145 | /* Start the vertical centering */ 146 | .site-wrapper-inner { 147 | vertical-align: middle; 148 | } 149 | /* Handle the widths */ 150 | .masthead, 151 | .mastfoot, 152 | .cover-container { 153 | width: 100%; /* Must be percentage or pixels for horizontal alignment */ 154 | } 155 | } 156 | 157 | @media (min-width: 992px) { 158 | .masthead, 159 | .mastfoot, 160 | .cover-container { 161 | width: 700px; 162 | } 163 | } -------------------------------------------------------------------------------- /src/static/bars.js: -------------------------------------------------------------------------------- 1 | $.each(window.calendarMap, function(k, v) { 2 | d3.select("[id='" + k + "']").on("click", function() { make_graph(k); }); 3 | }); 4 | 5 | var width = 1300, 6 | height = 250; 7 | 8 | var svg = d3.select("body").append("svg") 9 | .attr("width", width) 10 | .attr("height", height); 11 | 12 | var tip = d3.tip() 13 | .attr('class', 'd3-tip') 14 | .offset([-10, 0]) 15 | .html(function(d) { 16 | return "event: " + d.event_name + "" + 17 | "date: " + d.date + ""; 18 | }) 19 | 20 | //http://bl.ocks.org/Caged/6476579 21 | svg.call(tip); 22 | 23 | function make_graph(event_type) { 24 | url = "http://127.0.0.1:5000/api/".concat(event_type) 25 | console.log(url) 26 | d3.json(url, function(error, data) { 27 | 28 | console.log(event_type.concat(" is loaded!")) 29 | console.log(data['json_list']) 30 | 31 | var dataset = data['json_list'] 32 | var barwidth = width / dataset.length; 33 | 34 | // http://bost.ocks.org/mike/bar/3/ 35 | var x = d3.scale.linear() 36 | .domain([0, dataset.length]) 37 | .range([0, width]); 38 | 39 | var y = d3.scale.linear() 40 | .domain([0, d3.max(dataset, function(d) { return d.duration; })]) 41 | .range([height - 30, 0]); 42 | 43 | var xAxis = d3.svg.axis() 44 | .scale(x) 45 | .ticks(25) 46 | .orient("bottom"); 47 | 48 | var yAxis = d3.svg.axis() 49 | .scale(y) 50 | .ticks(5) 51 | .orient("left"); 52 | 53 | // This is more of a hack, because there is no transition 54 | // I basically removed the old axes, rebuild news ones, and plot them 55 | 56 | svg.selectAll("g").remove(); 57 | 58 | svg.append("g") 59 | .attr("class", "xaxis") 60 | .attr("transform", "translate(50," + (height - 30 - 10) + ")") 61 | .transition() 62 | .duration(750) 63 | .call(xAxis); 64 | 65 | svg.append("g") 66 | .attr("class", "yaxis") 67 | .attr("transform", "translate(50, 0)") 68 | .transition() 69 | .duration(750) 70 | .call(yAxis); 71 | 72 | 73 | // http://bl.ocks.org/mbostock/3808218 74 | // Enter, Update, Exit Design Pattern - suggested by Krist 75 | 76 | var bars = svg.selectAll(".bar") 77 | .data(dataset); 78 | 79 | bars.enter() 80 | .append("rect") 81 | .classed("bar", true) 82 | .transition() 83 | .duration(2000) 84 | .attr("transform", function(d, i) { 85 | return "translate(" + (i * barwidth + 50) + ",0)"; }) 86 | .attr("y", function(d) { return y(d.duration); }) 87 | .attr("height", function(d) { return height - 40 - y(d.duration); }) 88 | .attr("width", barwidth - 1) 89 | .attr("fill", "#fdae6b"); 90 | 91 | bars 92 | .attr("transform", function(d, i) { 93 | return "translate(" + (i * barwidth + 50) + ",0)"; }) 94 | .attr("y", function(d) { return y(d.duration); }) 95 | .attr("height", function(d) { return height - 40 - y(d.duration); }) 96 | .attr("width", barwidth - 1); 97 | 98 | bars.exit().remove(); 99 | 100 | bars.on('mouseover', tip.show) 101 | .on('mouseout', tip.hide); 102 | 103 | }); 104 | } -------------------------------------------------------------------------------- /MOTIVATION.md: -------------------------------------------------------------------------------- 1 | # Building a Interactive Data Visualization Application using Python Flask and Google Calendar API 2 | 3 | ## Motivation 4 | Right around 02/07/2015, I asked the following [question] on Quora: 5 | 6 | > As a data scientist, what are the things that I can learn from full stack developers so that I can build interesting web applications for data science? 7 | I came across Hadley Wickham's slide from Conference | Simply Statistics, where he rightly pointed out that a lot of the statistical innovations are moving to web: 8 | 9 | > PDF -> HTML 10 | LaTex -> Markdown 11 | Static -> Interactive 12 | 13 | > More people are sharing their codes and analyses in the form of Rmarkdown or iPython notebooks. Many are pushing their new R/Python packages to Github, and we see increasingly better integration of data and web applications (e.g. ŷhat | Data Science Operations Platform, Domino Data Lab ... etc) 14 | 15 | > What are the things that data scientists can learn about building web application that makes our work more effective, engaging, and highly visible? 16 | 17 | Very soon, I came across Alexander Blocker's awesome answer, which has served as the backbone for my exploration of this area. In his words: 18 | 19 | > If you want to build a web application based on an analysis, here are a few skills I would start with: 20 | 21 | > * Learn how to map your data and analysis to a **RESTful API** (Representational state transfer). Data scientists usually like to work with tabular data, often linked across tables by common identifiers and accessed via SQL. Translating this to a set of nouns and verbs in the REST style is a great step towards making your tools and results more widely usable. 22 | 23 | > * Learn basic **AJAX** so you can **hit your RESTful API from client-side code**. I find **jQuery** the nicest way to get started with this. Also, don't get hung up on low-level DOM manipulation in Javascript. Learn Selectors and take advantage of all the hard work jQuery developers have put into the problem. 24 | 25 | > * Learn how to approach layout on the web. This means **HTML + CSS. Twitter's Bootstrap framework** is a lovely, well-designed starting point. 26 | Learn how to build interactive graphics on the client side. D3.js is the most mature way to do this, but it's verbose. Plottable (Home, from Palantir) and NVd3 (nvd3-community/nvd3, recently revived) are nice layers of abstraction over the details of D3. 27 | 28 | > * To experiment with all of this, a platform like Google App Engine is a nice place to start. It handles a lot of the operational details and lets you focus on implementing the application. I would suggest starting in **Python**, if that's what you're comfortable in, or Go, which I prefer. Go on App Engine is still in its early days, but it's easy to write web services in, runs and compiles fast, and the App Engine-specific parts are lightweight. The core of the code is just the Go standard library, which makes it easier to move to Google Compute Engine, **AWS**, or anywhere else you want. 29 | 30 | > Finally, a bit on **mindset**. I like to think of building web applications as **another way to express your ideas**, just like giving talks, writing papers, or distributing R packages. A working prototype based on a new algorithm or dataset is often far more compelling than a report or command-line tool, especially to non-data-scientists. Your web apps don't have to be completely polished and productionised to be useful in this capacity. But, you do need to communicate your work just as clearly in an interactive app as would would in any other medium. 31 | 32 | [question]: https://www.quora.com/As-a-data-scientist-what-are-the-things-that-I-can-learn-from-full-stack-developers-so-that-I-can-build-interesting-web-applications-for-data-science -------------------------------------------------------------------------------- /src/models/calendarapi.py: -------------------------------------------------------------------------------- 1 | import os 2 | import httplib2 3 | import oauth2client 4 | import datetime 5 | import time 6 | import csv 7 | from apiclient import discovery 8 | from oauth2client import client 9 | from oauth2client import tools 10 | from collections import defaultdict 11 | 12 | # Global Constants 13 | SCOPES = 'https://www.googleapis.com/auth/calendar.readonly' 14 | CLIENT_SECRET_FILE = 'client_secret.json' 15 | APPLICATION_NAME = 'Google Calendar API Quickstart' 16 | 17 | # Helper Functions 18 | def get_credentials(): 19 | """Gets valid user credentials from storage. 20 | 21 | If nothing has been stored, or if the stored credentials are invalid, 22 | the OAuth2 flow is completed to obtain the new credentials. 23 | 24 | Returns: 25 | Credentials, the obtained credential. 26 | """ 27 | try: 28 | import argparse 29 | flags = argparse.ArgumentParser(parents=[tools.argparser]).parse_args() 30 | except ImportError: 31 | flags = None 32 | 33 | home_dir = os.path.expanduser('~') 34 | credential_dir = os.path.join(home_dir, '.credentials') 35 | if not os.path.exists(credential_dir): 36 | os.makedirs(credential_dir) 37 | credential_path = os.path.join(credential_dir, 38 | 'calendar-quickstart.json') 39 | 40 | store = oauth2client.file.Storage(credential_path) 41 | credentials = store.get() 42 | if not credentials or credentials.invalid: 43 | flow = client.flow_from_clientsecrets(CLIENT_SECRET_FILE, SCOPES) 44 | flow.user_agent = APPLICATION_NAME 45 | if flags: 46 | credentials = tools.run_flow(flow, store, flags) 47 | else: # Needed only for compatability with Python 2.6 48 | credentials = tools.run(flow, store) 49 | print 'Storing credentials to ' + credential_path 50 | return credentials 51 | 52 | def build_service(): 53 | """Build a Google Calendar API query endpoint""" 54 | credentials = get_credentials() 55 | http = credentials.authorize(httplib2.Http()) 56 | service = discovery.build('calendar', 'v3', http=http) 57 | return service 58 | 59 | def get_start_date(startDate): 60 | """ 61 | Given a string startDate in the format of YYYY-mm-dd 62 | return a datetime object with the proper time zone 63 | """ 64 | return (datetime.datetime 65 | .strptime(startDate, "%Y-%m-%d") 66 | .isoformat() + 'Z') 67 | 68 | def get_now_date(): 69 | """Return a datetime object of current time""" 70 | return (datetime.datetime.utcnow() 71 | .isoformat() + 'Z') 72 | 73 | def get_end_date(): 74 | pass 75 | 76 | def get_ts_from_datetime(start, end): 77 | """convert datetime string into a time event 78 | so we can perform time arithmetic on them""" 79 | 80 | if start[-2:] == '00': 81 | startHr = datetime.datetime.strptime(start[:-6], "%Y-%m-%dT%H:%M:%S") 82 | elif start[-1] == 'Z': 83 | startHr = datetime.datetime.strptime(start, "%Y-%m-%dT%H:%M:%SZ") 84 | else: 85 | startHr = datetime.datetime.strptime(start, "%Y-%m-%d") 86 | 87 | if end[-2:] == '00': 88 | endHr = datetime.datetime.strptime(end[:-6], "%Y-%m-%dT%H:%M:%S") 89 | elif end[-1] == 'Z': 90 | endHr = datetime.datetime.strptime(end, "%Y-%m-%dT%H:%M:%SZ") 91 | else: 92 | endHr = datetime.datetime.strptime(end, "%Y-%m-%d") 93 | 94 | startTS = time.mktime(startHr.timetuple()) 95 | endTS = time.mktime(endHr.timetuple()) 96 | return (startTS, endTS) 97 | 98 | def get_calendar_list_map(service): 99 | """Get the list of Calendar Names, and store their corresponding ids""" 100 | 101 | calendarMap = defaultdict() 102 | page_token = None 103 | 104 | calendars = (service.calendarList() 105 | .list(pageToken = page_token).execute()) 106 | 107 | for calendar in calendars['items']: 108 | id = calendar.get('id', None) 109 | summary = calendar.get('summary', None) 110 | calendarMap[summary] = id 111 | return calendarMap 112 | 113 | def get_events_in_calendar(calendarName, calendarMap, service, startDate, endDate=None): 114 | """Get all the events in a calendar for a specific time period""" 115 | 116 | print '\nGet ' + calendarName + ' Events Since the beginning of the year.............' 117 | startDate = get_start_date(startDate) 118 | endDate = get_now_date() 119 | # service = build_service() 120 | # calendarMap = get_calendar_list_map(service) 121 | calendarId = calendarMap.get(calendarName) 122 | eventsResult = (service.events().list(calendarId = calendarId, timeMin = startDate, timeMax = endDate, maxResults = 1000, singleEvents = True, orderBy = 'startTime').execute()) 123 | 124 | events = eventsResult.get('items', []) 125 | events_list = [] 126 | 127 | if not events: 128 | print 'No events found.' 129 | for event in events: 130 | start = event['start'].get('dateTime', event['start'].get('date')) 131 | end = event['end'].get('dateTime', event['end'].get('date')) 132 | startTS, endTS = get_ts_from_datetime(start, end) 133 | 134 | duration = (endTS - startTS) / 60 135 | event_type = calendarName 136 | 137 | eventTitle = event['summary'] 138 | event_name = ''.join([i if ord(i) < 128 else '' for i in eventTitle.replace(',','')]) 139 | 140 | #print start[:10], duration, event_type, event_name 141 | 142 | events_list.append([start[:10], duration, event_type, event_name]) 143 | return events_list 144 | 145 | def write_events_to_csv(events_list): 146 | with open('events.csv', 'a') as events_csv: 147 | writer = csv.writer(events_csv) 148 | for each_event in events_list: 149 | writer.writerow(each_event) 150 | 151 | def main(): 152 | """Main Entry Point of the App""" 153 | 154 | service = build_service() 155 | calendarMap = get_calendar_list_map(service) 156 | 157 | for calendarName in calendarMap: 158 | events_list = get_events_in_calendar(calendarName, calendarMap, '2013-01-01') 159 | write_events_to_csv(events_list) 160 | 161 | if __name__ == '__main__': 162 | main() 163 | -------------------------------------------------------------------------------- /src/webapp.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask import request 3 | from flask import render_template 4 | from flask import jsonify 5 | from apiclient import discovery 6 | from oauth2client import client 7 | from flask.ext.cors import CORS 8 | from oauth2client import client 9 | from sqlalchemy import * 10 | from models.models import * 11 | from models.calendarapi import * 12 | from models.loadcsv import * 13 | import sqlite3 14 | import datetime 15 | import os 16 | import flask 17 | import httplib2 18 | 19 | webapp = Flask(__name__) 20 | webapp.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db/events_all_2014.sqlite3' 21 | webapp.config['SESSION_TYPE'] = 'filesystem' 22 | webapp.config['SECRET_KEY'] = 'super secret key' 23 | CORS(webapp) 24 | db.init_app(webapp) 25 | 26 | # event_type_map = {'studytime': 'Study Time', 27 | # 'outsidereading': 'Outside Reading', 28 | # 'birthdays': 'Birthdays', 29 | # 'misc': 'Misc', 30 | # 'deadline': 'Deadline', 31 | # 'exercise': 'Exercise'} 32 | 33 | @webapp.route('/', methods=['GET','POST']) 34 | def index(): 35 | return render_template('index.html') 36 | 37 | @webapp.route('/login') 38 | def login(): 39 | if 'credentials' not in flask.session: 40 | return flask.redirect(flask.url_for('oauth2callback')) 41 | credentials = client.OAuth2Credentials.from_json(flask.session['credentials']) 42 | if credentials.access_token_expired: 43 | return flask.redirect(flask.url_for('oauth2callback')) 44 | else: 45 | if os.path.exists("events.csv") and os.path.exists("db/events_all_2014.sqlite3"): 46 | os.remove("events.csv") 47 | os.remove("db/events_all_2014.sqlite3") 48 | 49 | http_auth = credentials.authorize(httplib2.Http()) 50 | service = discovery.build('calendar', 'v3', http = http_auth) 51 | global calendarMap 52 | calendarMap = get_calendar_list_map(service) 53 | 54 | for calendarName in calendarMap: 55 | print "processing..." + calendarName + ".........." 56 | events_list = get_events_in_calendar(calendarName, calendarMap, service, '2014-01-01') 57 | write_events_to_csv(events_list) 58 | 59 | load_csv_to_db("events.csv") 60 | str = '\n'.join([calendar for calendar in calendarMap]) 61 | 62 | return str 63 | 64 | @webapp.route('/oauth2callback') 65 | def oauth2callback(): 66 | flow = client.flow_from_clientsecrets( 67 | 'client_secrets.json', 68 | scope='https://www.googleapis.com/auth/calendar', 69 | redirect_uri=flask.url_for('oauth2callback', _external=True)) 70 | 71 | if 'code' not in flask.request.args: 72 | auth_uri = flow.step1_get_authorize_url() 73 | return flask.redirect(auth_uri) 74 | else: 75 | auth_code = flask.request.args.get('code') 76 | credentials = flow.step2_exchange(auth_code) 77 | flask.session['credentials'] = credentials.to_json() 78 | return flask.redirect(flask.url_for('finishoauth')) 79 | 80 | @webapp.route('/finishoauth') 81 | def finishoauth(): 82 | return render_template('finishoauth.html') 83 | 84 | # Regular Views 85 | @webapp.route('/dbdisplay') 86 | def display(): 87 | return render_template("dbdisplay.html", 88 | events = Events.query.all()) 89 | 90 | @webapp.route('/dbdisplay/') 91 | def display_by_event_type(event_type): 92 | # event_type = event_type_map[event_type] 93 | return render_template("dbdisplay.html", 94 | events = Events.query.filter_by(event_type = event_type).all()) 95 | 96 | @webapp.route('/dbdisplay///') 97 | def display_by_date(year, month, day): 98 | date = datetime.date(int(year), int(month), int(day)) 99 | return render_template("dbdisplay.html", 100 | events = Events.query.filter_by(date = date).all()) 101 | 102 | @webapp.route('/dbdisplay/duration/') 103 | def display_duration_greater_than(duration): 104 | return render_template("dbdisplay.html", 105 | events = Events.query.filter(Events.duration > float(duration)).all()) 106 | 107 | @webapp.route('/distinct') 108 | def distinct(): 109 | #TODO: load all the calendarNames from the db using SQLAlchemy 110 | return ''.join([event_type for event_type in event_types]) 111 | 112 | # API endpoints 113 | @webapp.route('/api/all') 114 | def api_all(): 115 | events = Events.query.all() 116 | return jsonify(json_list = [event.serialize for event in events]) 117 | 118 | @webapp.route('/api/') 119 | def api_by_event_type(event_type): 120 | # event_type = event_type_map[event_type] 121 | events = Events.query.filter_by(event_type = event_type).all() 122 | return jsonify(json_list = [event.serialize for event in events]) 123 | 124 | @webapp.route('/api///') 125 | def api_by_date(year, month, day): 126 | date = datetime.date(int(year), int(month), int(day)) 127 | events = Events.query.filter_by(date = date).all() 128 | return jsonify(json_list = [event.serialize for event in events]) 129 | 130 | @webapp.route('/api/duration/') 131 | def api_by_duration(duration): 132 | events = Events.query.filter(Events.duration > float(duration)).all() 133 | return jsonify(json_list = [event.serialize for event in events]) 134 | 135 | # Plotting endpoints 136 | @webapp.route('/bars') 137 | def plot_d3_bars(): 138 | return render_template("bars.html", calendarMap = calendarMap) 139 | 140 | @webapp.route('/calendar') 141 | def plot_d3_calendar(): 142 | return render_template("calendar.html", calendarMap = calendarMap) 143 | 144 | @webapp.route('/test') 145 | def test(): 146 | str = '\n'.join([calendar for calendar in calendarMap]) 147 | return str 148 | 149 | if __name__ == '__main__': 150 | 151 | # EVENTS_CSV_FILE = "events.csv" 152 | # DB_FILE = "db/events_all_2014.sqlite3" 153 | 154 | # if not os.path.exists(EVENTS_CSV_FILE) and not os.path.exists(DB_FILE): 155 | 156 | # service = build_service() 157 | # calendarMap = get_calendar_list_map(service) 158 | 159 | # for calendarName in calendarMap: 160 | # events_list = get_events_in_calendar(calendarName, calendarMap, service, '2014-01-01') 161 | # write_events_to_csv(events_list) 162 | 163 | # load_csv_to_db("events.csv") 164 | 165 | webapp.debug = True 166 | webapp.run() -------------------------------------------------------------------------------- /src/static/css/d3.tip.v0.6.3.js: -------------------------------------------------------------------------------- 1 | // d3.tip 2 | // Copyright (c) 2013 Justin Palmer 3 | // 4 | // Tooltips for d3.js SVG visualizations 5 | 6 | // Public - contructs a new tooltip 7 | // 8 | // Returns a tip 9 | d3.tip = function() { 10 | var direction = d3_tip_direction, 11 | offset = d3_tip_offset, 12 | html = d3_tip_html, 13 | node = initNode(), 14 | svg = null, 15 | point = null, 16 | target = null 17 | 18 | function tip(vis) { 19 | svg = getSVGNode(vis) 20 | point = svg.createSVGPoint() 21 | document.body.appendChild(node) 22 | } 23 | 24 | // Public - show the tooltip on the screen 25 | // 26 | // Returns a tip 27 | tip.show = function() { 28 | var args = Array.prototype.slice.call(arguments) 29 | if(args[args.length - 1] instanceof SVGElement) target = args.pop() 30 | 31 | var content = html.apply(this, args), 32 | poffset = offset.apply(this, args), 33 | dir = direction.apply(this, args), 34 | nodel = d3.select(node), i = 0, 35 | coords 36 | 37 | nodel.html(content) 38 | .style({ opacity: 1, 'pointer-events': 'all' }) 39 | 40 | while(i--) nodel.classed(directions[i], false) 41 | coords = direction_callbacks.get(dir).apply(this) 42 | nodel.classed(dir, true).style({ 43 | top: (coords.top + poffset[0]) + 'px', 44 | left: (coords.left + poffset[1]) + 'px' 45 | }) 46 | 47 | return tip 48 | } 49 | 50 | // Public - hide the tooltip 51 | // 52 | // Returns a tip 53 | tip.hide = function() { 54 | nodel = d3.select(node) 55 | nodel.style({ opacity: 0, 'pointer-events': 'none' }) 56 | return tip 57 | } 58 | 59 | // Public: Proxy attr calls to the d3 tip container. Sets or gets attribute value. 60 | // 61 | // n - name of the attribute 62 | // v - value of the attribute 63 | // 64 | // Returns tip or attribute value 65 | tip.attr = function(n, v) { 66 | if (arguments.length < 2 && typeof n === 'string') { 67 | return d3.select(node).attr(n) 68 | } else { 69 | var args = Array.prototype.slice.call(arguments) 70 | d3.selection.prototype.attr.apply(d3.select(node), args) 71 | } 72 | 73 | return tip 74 | } 75 | 76 | // Public: Proxy style calls to the d3 tip container. Sets or gets a style value. 77 | // 78 | // n - name of the property 79 | // v - value of the property 80 | // 81 | // Returns tip or style property value 82 | tip.style = function(n, v) { 83 | if (arguments.length < 2 && typeof n === 'string') { 84 | return d3.select(node).style(n) 85 | } else { 86 | var args = Array.prototype.slice.call(arguments) 87 | d3.selection.prototype.style.apply(d3.select(node), args) 88 | } 89 | 90 | return tip 91 | } 92 | 93 | // Public: Set or get the direction of the tooltip 94 | // 95 | // v - One of n(north), s(south), e(east), or w(west), nw(northwest), 96 | // sw(southwest), ne(northeast) or se(southeast) 97 | // 98 | // Returns tip or direction 99 | tip.direction = function(v) { 100 | if (!arguments.length) return direction 101 | direction = v == null ? v : d3.functor(v) 102 | 103 | return tip 104 | } 105 | 106 | // Public: Sets or gets the offset of the tip 107 | // 108 | // v - Array of [x, y] offset 109 | // 110 | // Returns offset or 111 | tip.offset = function(v) { 112 | if (!arguments.length) return offset 113 | offset = v == null ? v : d3.functor(v) 114 | 115 | return tip 116 | } 117 | 118 | // Public: sets or gets the html value of the tooltip 119 | // 120 | // v - String value of the tip 121 | // 122 | // Returns html value or tip 123 | tip.html = function(v) { 124 | if (!arguments.length) return html 125 | html = v == null ? v : d3.functor(v) 126 | 127 | return tip 128 | } 129 | 130 | function d3_tip_direction() { return 'n' } 131 | function d3_tip_offset() { return [0, 0] } 132 | function d3_tip_html() { return ' ' } 133 | 134 | var direction_callbacks = d3.map({ 135 | n: direction_n, 136 | s: direction_s, 137 | e: direction_e, 138 | w: direction_w, 139 | nw: direction_nw, 140 | ne: direction_ne, 141 | sw: direction_sw, 142 | se: direction_se 143 | }), 144 | 145 | directions = direction_callbacks.keys() 146 | 147 | function direction_n() { 148 | var bbox = getScreenBBox() 149 | return { 150 | top: bbox.n.y - node.offsetHeight, 151 | left: bbox.n.x - node.offsetWidth / 2 152 | } 153 | } 154 | 155 | function direction_s() { 156 | var bbox = getScreenBBox() 157 | return { 158 | top: bbox.s.y, 159 | left: bbox.s.x - node.offsetWidth / 2 160 | } 161 | } 162 | 163 | function direction_e() { 164 | var bbox = getScreenBBox() 165 | return { 166 | top: bbox.e.y - node.offsetHeight / 2, 167 | left: bbox.e.x 168 | } 169 | } 170 | 171 | function direction_w() { 172 | var bbox = getScreenBBox() 173 | return { 174 | top: bbox.w.y - node.offsetHeight / 2, 175 | left: bbox.w.x - node.offsetWidth 176 | } 177 | } 178 | 179 | function direction_nw() { 180 | var bbox = getScreenBBox() 181 | return { 182 | top: bbox.nw.y - node.offsetHeight, 183 | left: bbox.nw.x - node.offsetWidth 184 | } 185 | } 186 | 187 | function direction_ne() { 188 | var bbox = getScreenBBox() 189 | return { 190 | top: bbox.ne.y - node.offsetHeight, 191 | left: bbox.ne.x 192 | } 193 | } 194 | 195 | function direction_sw() { 196 | var bbox = getScreenBBox() 197 | return { 198 | top: bbox.sw.y, 199 | left: bbox.sw.x - node.offsetWidth 200 | } 201 | } 202 | 203 | function direction_se() { 204 | var bbox = getScreenBBox() 205 | return { 206 | top: bbox.se.y, 207 | left: bbox.e.x 208 | } 209 | } 210 | 211 | function initNode() { 212 | var node = d3.select(document.createElement('div')) 213 | node.style({ 214 | position: 'absolute', 215 | opacity: 0, 216 | pointerEvents: 'none', 217 | boxSizing: 'border-box' 218 | }) 219 | 220 | return node.node() 221 | } 222 | 223 | function getSVGNode(el) { 224 | el = el.node() 225 | if(el.tagName.toLowerCase() == 'svg') 226 | return el 227 | 228 | return el.ownerSVGElement 229 | } 230 | 231 | // Private - gets the screen coordinates of a shape 232 | // 233 | // Given a shape on the screen, will return an SVGPoint for the directions 234 | // n(north), s(south), e(east), w(west), ne(northeast), se(southeast), nw(northwest), 235 | // sw(southwest). 236 | // 237 | // +-+-+ 238 | // | | 239 | // + + 240 | // | | 241 | // +-+-+ 242 | // 243 | // Returns an Object {n, s, e, w, nw, sw, ne, se} 244 | function getScreenBBox() { 245 | var targetel = target || d3.event.target, 246 | bbox = {}, 247 | matrix = targetel.getScreenCTM(), 248 | tbbox = targetel.getBBox(), 249 | width = tbbox.width, 250 | height = tbbox.height, 251 | x = tbbox.x, 252 | y = tbbox.y, 253 | scrollTop = document.documentElement.scrollTop || document.body.scrollTop, 254 | scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft 255 | 256 | 257 | point.x = x + scrollLeft 258 | point.y = y + scrollTop 259 | bbox.nw = point.matrixTransform(matrix) 260 | point.x += width 261 | bbox.ne = point.matrixTransform(matrix) 262 | point.y += height 263 | bbox.se = point.matrixTransform(matrix) 264 | point.x -= width 265 | bbox.sw = point.matrixTransform(matrix) 266 | point.y -= height / 2 267 | bbox.w = point.matrixTransform(matrix) 268 | point.x += width 269 | bbox.e = point.matrixTransform(matrix) 270 | point.x -= width / 2 271 | point.y -= height / 2 272 | bbox.n = point.matrixTransform(matrix) 273 | point.y += height 274 | bbox.s = point.matrixTransform(matrix) 275 | 276 | return bbox 277 | } 278 | 279 | return tip 280 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Building Interactive Data Visualization using Python Flask and Google Calendar API 2 | 3 | ## TODO 4 | 5 | * In calendar.html, still need to figure out how to do data.mapValues.max 6 | * Add OAuth so other people can analyze their own calendar [Almost Done] 7 | * Deploy the app using Heroku 8 | 9 | ## OAuth Section - WIP 10 | * The OAuth flow is still not perfect, I had to always delete events.csv & db/events_all_2014.sqlite3 to restart. The branching logic is still a bit off 11 | * Right now, calendarMap is being stored as a Python global variable. Not good. I tried using cache, but I couldn't easily loop through a cache. 12 | * I think an alternative is, in each view, recompute the calendar map by writing a 'select distinct event_type from Events', but I had no success in implementing that yet in '/distinct' view 13 | * Passing in variables in a template to a included template is free, but you can use { with clause } to make it more explicit 14 | * I did some jank to convert calendarMap into a json object, so it can be used/passed down in/to .js files 15 | * I then used JQuery .foreach to create specific d3.select 16 | * I also remove the handling of white space in /api/event_type calls 17 | 18 | ## Project Milestones 19 | 20 | * **Learn about Google APIs, specifically, Calendar API** 21 | * Learn what data is queryable through Google's API documentation 22 | * Learn How to query them. e.g. what parameters to pass in, in what language 23 | * List out the questions I want to analyze (ANALYSIS.md) 24 | 25 | * **Set up a bare-minimum Flask Application** 26 | * Basic routes. e.g. index.html 27 | * Use the opportunity to review Twitter Flask Series from Simeon Franklin to get back to speed 28 | 29 | * **Modularize HTML pages using templating system JinJia** 30 | * The philosophy is to keep the business logic in route definition as simplie as possible 31 | * Remove HTML from views, put them in separate HTML files 32 | * Learn how to pass in placeholder to build static HTML dynamically 33 | * Use Template inheritance to set up 'base.html' and extends it to provide consistent theme 34 | * make views return `render_tempalte` and that is it 35 | 36 | * **Leveaging sqlite3 and perform a one time dump** 37 | * Setting up sqlite3 - nothing but a delimited text file stored in my local laptop 38 | * Quick Data Modeling on what the schema should be 39 | * Learn how to query it in Flask 40 | * Make sure all the data I care about are being dump to a .csv and to sqlite3 41 | 42 | * **ORM Database interaction using SQLAlchemy** 43 | * Again, make views as simple as possible. Hide SQL statement away from views 44 | * Learn how to query the data using SQLAlchemy constructs instead of direct SQL query 45 | * Print the raw .csv data onto the Screen using SQLAlchemy 46 | * Set up new views to render different subset of the db table 47 | 48 | * **Twitter Bootstrap** 49 | * Now that I can issue a query to backend DB and display them as raw data, beautify the webpage by Twitter bootstrap 50 | * Better layout with consistency (using template inheritance of course) 51 | * Better index.html page 52 | 53 | * **Building API endpoints** 54 | * Learn how to translate the output into JSON objects 55 | * Build views to return these JSON objects 56 | * Here you go the API endpoints 57 | 58 | * **Interactive d3 Charts** 59 | * Create Bootstrap style buttons 60 | * Create listners (e.g. d3.select.on) that takes in parameters 61 | * On click, use d3.json to hit my own API endpoints to get data 62 | * use the data and plot bars and calendars 63 | 64 | * **Build OAuth into the app so any authorized Google Calendar can use this app** 65 | * Implement OAuth 66 | * Once users grant their consent, the app stores the data into db 67 | * Update the front-end code so a list of calendarName would show up 68 | * Update the front-end code so that bar chart and calendar chart would still work 69 | 70 | ## Review of the Fundamentals of Web Application 71 | * Python 72 | * [Starting a Python Project the Right Way] 73 | * [Writing Idiomatic Python]: Also there are three vidoes on this very topic 74 | * [Import from a relative path] 75 | 76 | * Web Programming 77 | * [Web Programming Basics]: Philip Guo's intro to web programming 78 | * [What is a Web Framework?]: Given the foundation above, how web framework can help 79 | * Client-Server Interaction via HTTP protocol 80 | * The request always is initiated by the client, the server also respond with, eventually, a HTML 81 | * Web frameworks solves the nitty gritty details of routing and template-ing 82 | 83 | ## Flask Resources 84 | * [Intro to Flask with JQuery]: Philip Guo 85 | * [Flask for Data Science] 86 | * [Jeff Knupp's Flask Posts] 87 | * [Building a Flask web app with EC2, D3] 88 | 89 | ## Git References 90 | * [Resovling Git Merge Conflicts] 91 | * [How and When to Do Git Rebase] 92 | * [Uncommit a Git Commit] 93 | * [Remove directory from git but not local] 94 | * [Use .gitignore to ignore checking in certain files] 95 | 96 | ## D3 References 97 | * [General Update Pattern] 98 | * [General Update Pattern III] 99 | * [d3 nest] 100 | 101 | ## Google API References 102 | * [QPX Express API]: Global airline pricing and shopping in a single, standard API. 103 | * [Google Calendar API] 104 | 105 | ## Using Grunt to do Front-End Web Development 106 | * [Browser Auto-refresh] 107 | * [Another example] 108 | 109 | ## Heroku 110 | * [Python Boilerplate] - Deploy to Heroku section 111 | * [Getting started with Python in Heroku] 112 | 113 | ## OAuth on Flask 114 | * [Using Google OAuth2 on Flask] 115 | * [OAuth 2.0 overview] 116 | * [Google API client library python] 117 | * [Using OAuth2.0 for web application python] 118 | 119 | ## JQuery 120 | * [Select id with space] 121 | 122 | Another new thing I am doing is to paste the references I found right next to the code I wrote down to solve a particular problem. 123 | 124 | [question]: https://www.quora.com/As-a-data-scientist-what-are-the-things-that-I-can-learn-from-full-stack-developers-so-that-I-can-build-interesting-web-applications-for-data-science 125 | 126 | [Starting a Python Project the Right Way]: http://www.jeffknupp.com/blog/2014/02/04/starting-a-python-project-the-right-way/ 127 | [Writing Idiomatic Python]: https://speakerdeck.com/nycpython/writing-idiomatic-python-jeff-knupp 128 | [Import from a relative path]:http://stackoverflow.com/questions/279237/import-a-module-from-a-relative-path 129 | 130 | [Web Programming Basics]: http://www.pgbovine.net/teaching-web-programming.htm 131 | [What is a Web Framework?]: http://www.jeffknupp.com/blog/2014/03/03/what-is-a-web-framework/ 132 | [Intro to Flask with JQuery]: http://www.pgbovine.net/flask-python-tutorial.htm 133 | [Flask for Data Science]: http://www.datacommunitydc.org/blog/2014/02/flask-mega-meta-tutorial-data-scientists 134 | [Jeff Knupp's Flask Posts]: http://www.jeffknupp.com/blog/categories/flask/ 135 | [Building a Flask web app with EC2, D3]: http://www.datasciencebytes.com/bytes/2015/03/07/a-d3js-plot-powered-by-a-sql-database/ 136 | 137 | [Resovling Git Merge Conflicts]: https://help.github.com/articles/resolving-a-merge-conflict-from-the-command-line/ 138 | [How and When to Do Git Rebase]: https://www.atlassian.com/git/tutorials/rewriting-history/git-rebase/ 139 | [Uncommit a Git Commit]: http://stackoverflow.com/questions/2845731/how-to-uncommit-my-last-commit-in-git 140 | [Remove directory from git but not local]: http://stackoverflow.com/questions/6313126/how-to-remove-a-directory-in-my-github-repository 141 | [Use .gitignore to ignore checking in certain files]:https://help.github.com/articles/ignoring-files/ 142 | 143 | [QPX Express API]: https://developers.google.com/qpx-express/ 144 | [Google Calendar API]: https://developers.google.com/google-apps/calendar/ 145 | 146 | [Browser Auto-refresh]: http://stackoverflow.com/questions/21913363/why-isnt-grunt-contrib-watch-livereload-working 147 | [Another example]: http://justinklemm.com/grunt-watch-livereload-javascript-less-sass-compilation/ 148 | 149 | [General Update Pattern]: http://bl.ocks.org/mbostock/3808218 150 | [General Update Pattern III]: http://bl.ocks.org/mbostock/3808234 151 | [d3 nest]: http://bl.ocks.org/phoebebright/raw/3176159/ 152 | 153 | [Python Boilerplate]: https://github.com/mjhea0/flask-boilerplate 154 | [Getting started with Python in Heroku]: https://devcenter.heroku.com/articles/getting-started-with-python#next-steps 155 | 156 | [Using Google OAuth2 on Flask]: https://github.com/mimming/python-flask-google-api-starter 157 | [OAuth 2.0 overview]: https://developers.google.com/identity/protocols/OAuth2 158 | [Google API client library python]: https://developers.google.com/api-client-library/python/guide/aaa_oauth 159 | [Using OAuth2.0 for web application python]: https://developers.google.com/api-client-library/python/auth/web-app 160 | 161 | [Select id with space]: http://stackoverflow.com/questions/596314/jquery-ids-with-spaces -------------------------------------------------------------------------------- /src/static/css/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.2.0 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */.btn-default,.btn-primary,.btn-success,.btn-info,.btn-warning,.btn-danger{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-default:active,.btn-primary:active,.btn-success:active,.btn-info:active,.btn-warning:active,.btn-danger:active,.btn-default.active,.btn-primary.active,.btn-success.active,.btn-info.active,.btn-warning.active,.btn-danger.active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn:active,.btn.active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc}.btn-default:hover,.btn-default:focus{background-color:#e0e0e0;background-position:0 -15px}.btn-default:active,.btn-default.active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default:disabled,.btn-default[disabled]{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:-o-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#2d6ca2));background-image:linear-gradient(to bottom,#428bca 0,#2d6ca2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#2b669a}.btn-primary:hover,.btn-primary:focus{background-color:#2d6ca2;background-position:0 -15px}.btn-primary:active,.btn-primary.active{background-color:#2d6ca2;border-color:#2b669a}.btn-primary:disabled,.btn-primary[disabled]{background-color:#2d6ca2;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:hover,.btn-success:focus{background-color:#419641;background-position:0 -15px}.btn-success:active,.btn-success.active{background-color:#419641;border-color:#3e8f3e}.btn-success:disabled,.btn-success[disabled]{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:hover,.btn-info:focus{background-color:#2aabd2;background-position:0 -15px}.btn-info:active,.btn-info.active{background-color:#2aabd2;border-color:#28a4c9}.btn-info:disabled,.btn-info[disabled]{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:hover,.btn-warning:focus{background-color:#eb9316;background-position:0 -15px}.btn-warning:active,.btn-warning.active{background-color:#eb9316;border-color:#e38d13}.btn-warning:disabled,.btn-warning[disabled]{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:hover,.btn-danger:focus{background-color:#c12e2a;background-position:0 -15px}.btn-danger:active,.btn-danger.active{background-color:#c12e2a;border-color:#b92c28}.btn-danger:disabled,.btn-danger[disabled]{background-color:#c12e2a;background-image:none}.thumbnail,.img-thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-color:#357ebd;background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-o-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#357ebd));background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);background-repeat:repeat-x}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f3f3f3 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f3f3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f3f3f3));background-image:linear-gradient(to bottom,#ebebeb 0,#f3f3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x}.navbar-inverse .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#222 0,#282828 100%);background-image:-o-linear-gradient(top,#222 0,#282828 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#222),to(#282828));background-image:linear-gradient(to bottom,#222 0,#282828 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-static-top,.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:-o-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#3071a9));background-image:linear-gradient(to bottom,#428bca 0,#3071a9 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{text-shadow:0 -1px 0 #3071a9;background-image:-webkit-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:-o-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#3278b3));background-image:linear-gradient(to bottom,#428bca 0,#3278b3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0);background-repeat:repeat-x;border-color:#3278b3}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-o-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#357ebd));background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} -------------------------------------------------------------------------------- /src/static/css/bootstrap-theme.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"bootstrap-theme.css","sources":["less/theme.less","less/mixins/vendor-prefixes.less","bootstrap-theme.css","less/mixins/gradients.less","less/mixins/reset-filter.less"],"names":[],"mappings":"AAeA;;;;;;EAME,0CAAA;EC+CA,6FAAA;EACQ,qFAAA;EC5DT;AFiBC;;;;;;;;;;;;EC0CA,0DAAA;EACQ,kDAAA;EC7CT;AFqCC;;EAEE,wBAAA;EEnCH;AFwCD;EG/CI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EAEA,wHAAA;ECnBF,qEAAA;EJ8BA,6BAAA;EACA,uBAAA;EA+B2C,2BAAA;EAA2B,oBAAA;EE7BvE;AFAC;;EAEE,2BAAA;EACA,8BAAA;EEEH;AFCC;;EAEE,2BAAA;EACA,uBAAA;EECH;AFEC;;EAEE,2BAAA;EACA,wBAAA;EEAH;AFeD;EGhDI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EAEA,wHAAA;ECnBF,qEAAA;EJ8BA,6BAAA;EACA,uBAAA;EE0BD;AFxBC;;EAEE,2BAAA;EACA,8BAAA;EE0BH;AFvBC;;EAEE,2BAAA;EACA,uBAAA;EEyBH;AFtBC;;EAEE,2BAAA;EACA,wBAAA;EEwBH;AFRD;EGjDI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EAEA,wHAAA;ECnBF,qEAAA;EJ8BA,6BAAA;EACA,uBAAA;EEkDD;AFhDC;;EAEE,2BAAA;EACA,8BAAA;EEkDH;AF/CC;;EAEE,2BAAA;EACA,uBAAA;EEiDH;AF9CC;;EAEE,2BAAA;EACA,wBAAA;EEgDH;AF/BD;EGlDI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EAEA,wHAAA;ECnBF,qEAAA;EJ8BA,6BAAA;EACA,uBAAA;EE0ED;AFxEC;;EAEE,2BAAA;EACA,8BAAA;EE0EH;AFvEC;;EAEE,2BAAA;EACA,uBAAA;EEyEH;AFtEC;;EAEE,2BAAA;EACA,wBAAA;EEwEH;AFtDD;EGnDI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EAEA,wHAAA;ECnBF,qEAAA;EJ8BA,6BAAA;EACA,uBAAA;EEkGD;AFhGC;;EAEE,2BAAA;EACA,8BAAA;EEkGH;AF/FC;;EAEE,2BAAA;EACA,uBAAA;EEiGH;AF9FC;;EAEE,2BAAA;EACA,wBAAA;EEgGH;AF7ED;EGpDI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EAEA,wHAAA;ECnBF,qEAAA;EJ8BA,6BAAA;EACA,uBAAA;EE0HD;AFxHC;;EAEE,2BAAA;EACA,8BAAA;EE0HH;AFvHC;;EAEE,2BAAA;EACA,uBAAA;EEyHH;AFtHC;;EAEE,2BAAA;EACA,wBAAA;EEwHH;AF7FD;;ECbE,oDAAA;EACQ,4CAAA;EC8GT;AFvFD;;EGvEI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EHsEF,2BAAA;EE6FD;AF3FD;;;EG5EI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EH4EF,2BAAA;EEiGD;AFvFD;EG1FI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;ECnBF,qEAAA;EJ4GA,oBAAA;EC9CA,6FAAA;EACQ,qFAAA;EC4IT;AFlGD;EG1FI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EF2CF,0DAAA;EACQ,kDAAA;ECqJT;AF/FD;;EAEE,gDAAA;EEiGD;AF7FD;EG5GI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;ECnBF,qEAAA;EFgOD;AFrGD;EG5GI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EF2CF,yDAAA;EACQ,iDAAA;EC0KT;AF9GD;;EAWI,2CAAA;EEuGH;AFlGD;;;EAGE,kBAAA;EEoGD;AF1FD;EACE,+CAAA;EC3FA,4FAAA;EACQ,oFAAA;ECwLT;AFlFD;EGtJI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EH8IF,uBAAA;EE8FD;AFzFD;EGvJI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EH8IF,uBAAA;EEsGD;AFhGD;EGxJI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EH8IF,uBAAA;EE8GD;AFvGD;EGzJI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EH8IF,uBAAA;EEsHD;AFtGD;EGlKI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;ED2QH;AFnGD;EG5KI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EDkRH;AFzGD;EG7KI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EDyRH;AF/GD;EG9KI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EDgSH;AFrHD;EG/KI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EDuSH;AF3HD;EGhLI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;ED8SH;AF9HD;EGnJI,+MAAA;EACA,0MAAA;EACA,uMAAA;EDoRH;AF1HD;EACE,oBAAA;EC/IA,oDAAA;EACQ,4CAAA;EC4QT;AF3HD;;;EAGE,+BAAA;EGpME,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EHkMF,uBAAA;EEiID;AFvHD;ECjKE,mDAAA;EACQ,2CAAA;EC2RT;AFjHD;EG1NI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;ED8UH;AFvHD;EG3NI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EDqVH;AF7HD;EG5NI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;ED4VH;AFnID;EG7NI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EDmWH;AFzID;EG9NI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;ED0WH;AF/ID;EG/NI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EDiXH;AF9ID;EGvOI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EHqOF,uBAAA;EC1LA,2FAAA;EACQ,mFAAA;EC+UT","sourcesContent":["\n//\n// Load core variables and mixins\n// --------------------------------------------------\n\n@import \"variables.less\";\n@import \"mixins.less\";\n\n\n\n//\n// Buttons\n// --------------------------------------------------\n\n// Common styles\n.btn-default,\n.btn-primary,\n.btn-success,\n.btn-info,\n.btn-warning,\n.btn-danger {\n text-shadow: 0 -1px 0 rgba(0,0,0,.2);\n @shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 1px rgba(0,0,0,.075);\n .box-shadow(@shadow);\n\n // Reset the shadow\n &:active,\n &.active {\n .box-shadow(inset 0 3px 5px rgba(0,0,0,.125));\n }\n}\n\n// Mixin for generating new styles\n.btn-styles(@btn-color: #555) {\n #gradient > .vertical(@start-color: @btn-color; @end-color: darken(@btn-color, 12%));\n .reset-filter(); // Disable gradients for IE9 because filter bleeds through rounded corners\n background-repeat: repeat-x;\n border-color: darken(@btn-color, 14%);\n\n &:hover,\n &:focus {\n background-color: darken(@btn-color, 12%);\n background-position: 0 -15px;\n }\n\n &:active,\n &.active {\n background-color: darken(@btn-color, 12%);\n border-color: darken(@btn-color, 14%);\n }\n\n &:disabled,\n &[disabled] {\n background-color: darken(@btn-color, 12%);\n background-image: none;\n }\n}\n\n// Common styles\n.btn {\n // Remove the gradient for the pressed/active state\n &:active,\n &.active {\n background-image: none;\n }\n}\n\n// Apply the mixin to the buttons\n.btn-default { .btn-styles(@btn-default-bg); text-shadow: 0 1px 0 #fff; border-color: #ccc; }\n.btn-primary { .btn-styles(@btn-primary-bg); }\n.btn-success { .btn-styles(@btn-success-bg); }\n.btn-info { .btn-styles(@btn-info-bg); }\n.btn-warning { .btn-styles(@btn-warning-bg); }\n.btn-danger { .btn-styles(@btn-danger-bg); }\n\n\n\n//\n// Images\n// --------------------------------------------------\n\n.thumbnail,\n.img-thumbnail {\n .box-shadow(0 1px 2px rgba(0,0,0,.075));\n}\n\n\n\n//\n// Dropdowns\n// --------------------------------------------------\n\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n #gradient > .vertical(@start-color: @dropdown-link-hover-bg; @end-color: darken(@dropdown-link-hover-bg, 5%));\n background-color: darken(@dropdown-link-hover-bg, 5%);\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n #gradient > .vertical(@start-color: @dropdown-link-active-bg; @end-color: darken(@dropdown-link-active-bg, 5%));\n background-color: darken(@dropdown-link-active-bg, 5%);\n}\n\n\n\n//\n// Navbar\n// --------------------------------------------------\n\n// Default navbar\n.navbar-default {\n #gradient > .vertical(@start-color: lighten(@navbar-default-bg, 10%); @end-color: @navbar-default-bg);\n .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered\n border-radius: @navbar-border-radius;\n @shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 5px rgba(0,0,0,.075);\n .box-shadow(@shadow);\n\n .navbar-nav > .active > a {\n #gradient > .vertical(@start-color: darken(@navbar-default-bg, 5%); @end-color: darken(@navbar-default-bg, 2%));\n .box-shadow(inset 0 3px 9px rgba(0,0,0,.075));\n }\n}\n.navbar-brand,\n.navbar-nav > li > a {\n text-shadow: 0 1px 0 rgba(255,255,255,.25);\n}\n\n// Inverted navbar\n.navbar-inverse {\n #gradient > .vertical(@start-color: lighten(@navbar-inverse-bg, 10%); @end-color: @navbar-inverse-bg);\n .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered\n\n .navbar-nav > .active > a {\n #gradient > .vertical(@start-color: @navbar-inverse-bg; @end-color: lighten(@navbar-inverse-bg, 2.5%));\n .box-shadow(inset 0 3px 9px rgba(0,0,0,.25));\n }\n\n .navbar-brand,\n .navbar-nav > li > a {\n text-shadow: 0 -1px 0 rgba(0,0,0,.25);\n }\n}\n\n// Undo rounded corners in static and fixed navbars\n.navbar-static-top,\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n border-radius: 0;\n}\n\n\n\n//\n// Alerts\n// --------------------------------------------------\n\n// Common styles\n.alert {\n text-shadow: 0 1px 0 rgba(255,255,255,.2);\n @shadow: inset 0 1px 0 rgba(255,255,255,.25), 0 1px 2px rgba(0,0,0,.05);\n .box-shadow(@shadow);\n}\n\n// Mixin for generating new styles\n.alert-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 7.5%));\n border-color: darken(@color, 15%);\n}\n\n// Apply the mixin to the alerts\n.alert-success { .alert-styles(@alert-success-bg); }\n.alert-info { .alert-styles(@alert-info-bg); }\n.alert-warning { .alert-styles(@alert-warning-bg); }\n.alert-danger { .alert-styles(@alert-danger-bg); }\n\n\n\n//\n// Progress bars\n// --------------------------------------------------\n\n// Give the progress background some depth\n.progress {\n #gradient > .vertical(@start-color: darken(@progress-bg, 4%); @end-color: @progress-bg)\n}\n\n// Mixin for generating new styles\n.progress-bar-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 10%));\n}\n\n// Apply the mixin to the progress bars\n.progress-bar { .progress-bar-styles(@progress-bar-bg); }\n.progress-bar-success { .progress-bar-styles(@progress-bar-success-bg); }\n.progress-bar-info { .progress-bar-styles(@progress-bar-info-bg); }\n.progress-bar-warning { .progress-bar-styles(@progress-bar-warning-bg); }\n.progress-bar-danger { .progress-bar-styles(@progress-bar-danger-bg); }\n\n// Reset the striped class because our mixins don't do multiple gradients and\n// the above custom styles override the new `.progress-bar-striped` in v3.2.0.\n.progress-bar-striped {\n #gradient > .striped();\n}\n\n\n//\n// List groups\n// --------------------------------------------------\n\n.list-group {\n border-radius: @border-radius-base;\n .box-shadow(0 1px 2px rgba(0,0,0,.075));\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n text-shadow: 0 -1px 0 darken(@list-group-active-bg, 10%);\n #gradient > .vertical(@start-color: @list-group-active-bg; @end-color: darken(@list-group-active-bg, 7.5%));\n border-color: darken(@list-group-active-border, 7.5%);\n}\n\n\n\n//\n// Panels\n// --------------------------------------------------\n\n// Common styles\n.panel {\n .box-shadow(0 1px 2px rgba(0,0,0,.05));\n}\n\n// Mixin for generating new styles\n.panel-heading-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 5%));\n}\n\n// Apply the mixin to the panel headings only\n.panel-default > .panel-heading { .panel-heading-styles(@panel-default-heading-bg); }\n.panel-primary > .panel-heading { .panel-heading-styles(@panel-primary-heading-bg); }\n.panel-success > .panel-heading { .panel-heading-styles(@panel-success-heading-bg); }\n.panel-info > .panel-heading { .panel-heading-styles(@panel-info-heading-bg); }\n.panel-warning > .panel-heading { .panel-heading-styles(@panel-warning-heading-bg); }\n.panel-danger > .panel-heading { .panel-heading-styles(@panel-danger-heading-bg); }\n\n\n\n//\n// Wells\n// --------------------------------------------------\n\n.well {\n #gradient > .vertical(@start-color: darken(@well-bg, 5%); @end-color: @well-bg);\n border-color: darken(@well-bg, 10%);\n @shadow: inset 0 1px 3px rgba(0,0,0,.05), 0 1px 0 rgba(255,255,255,.1);\n .box-shadow(@shadow);\n}\n","// Vendor Prefixes\n//\n// All vendor mixins are deprecated as of v3.2.0 due to the introduction of\n// Autoprefixer in our Gruntfile. They will be removed in v4.\n\n// - Animations\n// - Backface visibility\n// - Box shadow\n// - Box sizing\n// - Content columns\n// - Hyphens\n// - Placeholder text\n// - Transformations\n// - Transitions\n// - User Select\n\n\n// Animations\n.animation(@animation) {\n -webkit-animation: @animation;\n -o-animation: @animation;\n animation: @animation;\n}\n.animation-name(@name) {\n -webkit-animation-name: @name;\n animation-name: @name;\n}\n.animation-duration(@duration) {\n -webkit-animation-duration: @duration;\n animation-duration: @duration;\n}\n.animation-timing-function(@timing-function) {\n -webkit-animation-timing-function: @timing-function;\n animation-timing-function: @timing-function;\n}\n.animation-delay(@delay) {\n -webkit-animation-delay: @delay;\n animation-delay: @delay;\n}\n.animation-iteration-count(@iteration-count) {\n -webkit-animation-iteration-count: @iteration-count;\n animation-iteration-count: @iteration-count;\n}\n.animation-direction(@direction) {\n -webkit-animation-direction: @direction;\n animation-direction: @direction;\n}\n.animation-fill-mode(@fill-mode) {\n -webkit-animation-fill-mode: @fill-mode;\n animation-fill-mode: @fill-mode;\n}\n\n// Backface visibility\n// Prevent browsers from flickering when using CSS 3D transforms.\n// Default value is `visible`, but can be changed to `hidden`\n\n.backface-visibility(@visibility){\n -webkit-backface-visibility: @visibility;\n -moz-backface-visibility: @visibility;\n backface-visibility: @visibility;\n}\n\n// Drop shadows\n//\n// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's\n// supported browsers that have box shadow capabilities now support it.\n\n.box-shadow(@shadow) {\n -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1\n box-shadow: @shadow;\n}\n\n// Box sizing\n.box-sizing(@boxmodel) {\n -webkit-box-sizing: @boxmodel;\n -moz-box-sizing: @boxmodel;\n box-sizing: @boxmodel;\n}\n\n// CSS3 Content Columns\n.content-columns(@column-count; @column-gap: @grid-gutter-width) {\n -webkit-column-count: @column-count;\n -moz-column-count: @column-count;\n column-count: @column-count;\n -webkit-column-gap: @column-gap;\n -moz-column-gap: @column-gap;\n column-gap: @column-gap;\n}\n\n// Optional hyphenation\n.hyphens(@mode: auto) {\n word-wrap: break-word;\n -webkit-hyphens: @mode;\n -moz-hyphens: @mode;\n -ms-hyphens: @mode; // IE10+\n -o-hyphens: @mode;\n hyphens: @mode;\n}\n\n// Placeholder text\n.placeholder(@color: @input-color-placeholder) {\n &::-moz-placeholder { color: @color; // Firefox\n opacity: 1; } // See https://github.com/twbs/bootstrap/pull/11526\n &:-ms-input-placeholder { color: @color; } // Internet Explorer 10+\n &::-webkit-input-placeholder { color: @color; } // Safari and Chrome\n}\n\n// Transformations\n.scale(@ratio) {\n -webkit-transform: scale(@ratio);\n -ms-transform: scale(@ratio); // IE9 only\n -o-transform: scale(@ratio);\n transform: scale(@ratio);\n}\n.scale(@ratioX; @ratioY) {\n -webkit-transform: scale(@ratioX, @ratioY);\n -ms-transform: scale(@ratioX, @ratioY); // IE9 only\n -o-transform: scale(@ratioX, @ratioY);\n transform: scale(@ratioX, @ratioY);\n}\n.scaleX(@ratio) {\n -webkit-transform: scaleX(@ratio);\n -ms-transform: scaleX(@ratio); // IE9 only\n -o-transform: scaleX(@ratio);\n transform: scaleX(@ratio);\n}\n.scaleY(@ratio) {\n -webkit-transform: scaleY(@ratio);\n -ms-transform: scaleY(@ratio); // IE9 only\n -o-transform: scaleY(@ratio);\n transform: scaleY(@ratio);\n}\n.skew(@x; @y) {\n -webkit-transform: skewX(@x) skewY(@y);\n -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+\n -o-transform: skewX(@x) skewY(@y);\n transform: skewX(@x) skewY(@y);\n}\n.translate(@x; @y) {\n -webkit-transform: translate(@x, @y);\n -ms-transform: translate(@x, @y); // IE9 only\n -o-transform: translate(@x, @y);\n transform: translate(@x, @y);\n}\n.translate3d(@x; @y; @z) {\n -webkit-transform: translate3d(@x, @y, @z);\n transform: translate3d(@x, @y, @z);\n}\n.rotate(@degrees) {\n -webkit-transform: rotate(@degrees);\n -ms-transform: rotate(@degrees); // IE9 only\n -o-transform: rotate(@degrees);\n transform: rotate(@degrees);\n}\n.rotateX(@degrees) {\n -webkit-transform: rotateX(@degrees);\n -ms-transform: rotateX(@degrees); // IE9 only\n -o-transform: rotateX(@degrees);\n transform: rotateX(@degrees);\n}\n.rotateY(@degrees) {\n -webkit-transform: rotateY(@degrees);\n -ms-transform: rotateY(@degrees); // IE9 only\n -o-transform: rotateY(@degrees);\n transform: rotateY(@degrees);\n}\n.perspective(@perspective) {\n -webkit-perspective: @perspective;\n -moz-perspective: @perspective;\n perspective: @perspective;\n}\n.perspective-origin(@perspective) {\n -webkit-perspective-origin: @perspective;\n -moz-perspective-origin: @perspective;\n perspective-origin: @perspective;\n}\n.transform-origin(@origin) {\n -webkit-transform-origin: @origin;\n -moz-transform-origin: @origin;\n -ms-transform-origin: @origin; // IE9 only\n transform-origin: @origin;\n}\n\n\n// Transitions\n\n.transition(@transition) {\n -webkit-transition: @transition;\n -o-transition: @transition;\n transition: @transition;\n}\n.transition-property(@transition-property) {\n -webkit-transition-property: @transition-property;\n transition-property: @transition-property;\n}\n.transition-delay(@transition-delay) {\n -webkit-transition-delay: @transition-delay;\n transition-delay: @transition-delay;\n}\n.transition-duration(@transition-duration) {\n -webkit-transition-duration: @transition-duration;\n transition-duration: @transition-duration;\n}\n.transition-timing-function(@timing-function) {\n -webkit-transition-timing-function: @timing-function;\n transition-timing-function: @timing-function;\n}\n.transition-transform(@transition) {\n -webkit-transition: -webkit-transform @transition;\n -moz-transition: -moz-transform @transition;\n -o-transition: -o-transform @transition;\n transition: transform @transition;\n}\n\n\n// User select\n// For selecting text on the page\n\n.user-select(@select) {\n -webkit-user-select: @select;\n -moz-user-select: @select;\n -ms-user-select: @select; // IE10+\n user-select: @select;\n}\n",null,"// Gradients\n\n#gradient {\n\n // Horizontal gradient, from left to right\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .horizontal(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Opera 12\n background-image: linear-gradient(to right, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n background-repeat: repeat-x;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\",argb(@start-color),argb(@end-color))); // IE9 and down\n }\n\n // Vertical gradient, from top to bottom\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .vertical(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Opera 12\n background-image: linear-gradient(to bottom, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n background-repeat: repeat-x;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\",argb(@start-color),argb(@end-color))); // IE9 and down\n }\n\n .directional(@start-color: #555; @end-color: #333; @deg: 45deg) {\n background-repeat: repeat-x;\n background-image: -webkit-linear-gradient(@deg, @start-color, @end-color); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(@deg, @start-color, @end-color); // Opera 12\n background-image: linear-gradient(@deg, @start-color, @end-color); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n }\n .horizontal-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color);\n background-image: -o-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(to right, @start-color, @mid-color @color-stop, @end-color);\n background-repeat: no-repeat;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n }\n .vertical-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-image: -o-linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-repeat: no-repeat;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n }\n .radial(@inner-color: #555; @outer-color: #333) {\n background-image: -webkit-radial-gradient(circle, @inner-color, @outer-color);\n background-image: radial-gradient(circle, @inner-color, @outer-color);\n background-repeat: no-repeat;\n }\n .striped(@color: rgba(255,255,255,.15); @angle: 45deg) {\n background-image: -webkit-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n background-image: linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n }\n}\n","// Reset filters for IE\n//\n// When you need to remove a gradient background, do not forget to use this to reset\n// the IE filter for IE9 and below.\n\n.reset-filter() {\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(enabled = false)\"));\n}\n"]} -------------------------------------------------------------------------------- /src/static/css/bootstrap-theme.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.2.0 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | .btn-default, 8 | .btn-primary, 9 | .btn-success, 10 | .btn-info, 11 | .btn-warning, 12 | .btn-danger { 13 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .2); 14 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); 15 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); 16 | } 17 | .btn-default:active, 18 | .btn-primary:active, 19 | .btn-success:active, 20 | .btn-info:active, 21 | .btn-warning:active, 22 | .btn-danger:active, 23 | .btn-default.active, 24 | .btn-primary.active, 25 | .btn-success.active, 26 | .btn-info.active, 27 | .btn-warning.active, 28 | .btn-danger.active { 29 | -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); 30 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); 31 | } 32 | .btn:active, 33 | .btn.active { 34 | background-image: none; 35 | } 36 | .btn-default { 37 | text-shadow: 0 1px 0 #fff; 38 | background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%); 39 | background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%); 40 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0)); 41 | background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%); 42 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0); 43 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 44 | background-repeat: repeat-x; 45 | border-color: #dbdbdb; 46 | border-color: #ccc; 47 | } 48 | .btn-default:hover, 49 | .btn-default:focus { 50 | background-color: #e0e0e0; 51 | background-position: 0 -15px; 52 | } 53 | .btn-default:active, 54 | .btn-default.active { 55 | background-color: #e0e0e0; 56 | border-color: #dbdbdb; 57 | } 58 | .btn-default:disabled, 59 | .btn-default[disabled] { 60 | background-color: #e0e0e0; 61 | background-image: none; 62 | } 63 | .btn-primary { 64 | background-image: -webkit-linear-gradient(top, #428bca 0%, #2d6ca2 100%); 65 | background-image: -o-linear-gradient(top, #428bca 0%, #2d6ca2 100%); 66 | background-image: -webkit-gradient(linear, left top, left bottom, from(#428bca), to(#2d6ca2)); 67 | background-image: linear-gradient(to bottom, #428bca 0%, #2d6ca2 100%); 68 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0); 69 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 70 | background-repeat: repeat-x; 71 | border-color: #2b669a; 72 | } 73 | .btn-primary:hover, 74 | .btn-primary:focus { 75 | background-color: #2d6ca2; 76 | background-position: 0 -15px; 77 | } 78 | .btn-primary:active, 79 | .btn-primary.active { 80 | background-color: #2d6ca2; 81 | border-color: #2b669a; 82 | } 83 | .btn-primary:disabled, 84 | .btn-primary[disabled] { 85 | background-color: #2d6ca2; 86 | background-image: none; 87 | } 88 | .btn-success { 89 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%); 90 | background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%); 91 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641)); 92 | background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%); 93 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0); 94 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 95 | background-repeat: repeat-x; 96 | border-color: #3e8f3e; 97 | } 98 | .btn-success:hover, 99 | .btn-success:focus { 100 | background-color: #419641; 101 | background-position: 0 -15px; 102 | } 103 | .btn-success:active, 104 | .btn-success.active { 105 | background-color: #419641; 106 | border-color: #3e8f3e; 107 | } 108 | .btn-success:disabled, 109 | .btn-success[disabled] { 110 | background-color: #419641; 111 | background-image: none; 112 | } 113 | .btn-info { 114 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); 115 | background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); 116 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2)); 117 | background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%); 118 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0); 119 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 120 | background-repeat: repeat-x; 121 | border-color: #28a4c9; 122 | } 123 | .btn-info:hover, 124 | .btn-info:focus { 125 | background-color: #2aabd2; 126 | background-position: 0 -15px; 127 | } 128 | .btn-info:active, 129 | .btn-info.active { 130 | background-color: #2aabd2; 131 | border-color: #28a4c9; 132 | } 133 | .btn-info:disabled, 134 | .btn-info[disabled] { 135 | background-color: #2aabd2; 136 | background-image: none; 137 | } 138 | .btn-warning { 139 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); 140 | background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); 141 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316)); 142 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%); 143 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0); 144 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 145 | background-repeat: repeat-x; 146 | border-color: #e38d13; 147 | } 148 | .btn-warning:hover, 149 | .btn-warning:focus { 150 | background-color: #eb9316; 151 | background-position: 0 -15px; 152 | } 153 | .btn-warning:active, 154 | .btn-warning.active { 155 | background-color: #eb9316; 156 | border-color: #e38d13; 157 | } 158 | .btn-warning:disabled, 159 | .btn-warning[disabled] { 160 | background-color: #eb9316; 161 | background-image: none; 162 | } 163 | .btn-danger { 164 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%); 165 | background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%); 166 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a)); 167 | background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%); 168 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0); 169 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 170 | background-repeat: repeat-x; 171 | border-color: #b92c28; 172 | } 173 | .btn-danger:hover, 174 | .btn-danger:focus { 175 | background-color: #c12e2a; 176 | background-position: 0 -15px; 177 | } 178 | .btn-danger:active, 179 | .btn-danger.active { 180 | background-color: #c12e2a; 181 | border-color: #b92c28; 182 | } 183 | .btn-danger:disabled, 184 | .btn-danger[disabled] { 185 | background-color: #c12e2a; 186 | background-image: none; 187 | } 188 | .thumbnail, 189 | .img-thumbnail { 190 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 191 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 192 | } 193 | .dropdown-menu > li > a:hover, 194 | .dropdown-menu > li > a:focus { 195 | background-color: #e8e8e8; 196 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 197 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 198 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); 199 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 200 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 201 | background-repeat: repeat-x; 202 | } 203 | .dropdown-menu > .active > a, 204 | .dropdown-menu > .active > a:hover, 205 | .dropdown-menu > .active > a:focus { 206 | background-color: #357ebd; 207 | background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%); 208 | background-image: -o-linear-gradient(top, #428bca 0%, #357ebd 100%); 209 | background-image: -webkit-gradient(linear, left top, left bottom, from(#428bca), to(#357ebd)); 210 | background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%); 211 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0); 212 | background-repeat: repeat-x; 213 | } 214 | .navbar-default { 215 | background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%); 216 | background-image: -o-linear-gradient(top, #fff 0%, #f8f8f8 100%); 217 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f8f8f8)); 218 | background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%); 219 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0); 220 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 221 | background-repeat: repeat-x; 222 | border-radius: 4px; 223 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); 224 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); 225 | } 226 | .navbar-default .navbar-nav > .active > a { 227 | background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f3f3f3 100%); 228 | background-image: -o-linear-gradient(top, #ebebeb 0%, #f3f3f3 100%); 229 | background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f3f3f3)); 230 | background-image: linear-gradient(to bottom, #ebebeb 0%, #f3f3f3 100%); 231 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0); 232 | background-repeat: repeat-x; 233 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); 234 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); 235 | } 236 | .navbar-brand, 237 | .navbar-nav > li > a { 238 | text-shadow: 0 1px 0 rgba(255, 255, 255, .25); 239 | } 240 | .navbar-inverse { 241 | background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%); 242 | background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%); 243 | background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222)); 244 | background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%); 245 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0); 246 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 247 | background-repeat: repeat-x; 248 | } 249 | .navbar-inverse .navbar-nav > .active > a { 250 | background-image: -webkit-linear-gradient(top, #222 0%, #282828 100%); 251 | background-image: -o-linear-gradient(top, #222 0%, #282828 100%); 252 | background-image: -webkit-gradient(linear, left top, left bottom, from(#222), to(#282828)); 253 | background-image: linear-gradient(to bottom, #222 0%, #282828 100%); 254 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0); 255 | background-repeat: repeat-x; 256 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); 257 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); 258 | } 259 | .navbar-inverse .navbar-brand, 260 | .navbar-inverse .navbar-nav > li > a { 261 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .25); 262 | } 263 | .navbar-static-top, 264 | .navbar-fixed-top, 265 | .navbar-fixed-bottom { 266 | border-radius: 0; 267 | } 268 | .alert { 269 | text-shadow: 0 1px 0 rgba(255, 255, 255, .2); 270 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); 271 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); 272 | } 273 | .alert-success { 274 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 275 | background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 276 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc)); 277 | background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%); 278 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0); 279 | background-repeat: repeat-x; 280 | border-color: #b2dba1; 281 | } 282 | .alert-info { 283 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 284 | background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 285 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0)); 286 | background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%); 287 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0); 288 | background-repeat: repeat-x; 289 | border-color: #9acfea; 290 | } 291 | .alert-warning { 292 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 293 | background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 294 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0)); 295 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%); 296 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0); 297 | background-repeat: repeat-x; 298 | border-color: #f5e79e; 299 | } 300 | .alert-danger { 301 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 302 | background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 303 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3)); 304 | background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%); 305 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0); 306 | background-repeat: repeat-x; 307 | border-color: #dca7a7; 308 | } 309 | .progress { 310 | background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 311 | background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 312 | background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5)); 313 | background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%); 314 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0); 315 | background-repeat: repeat-x; 316 | } 317 | .progress-bar { 318 | background-image: -webkit-linear-gradient(top, #428bca 0%, #3071a9 100%); 319 | background-image: -o-linear-gradient(top, #428bca 0%, #3071a9 100%); 320 | background-image: -webkit-gradient(linear, left top, left bottom, from(#428bca), to(#3071a9)); 321 | background-image: linear-gradient(to bottom, #428bca 0%, #3071a9 100%); 322 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0); 323 | background-repeat: repeat-x; 324 | } 325 | .progress-bar-success { 326 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%); 327 | background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%); 328 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44)); 329 | background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%); 330 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0); 331 | background-repeat: repeat-x; 332 | } 333 | .progress-bar-info { 334 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 335 | background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 336 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5)); 337 | background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%); 338 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0); 339 | background-repeat: repeat-x; 340 | } 341 | .progress-bar-warning { 342 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 343 | background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 344 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f)); 345 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%); 346 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0); 347 | background-repeat: repeat-x; 348 | } 349 | .progress-bar-danger { 350 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%); 351 | background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%); 352 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c)); 353 | background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%); 354 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0); 355 | background-repeat: repeat-x; 356 | } 357 | .progress-bar-striped { 358 | background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 359 | background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 360 | background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 361 | } 362 | .list-group { 363 | border-radius: 4px; 364 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 365 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 366 | } 367 | .list-group-item.active, 368 | .list-group-item.active:hover, 369 | .list-group-item.active:focus { 370 | text-shadow: 0 -1px 0 #3071a9; 371 | background-image: -webkit-linear-gradient(top, #428bca 0%, #3278b3 100%); 372 | background-image: -o-linear-gradient(top, #428bca 0%, #3278b3 100%); 373 | background-image: -webkit-gradient(linear, left top, left bottom, from(#428bca), to(#3278b3)); 374 | background-image: linear-gradient(to bottom, #428bca 0%, #3278b3 100%); 375 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0); 376 | background-repeat: repeat-x; 377 | border-color: #3278b3; 378 | } 379 | .panel { 380 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 381 | box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 382 | } 383 | .panel-default > .panel-heading { 384 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 385 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 386 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); 387 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 388 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 389 | background-repeat: repeat-x; 390 | } 391 | .panel-primary > .panel-heading { 392 | background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%); 393 | background-image: -o-linear-gradient(top, #428bca 0%, #357ebd 100%); 394 | background-image: -webkit-gradient(linear, left top, left bottom, from(#428bca), to(#357ebd)); 395 | background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%); 396 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0); 397 | background-repeat: repeat-x; 398 | } 399 | .panel-success > .panel-heading { 400 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 401 | background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 402 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6)); 403 | background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%); 404 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0); 405 | background-repeat: repeat-x; 406 | } 407 | .panel-info > .panel-heading { 408 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 409 | background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 410 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3)); 411 | background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%); 412 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0); 413 | background-repeat: repeat-x; 414 | } 415 | .panel-warning > .panel-heading { 416 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 417 | background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 418 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc)); 419 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%); 420 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0); 421 | background-repeat: repeat-x; 422 | } 423 | .panel-danger > .panel-heading { 424 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 425 | background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 426 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc)); 427 | background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%); 428 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0); 429 | background-repeat: repeat-x; 430 | } 431 | .well { 432 | background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 433 | background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 434 | background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5)); 435 | background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%); 436 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0); 437 | background-repeat: repeat-x; 438 | border-color: #dcdcdc; 439 | -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); 440 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); 441 | } 442 | /*# sourceMappingURL=bootstrap-theme.css.map */ 443 | -------------------------------------------------------------------------------- /src/static/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.2.0 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.2.0",d.prototype.close=function(b){function c(){f.detach().trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one("bsTransitionEnd",c).emulateTransitionEnd(150):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.2.0",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),d[e](null==f[b]?this.options[b]:f[b]),setTimeout(a.proxy(function(){"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}a&&this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),c.preventDefault()})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b).on("keydown.bs.carousel",a.proxy(this.keydown,this)),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.2.0",c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},c.prototype.keydown=function(a){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.to=function(b){var c=this,d=this.getItemIndex(this.$active=this.$element.find(".item.active"));return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}if(e.hasClass("active"))return this.sliding=!1;var j=e[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:g});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,f&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(e)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:g});return a.support.transition&&this.$element.hasClass("slide")?(e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one("bsTransitionEnd",function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(1e3*d.css("transition-duration").slice(0,-1))):(d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger(m)),f&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b);!e&&f.toggle&&"show"==b&&(b=!b),e||d.data("bs.collapse",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};c.VERSION="3.2.0",c.DEFAULTS={toggle:!0},c.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},c.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var c=a.Event("show.bs.collapse");if(this.$element.trigger(c),!c.isDefaultPrevented()){var d=this.$parent&&this.$parent.find("> .panel > .in");if(d&&d.length){var e=d.data("bs.collapse");if(e&&e.transitioning)return;b.call(d,"hide"),e||d.data("bs.collapse",null)}var f=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[f](0),this.transitioning=1;var g=function(){this.$element.removeClass("collapsing").addClass("collapse in")[f](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return g.call(this);var h=a.camelCase(["scroll",f].join("-"));this.$element.one("bsTransitionEnd",a.proxy(g,this)).emulateTransitionEnd(350)[f](this.$element[0][h])}}},c.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(d,this)).emulateTransitionEnd(350):d.call(this)}}},c.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var d=a.fn.collapse;a.fn.collapse=b,a.fn.collapse.Constructor=c,a.fn.collapse.noConflict=function(){return a.fn.collapse=d,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(c){var d,e=a(this),f=e.attr("data-target")||c.preventDefault()||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""),g=a(f),h=g.data("bs.collapse"),i=h?"toggle":e.data(),j=e.attr("data-parent"),k=j&&a(j);h&&h.transitioning||(k&&k.find('[data-toggle="collapse"][data-parent="'+j+'"]').not(e).addClass("collapsed"),e[g.hasClass("in")?"addClass":"removeClass"]("collapsed")),b.call(g,i)})}(jQuery),+function(a){"use strict";function b(b){b&&3===b.which||(a(e).remove(),a(f).each(function(){var d=c(a(this)),e={relatedTarget:this};d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown",e)),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown",e))}))}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.2.0",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('