├── .env ├── .gitignore ├── README.md ├── app.py ├── cron.py ├── models.py ├── requirements.txt ├── secrets-example.py ├── sniper.fcgi ├── soc.py ├── static ├── css │ ├── animate.css │ ├── bootstrap-responsive.css │ ├── bootstrap-responsive.min.css │ ├── bootstrap.css │ ├── bootstrap.min.css │ └── sniper.css ├── img │ ├── favicon.ico │ ├── glyphicons-halflings-white.png │ └── glyphicons-halflings.png └── js │ ├── app.js │ ├── bootstrap.js │ ├── bootstrap.min.js │ ├── jquery.min.js │ └── typeahead.js └── templates ├── _faq.html ├── _formhelpers.html ├── down.html ├── faq.html ├── home.html ├── layout.html └── success.html /.env: -------------------------------------------------------------------------------- 1 | use_env sniper 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.log 3 | *.db 4 | secrets.py 5 | ENV 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## THE SNIPER REPO HAS MOVED TO https://github.com/Rui-Zhang1997/sniper 2 | 3 | # Introduction 4 | 5 | [Sniper](http://sniper.rutgers.io) is an application that interfaces with the [Rutgers Schedule of Classes](http://sis.rutgers.edu/soc/) and notifies users via email/text when a course opens up. 6 | 7 | It uses 8 | * [Flask](http://flask.pocoo.org) for the application framework 9 | * [SQLAlchemy](http://www.sqlalchemy.org/) for ORM 10 | * [Requests](http://docs.python-requests.org/en/latest/index.html) for interfacing with the Rutgers Schedule of Classes. 11 | * [SendGrid](https://sendgrid.com) for email notifications. 12 | 13 | You can check out the [live version](http://sniper.rutgers.io) if you want to simply use its functionality 14 | 15 | # Installation 16 | 17 | You can setup an instance of sniper on your own Linux machine. 18 | 19 | 1. Start by setting up a [python virtualenv](http://lmgtfy.com/?q=setting+up+a+python+virtualenv) 20 | 2. Install the python packages in `requirements.txt` by running `pip install -r requirements.txt`. 21 | 3. Create a `db/` directory in the `sniper` folder (alongside `app.py`). 22 | 4. Create empty database tables by running `python -c "from models import db; db.create_all()"` 23 | 5. Copy `secrets-example.py` to `secrets.py`. Edit the mail_username and mail_password fields to your SendGrid account details (if you want sniper to send you email). 24 | 6. Run app.py `python app.py`. You should be able to visit `http://localhost:5000/` and input a class to snipe. 25 | 26 | When you input a snipe, you should see it under `db/production.db` in the user and snipe tables. 27 | 28 | If you run cron.py (`python cron.py`), all the courses in the database will be checked and the corresponding users will be notified if the class is open. 29 | You can run cron.py in a cronjob to automatically check for course openings. 30 | 31 | Example: Put ` */15 * * * * /path/to/virtualenv/bin/python cron.py ` into `crontab -e` to automatically check for openings every 15 minutes. 32 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | """ This file sets up the Flask Application for sniper. 2 | Sniper is an application that hits the Rutgers SOC API and notifies users when a class opens up. """ 3 | 4 | from flask import Flask, render_template, request 5 | from wtforms import Form, TextField, validators 6 | from wtforms.validators import StopValidation 7 | from models import Snipe, db, User 8 | from flaskext.mail import Mail 9 | from secrets import mail_username, mail_password 10 | from soc import Soc 11 | from werkzeug.contrib.fixers import ProxyFix 12 | import re 13 | import json 14 | 15 | import logging 16 | from logging import Formatter, FileHandler 17 | 18 | # Set up the Flask application 19 | app = Flask(__name__) 20 | 21 | # Set up a file for logging 22 | file_handler = FileHandler('everything.log') 23 | file_handler.setLevel(logging.INFO) 24 | file_handler.setFormatter(Formatter('%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]')) 25 | app.logger.addHandler(file_handler) 26 | 27 | app.wsgi_app = ProxyFix(app.wsgi_app) 28 | 29 | app.config['MAIL_SERVER'] = 'smtp.sendgrid.net' 30 | app.config['MAIL_PORT'] = 587 31 | app.config['MAIL_USE_TLS'] = True 32 | app.config['MAIL_USERNAME'] = mail_username 33 | app.config['MAIL_PASSWORD'] = mail_password 34 | 35 | mail = Mail(app) 36 | 37 | class SnipeForm(Form): 38 | """ Represents the Snipe form on the homepage. """ 39 | email = TextField('Email', [validators.Email(), validators.Required()]) 40 | subject = TextField('Subject') 41 | course_number = TextField('Course Number', [validators.Length(min=2, max=4), validators.NumberRange()]) 42 | section = TextField('Section', [validators.Length(min=1, max=4)]) 43 | 44 | def validate_subject(form, field): 45 | if not form.subject.data.isdigit(): 46 | m = re.search('(\d+)', form.subject.data) 47 | if m: 48 | form.subject.data = m.group(1) 49 | else: 50 | raise StopValidation('Please enter a valid subject') 51 | return True 52 | 53 | def validate_course_number(form, field): 54 | # course numbers sometime have leading zeroes 55 | if form.course_number.data.isdigit(): 56 | form.course_number.data = str(int(form.course_number.data)) 57 | return True 58 | 59 | def validate_section(form, field): 60 | if form.section.data.isdigit(): 61 | form.section.data = str(int(form.section.data)) 62 | return True 63 | 64 | def save(self): 65 | """ Saves to SQLAlchemy User and Snipe models """ 66 | 67 | snipe = Snipe.create(self.email.data, self.subject.data, self.course_number.data, self.section.data) 68 | 69 | db.session.add(snipe) 70 | db.session.commit() 71 | 72 | 73 | @app.route('/', methods=['GET', 'POST']) 74 | def home(): 75 | """ Handles the home page rendering.""" 76 | 77 | soc = Soc() 78 | subjects = soc.get_subjects() 79 | 80 | form = SnipeForm(request.form) 81 | if request.method == 'POST' and form.validate(): 82 | form.save() 83 | return render_template('success.html', form=form) 84 | if not request.form: 85 | # this trick allows us to prepopulate entries using links sent out in emails. 86 | form = SnipeForm(request.args) 87 | 88 | return render_template('home.html', form=form, subjects=subjects) 89 | 90 | @app.route('/faq', methods=['GET']) 91 | def faq(): 92 | return render_template('faq.html') 93 | 94 | @app.route('/test', methods=['GET', 'POST']) 95 | def ajaxtest(): 96 | result = { 97 | 'success': test(), 98 | } 99 | 100 | if not result['success']: 101 | from cron import EMAIL_SENDER 102 | from flaskext.mail import Message 103 | 104 | message = Message('Sniper tests are failing', sender=EMAIL_SENDER) 105 | message.body = 'FIX IT' 106 | message.add_recipient('vaibhav2614@gmail.com') 107 | mail.send(message) 108 | 109 | return json.dumps(result) 110 | 111 | def test(): 112 | from cron import poll 113 | 114 | soc = Soc() 115 | math_courses = soc.get_courses(640) 116 | open_courses = poll(640, result = True) 117 | for dept, sections in open_courses.iteritems(): 118 | open_courses[dept] = [section.number for section in sections] 119 | 120 | success = True 121 | 122 | for math_course in math_courses: 123 | course_number = math_course['courseNumber'] 124 | 125 | if course_number.isdigit(): 126 | course_number = str(int(course_number)) 127 | 128 | for section in math_course['sections']: 129 | section_number = section['number'] 130 | if section_number.isdigit(): 131 | section_number = str(int(section_number)) 132 | 133 | if section['openStatus'] and not section_number in open_courses[course_number]: 134 | raise Exception('Test failed') 135 | success = False 136 | 137 | return success 138 | 139 | if __name__ == '__main__': 140 | test() 141 | app.run(host='0.0.0.0', debug=True) 142 | -------------------------------------------------------------------------------- /cron.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ This represents the cronjob that runs to check for course openings""" 3 | from flaskext.mail import Message 4 | 5 | import urllib 6 | from models import db, Snipe 7 | from soc import Soc 8 | from app import mail, app 9 | import datetime 10 | from collections import namedtuple 11 | 12 | soc = Soc() 13 | 14 | EMAIL_SENDER = "Course Sniper " 15 | 16 | Section = namedtuple('Section', ['number', 'index']) 17 | 18 | def poll(subject, result=False): 19 | """ Poll a subject for open courses. """ 20 | app.logger.warning("Polling for %s" % (subject)) 21 | 22 | # get all the course data from SOC 23 | courses = soc.get_courses(subject) 24 | 25 | # build information about which courses/sections are currently open. 26 | open_data = {} 27 | if courses is not None: 28 | for course in courses: 29 | course_number = course['courseNumber'] 30 | 31 | # remove leading zeroes 32 | if course_number.isdigit(): 33 | course_number = str(int(course_number)) 34 | 35 | open_data[course_number] = [] 36 | for section in course['sections']: 37 | section_number = section['number'] 38 | if section_number.isdigit(): 39 | section_number = str(int(section_number)) 40 | # section is open 41 | if section['openStatus']: 42 | open_data[course_number].append(Section(section_number, section['index'])) 43 | 44 | # all of these course numbers are open 45 | open_courses = [course for course, open_sections in open_data.iteritems() if open_sections] 46 | 47 | if result: 48 | return open_data 49 | 50 | if open_courses: 51 | # Notify people that were looking for these courses 52 | snipes = Snipe.query.filter(Snipe.course_number.in_(open_courses), Snipe.subject==str(subject)) 53 | for snipe in snipes: 54 | for section in open_data[snipe.course_number]: 55 | if section.number == snipe.section: 56 | notify(snipe, section.index) 57 | else: 58 | app.logger.warning('Subject "%s" has no open courses' % (subject)) 59 | else: 60 | app.logger.warning('Subject "%s" is not valid' % (subject)) 61 | 62 | def notify(snipe, index): 63 | """ Notify this snipe that their course is open""" 64 | course = '%s:%s:%s' % (snipe.subject, snipe.course_number, snipe.section) 65 | 66 | if snipe.user.email: 67 | 68 | attributes = { 69 | 'email': snipe.user.email, 70 | 'subject': snipe.subject, 71 | 'course_number': snipe.course_number, 72 | 'section': snipe.section, 73 | } 74 | 75 | # build the url for prepopulated form 76 | url = 'http://sniper.rutgers.io/?%s' % (urllib.urlencode(attributes)) 77 | 78 | register_url = 'https://sims.rutgers.edu/webreg/editSchedule.htm?login=cas&semesterSelection=12018&indexList=%s' % (index) 79 | 80 | email_text = 'A course (%s) that you were watching looks open. Its index number is %s. Click the link below to register for it!\n\n %s \n\n If you don\'t get in, visit this URL: \n\n %s \n\n to continue watching it.\n\n Send any feedback to sniper@rutgers.io' % (course, index, register_url, url) 81 | 82 | # send out the email 83 | message = Message('[Course Sniper](%s) is open' %(course), sender=EMAIL_SENDER) 84 | message.body = email_text 85 | message.add_recipient(snipe.user.email) 86 | message.add_recipient(snipe.user.email) 87 | 88 | mail.send(message) 89 | 90 | db.session.delete(snipe) 91 | db.session.commit() 92 | 93 | app.logger.warning('Notified user: %s about snipe %s' % (snipe.user, snipe)) 94 | 95 | 96 | 97 | if __name__ == '__main__': 98 | # get all the courses that should be queried. 99 | app.logger.warning("----------- Running the Cron %s " % (str(datetime.datetime.now()))) 100 | subjects = db.session.query(Snipe.subject).distinct().all() 101 | for subject in subjects: 102 | poll(subject[0]) 103 | -------------------------------------------------------------------------------- /models.py: -------------------------------------------------------------------------------- 1 | """ Represents the persistent models for the sniper application""" 2 | from flask.ext.sqlalchemy import SQLAlchemy 3 | from flask import Flask 4 | 5 | app = Flask(__name__) 6 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db/production.db' 7 | db = SQLAlchemy(app) 8 | 9 | class Snipe(db.Model): 10 | """ A snipe model represents the course info pertaining to a snipe""" 11 | id = db.Column(db.Integer, primary_key=True) 12 | subject = db.Column(db.String(8)) 13 | course_number = db.Column(db.String(8)) 14 | section = db.Column(db.String(8)) 15 | # set up the many to one relationship with the User model. 16 | user_id = db.Column(db.Integer, db.ForeignKey('user.id')) 17 | 18 | @classmethod 19 | def create(cls, email, subject, course_number, section): 20 | """ Creates a snipe, and its corresponding user if they don't already exist""" 21 | # see if the user exists already 22 | user = User.query.filter_by(email=email).first() 23 | 24 | if not user: 25 | return Snipe(email, subject, course_number, section) 26 | 27 | # see if the snipe exists already 28 | snipe = Snipe.query.filter_by(user=user, subject=subject, course_number=course_number, section=section).first() 29 | if not snipe: 30 | return Snipe(email, subject, course_number, section) 31 | 32 | return snipe 33 | 34 | 35 | 36 | def __init__(self, email, subject, course_number, section): 37 | user = User.query.filter_by(email=email).first() 38 | if user: 39 | self.user = user 40 | else: 41 | user = User(email) 42 | 43 | self.subject = subject 44 | self.course_number = course_number 45 | self.section = section 46 | self.user = user 47 | 48 | def __repr__(self): 49 | return '(%s:%s:%s)' % (self.subject, self.course_number, self.section) 50 | 51 | class User(db.Model): 52 | """ Represents a user in the database (phone_number and email pair). """ 53 | id = db.Column(db.Integer, primary_key=True) 54 | email = db.Column(db.String(255)) 55 | phone_number = db.Column(db.String(16)) 56 | # Set up the one to many relationship with the Snipe model. 57 | snipes = db.relationship('Snipe', backref='user') 58 | 59 | def __init__(self, email=None, phone_number=None): 60 | if not email: 61 | raise Exception('I don\'t have an email for a user') 62 | 63 | self.email = email 64 | 65 | def __repr__(self): 66 | return '(%s)' % (self.email) 67 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==0.9 2 | Flask-Mail==0.6.1 3 | Flask-SQLAlchemy==0.16 4 | Flask-WTF==0.8 5 | Jinja2==2.6 6 | Mako==0.7.2 7 | MarkupSafe==0.15 8 | PyJWT==0.1.2 9 | SQLAlchemy==0.7.8 10 | WTForms==1.0.1 11 | Werkzeug==0.8.3 12 | alembic==0.3.5 13 | argparse==1.2.1 14 | blinker==1.2 15 | chardet==1.1 16 | ## FIXME: could not find svn URL in dependency_links for this package: 17 | flup==1.0.3.dev-20110405 18 | gunicorn==0.15.0 19 | httplib2==0.7.4 20 | lamson==1.1 21 | lockfile==0.9.1 22 | mock==1.0b1 23 | nose==1.1.2 24 | python-daemon==1.6 25 | requests==0.13.5 26 | twilio==3.3.10 27 | wsgiref==0.1.2 28 | -------------------------------------------------------------------------------- /secrets-example.py: -------------------------------------------------------------------------------- 1 | mail_username="" 2 | mail_password="" 3 | -------------------------------------------------------------------------------- /sniper.fcgi: -------------------------------------------------------------------------------- 1 | #!/var/virtualenvs/sniper/bin/python 2 | """ This is FastCGI process that communicates with lighty""" 3 | from flup.server.fcgi import WSGIServer 4 | from app import app 5 | 6 | class ScriptNameStripper(object): 7 | to_strip = '/sniper.fcgi' 8 | 9 | def __init__(self, app): 10 | self.app = app 11 | 12 | def __call__(self, environ, start_response): 13 | environ['SCRIPT_NAME'] = '' 14 | return self.app(environ, start_response) 15 | 16 | app = ScriptNameStripper(app) 17 | 18 | if __name__ == '__main__': 19 | WSGIServer(app, bindAddress='/tmp/sniper-fcgi.sock-0').run() 20 | -------------------------------------------------------------------------------- /soc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ This file implements a facade for the Rutgers Schedule of Classes API.""" 3 | 4 | # Requests is so awesome 5 | import requests 6 | 7 | class Soc: 8 | """ Communicates with Rutgers SOC """ 9 | def __init__(self, campus='NB', semester='12018', level='U,G'): 10 | """ We always use certain parameters""" 11 | self.base_url = 'http://sis.rutgers.edu/soc' 12 | self.params = { 13 | 'campus': campus, 14 | 'semester': semester, 15 | 'level': level, 16 | } 17 | 18 | # Spoof the user agent for good measure 19 | self.headers = { 20 | 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.60 Safari/537.1', 21 | } 22 | 23 | 24 | def query(self, resource, params): 25 | """Queries the given resource (a string) with the given parameters. 26 | For example self.query('/api/subjects.json', { 'keyword': 'Computer Science' })""" 27 | params.update(self.params) 28 | 29 | r = requests.get(self.base_url + resource, params=params, headers=self.headers) 30 | 31 | if r.status_code == requests.codes.ok: 32 | return r.json 33 | 34 | raise Exception('You made an invalid request %s: %s' % (r.status_code, r.text)) 35 | 36 | def get_subjects(self, **kwargs): 37 | """ Gives you a list of subjects (departments) """ 38 | return self.query('/subjects.json', params=kwargs) 39 | 40 | def get_courses(self, subject): 41 | """ Gives you a list of courses in a department """ 42 | return self.query('/courses.json', params={'subject': subject}) 43 | 44 | if __name__ == '__main__': 45 | soc = Soc() 46 | #print soc.get_courses(subject=198) 47 | asdf = soc.get_subjects() 48 | import pdb; pdb.set_trace() 49 | -------------------------------------------------------------------------------- /static/css/bootstrap-responsive.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.0.4 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */ 10 | 11 | .clearfix { 12 | *zoom: 1; 13 | } 14 | 15 | .clearfix:before, 16 | .clearfix:after { 17 | display: table; 18 | content: ""; 19 | } 20 | 21 | .clearfix:after { 22 | clear: both; 23 | } 24 | 25 | .hide-text { 26 | font: 0/0 a; 27 | color: transparent; 28 | text-shadow: none; 29 | background-color: transparent; 30 | border: 0; 31 | } 32 | 33 | .input-block-level { 34 | display: block; 35 | width: 100%; 36 | min-height: 28px; 37 | -webkit-box-sizing: border-box; 38 | -moz-box-sizing: border-box; 39 | -ms-box-sizing: border-box; 40 | box-sizing: border-box; 41 | } 42 | 43 | .hidden { 44 | display: none; 45 | visibility: hidden; 46 | } 47 | 48 | .visible-phone { 49 | display: none !important; 50 | } 51 | 52 | .visible-tablet { 53 | display: none !important; 54 | } 55 | 56 | .hidden-desktop { 57 | display: none !important; 58 | } 59 | 60 | @media (max-width: 767px) { 61 | .visible-phone { 62 | display: inherit !important; 63 | } 64 | .hidden-phone { 65 | display: none !important; 66 | } 67 | .hidden-desktop { 68 | display: inherit !important; 69 | } 70 | .visible-desktop { 71 | display: none !important; 72 | } 73 | } 74 | 75 | @media (min-width: 768px) and (max-width: 979px) { 76 | .visible-tablet { 77 | display: inherit !important; 78 | } 79 | .hidden-tablet { 80 | display: none !important; 81 | } 82 | .hidden-desktop { 83 | display: inherit !important; 84 | } 85 | .visible-desktop { 86 | display: none !important ; 87 | } 88 | } 89 | 90 | @media (max-width: 480px) { 91 | .nav-collapse { 92 | -webkit-transform: translate3d(0, 0, 0); 93 | } 94 | .page-header h1 small { 95 | display: block; 96 | line-height: 18px; 97 | } 98 | input[type="checkbox"], 99 | input[type="radio"] { 100 | border: 1px solid #ccc; 101 | } 102 | .form-horizontal .control-group > label { 103 | float: none; 104 | width: auto; 105 | padding-top: 0; 106 | text-align: left; 107 | } 108 | .form-horizontal .controls { 109 | margin-left: 0; 110 | } 111 | .form-horizontal .control-list { 112 | padding-top: 0; 113 | } 114 | .form-horizontal .form-actions { 115 | padding-right: 10px; 116 | padding-left: 10px; 117 | } 118 | .modal { 119 | position: absolute; 120 | top: 10px; 121 | right: 10px; 122 | left: 10px; 123 | width: auto; 124 | margin: 0; 125 | } 126 | .modal.fade.in { 127 | top: auto; 128 | } 129 | .modal-header .close { 130 | padding: 10px; 131 | margin: -10px; 132 | } 133 | .carousel-caption { 134 | position: static; 135 | } 136 | } 137 | 138 | @media (max-width: 767px) { 139 | body { 140 | padding-right: 20px; 141 | padding-left: 20px; 142 | } 143 | .navbar-fixed-top, 144 | .navbar-fixed-bottom { 145 | margin-right: -20px; 146 | margin-left: -20px; 147 | } 148 | .container-fluid { 149 | padding: 0; 150 | } 151 | .dl-horizontal dt { 152 | float: none; 153 | width: auto; 154 | clear: none; 155 | text-align: left; 156 | } 157 | .dl-horizontal dd { 158 | margin-left: 0; 159 | } 160 | .container { 161 | width: auto; 162 | } 163 | .row-fluid { 164 | width: 100%; 165 | } 166 | .row, 167 | .thumbnails { 168 | margin-left: 0; 169 | } 170 | [class*="span"], 171 | .row-fluid [class*="span"] { 172 | display: block; 173 | float: none; 174 | width: auto; 175 | margin-left: 0; 176 | } 177 | .input-large, 178 | .input-xlarge, 179 | .input-xxlarge, 180 | input[class*="span"], 181 | select[class*="span"], 182 | textarea[class*="span"], 183 | .uneditable-input { 184 | display: block; 185 | width: 100%; 186 | min-height: 28px; 187 | -webkit-box-sizing: border-box; 188 | -moz-box-sizing: border-box; 189 | -ms-box-sizing: border-box; 190 | box-sizing: border-box; 191 | } 192 | .input-prepend input, 193 | .input-append input, 194 | .input-prepend input[class*="span"], 195 | .input-append input[class*="span"] { 196 | display: inline-block; 197 | width: auto; 198 | } 199 | } 200 | 201 | @media (min-width: 768px) and (max-width: 979px) { 202 | .row { 203 | margin-left: -20px; 204 | *zoom: 1; 205 | } 206 | .row:before, 207 | .row:after { 208 | display: table; 209 | content: ""; 210 | } 211 | .row:after { 212 | clear: both; 213 | } 214 | [class*="span"] { 215 | float: left; 216 | margin-left: 20px; 217 | } 218 | .container, 219 | .navbar-fixed-top .container, 220 | .navbar-fixed-bottom .container { 221 | width: 724px; 222 | } 223 | .span12 { 224 | width: 724px; 225 | } 226 | .span11 { 227 | width: 662px; 228 | } 229 | .span10 { 230 | width: 600px; 231 | } 232 | .span9 { 233 | width: 538px; 234 | } 235 | .span8 { 236 | width: 476px; 237 | } 238 | .span7 { 239 | width: 414px; 240 | } 241 | .span6 { 242 | width: 352px; 243 | } 244 | .span5 { 245 | width: 290px; 246 | } 247 | .span4 { 248 | width: 228px; 249 | } 250 | .span3 { 251 | width: 166px; 252 | } 253 | .span2 { 254 | width: 104px; 255 | } 256 | .span1 { 257 | width: 42px; 258 | } 259 | .offset12 { 260 | margin-left: 764px; 261 | } 262 | .offset11 { 263 | margin-left: 702px; 264 | } 265 | .offset10 { 266 | margin-left: 640px; 267 | } 268 | .offset9 { 269 | margin-left: 578px; 270 | } 271 | .offset8 { 272 | margin-left: 516px; 273 | } 274 | .offset7 { 275 | margin-left: 454px; 276 | } 277 | .offset6 { 278 | margin-left: 392px; 279 | } 280 | .offset5 { 281 | margin-left: 330px; 282 | } 283 | .offset4 { 284 | margin-left: 268px; 285 | } 286 | .offset3 { 287 | margin-left: 206px; 288 | } 289 | .offset2 { 290 | margin-left: 144px; 291 | } 292 | .offset1 { 293 | margin-left: 82px; 294 | } 295 | .row-fluid { 296 | width: 100%; 297 | *zoom: 1; 298 | } 299 | .row-fluid:before, 300 | .row-fluid:after { 301 | display: table; 302 | content: ""; 303 | } 304 | .row-fluid:after { 305 | clear: both; 306 | } 307 | .row-fluid [class*="span"] { 308 | display: block; 309 | float: left; 310 | width: 100%; 311 | min-height: 28px; 312 | margin-left: 2.762430939%; 313 | *margin-left: 2.709239449638298%; 314 | -webkit-box-sizing: border-box; 315 | -moz-box-sizing: border-box; 316 | -ms-box-sizing: border-box; 317 | box-sizing: border-box; 318 | } 319 | .row-fluid [class*="span"]:first-child { 320 | margin-left: 0; 321 | } 322 | .row-fluid .span12 { 323 | width: 99.999999993%; 324 | *width: 99.9468085036383%; 325 | } 326 | .row-fluid .span11 { 327 | width: 91.436464082%; 328 | *width: 91.38327259263829%; 329 | } 330 | .row-fluid .span10 { 331 | width: 82.87292817100001%; 332 | *width: 82.8197366816383%; 333 | } 334 | .row-fluid .span9 { 335 | width: 74.30939226%; 336 | *width: 74.25620077063829%; 337 | } 338 | .row-fluid .span8 { 339 | width: 65.74585634900001%; 340 | *width: 65.6926648596383%; 341 | } 342 | .row-fluid .span7 { 343 | width: 57.182320438000005%; 344 | *width: 57.129128948638304%; 345 | } 346 | .row-fluid .span6 { 347 | width: 48.618784527%; 348 | *width: 48.5655930376383%; 349 | } 350 | .row-fluid .span5 { 351 | width: 40.055248616%; 352 | *width: 40.0020571266383%; 353 | } 354 | .row-fluid .span4 { 355 | width: 31.491712705%; 356 | *width: 31.4385212156383%; 357 | } 358 | .row-fluid .span3 { 359 | width: 22.928176794%; 360 | *width: 22.874985304638297%; 361 | } 362 | .row-fluid .span2 { 363 | width: 14.364640883%; 364 | *width: 14.311449393638298%; 365 | } 366 | .row-fluid .span1 { 367 | width: 5.801104972%; 368 | *width: 5.747913482638298%; 369 | } 370 | input, 371 | textarea, 372 | .uneditable-input { 373 | margin-left: 0; 374 | } 375 | input.span12, 376 | textarea.span12, 377 | .uneditable-input.span12 { 378 | width: 714px; 379 | } 380 | input.span11, 381 | textarea.span11, 382 | .uneditable-input.span11 { 383 | width: 652px; 384 | } 385 | input.span10, 386 | textarea.span10, 387 | .uneditable-input.span10 { 388 | width: 590px; 389 | } 390 | input.span9, 391 | textarea.span9, 392 | .uneditable-input.span9 { 393 | width: 528px; 394 | } 395 | input.span8, 396 | textarea.span8, 397 | .uneditable-input.span8 { 398 | width: 466px; 399 | } 400 | input.span7, 401 | textarea.span7, 402 | .uneditable-input.span7 { 403 | width: 404px; 404 | } 405 | input.span6, 406 | textarea.span6, 407 | .uneditable-input.span6 { 408 | width: 342px; 409 | } 410 | input.span5, 411 | textarea.span5, 412 | .uneditable-input.span5 { 413 | width: 280px; 414 | } 415 | input.span4, 416 | textarea.span4, 417 | .uneditable-input.span4 { 418 | width: 218px; 419 | } 420 | input.span3, 421 | textarea.span3, 422 | .uneditable-input.span3 { 423 | width: 156px; 424 | } 425 | input.span2, 426 | textarea.span2, 427 | .uneditable-input.span2 { 428 | width: 94px; 429 | } 430 | input.span1, 431 | textarea.span1, 432 | .uneditable-input.span1 { 433 | width: 32px; 434 | } 435 | } 436 | 437 | @media (min-width: 1200px) { 438 | .row { 439 | margin-left: -30px; 440 | *zoom: 1; 441 | } 442 | .row:before, 443 | .row:after { 444 | display: table; 445 | content: ""; 446 | } 447 | .row:after { 448 | clear: both; 449 | } 450 | [class*="span"] { 451 | float: left; 452 | margin-left: 30px; 453 | } 454 | .container, 455 | .navbar-fixed-top .container, 456 | .navbar-fixed-bottom .container { 457 | width: 1170px; 458 | } 459 | .span12 { 460 | width: 1170px; 461 | } 462 | .span11 { 463 | width: 1070px; 464 | } 465 | .span10 { 466 | width: 970px; 467 | } 468 | .span9 { 469 | width: 870px; 470 | } 471 | .span8 { 472 | width: 770px; 473 | } 474 | .span7 { 475 | width: 670px; 476 | } 477 | .span6 { 478 | width: 570px; 479 | } 480 | .span5 { 481 | width: 470px; 482 | } 483 | .span4 { 484 | width: 370px; 485 | } 486 | .span3 { 487 | width: 270px; 488 | } 489 | .span2 { 490 | width: 170px; 491 | } 492 | .span1 { 493 | width: 70px; 494 | } 495 | .offset12 { 496 | margin-left: 1230px; 497 | } 498 | .offset11 { 499 | margin-left: 1130px; 500 | } 501 | .offset10 { 502 | margin-left: 1030px; 503 | } 504 | .offset9 { 505 | margin-left: 930px; 506 | } 507 | .offset8 { 508 | margin-left: 830px; 509 | } 510 | .offset7 { 511 | margin-left: 730px; 512 | } 513 | .offset6 { 514 | margin-left: 630px; 515 | } 516 | .offset5 { 517 | margin-left: 530px; 518 | } 519 | .offset4 { 520 | margin-left: 430px; 521 | } 522 | .offset3 { 523 | margin-left: 330px; 524 | } 525 | .offset2 { 526 | margin-left: 230px; 527 | } 528 | .offset1 { 529 | margin-left: 130px; 530 | } 531 | .row-fluid { 532 | width: 100%; 533 | *zoom: 1; 534 | } 535 | .row-fluid:before, 536 | .row-fluid:after { 537 | display: table; 538 | content: ""; 539 | } 540 | .row-fluid:after { 541 | clear: both; 542 | } 543 | .row-fluid [class*="span"] { 544 | display: block; 545 | float: left; 546 | width: 100%; 547 | min-height: 28px; 548 | margin-left: 2.564102564%; 549 | *margin-left: 2.510911074638298%; 550 | -webkit-box-sizing: border-box; 551 | -moz-box-sizing: border-box; 552 | -ms-box-sizing: border-box; 553 | box-sizing: border-box; 554 | } 555 | .row-fluid [class*="span"]:first-child { 556 | margin-left: 0; 557 | } 558 | .row-fluid .span12 { 559 | width: 100%; 560 | *width: 99.94680851063829%; 561 | } 562 | .row-fluid .span11 { 563 | width: 91.45299145300001%; 564 | *width: 91.3997999636383%; 565 | } 566 | .row-fluid .span10 { 567 | width: 82.905982906%; 568 | *width: 82.8527914166383%; 569 | } 570 | .row-fluid .span9 { 571 | width: 74.358974359%; 572 | *width: 74.30578286963829%; 573 | } 574 | .row-fluid .span8 { 575 | width: 65.81196581200001%; 576 | *width: 65.7587743226383%; 577 | } 578 | .row-fluid .span7 { 579 | width: 57.264957265%; 580 | *width: 57.2117657756383%; 581 | } 582 | .row-fluid .span6 { 583 | width: 48.717948718%; 584 | *width: 48.6647572286383%; 585 | } 586 | .row-fluid .span5 { 587 | width: 40.170940171000005%; 588 | *width: 40.117748681638304%; 589 | } 590 | .row-fluid .span4 { 591 | width: 31.623931624%; 592 | *width: 31.5707401346383%; 593 | } 594 | .row-fluid .span3 { 595 | width: 23.076923077%; 596 | *width: 23.0237315876383%; 597 | } 598 | .row-fluid .span2 { 599 | width: 14.529914530000001%; 600 | *width: 14.4767230406383%; 601 | } 602 | .row-fluid .span1 { 603 | width: 5.982905983%; 604 | *width: 5.929714493638298%; 605 | } 606 | input, 607 | textarea, 608 | .uneditable-input { 609 | margin-left: 0; 610 | } 611 | input.span12, 612 | textarea.span12, 613 | .uneditable-input.span12 { 614 | width: 1160px; 615 | } 616 | input.span11, 617 | textarea.span11, 618 | .uneditable-input.span11 { 619 | width: 1060px; 620 | } 621 | input.span10, 622 | textarea.span10, 623 | .uneditable-input.span10 { 624 | width: 960px; 625 | } 626 | input.span9, 627 | textarea.span9, 628 | .uneditable-input.span9 { 629 | width: 860px; 630 | } 631 | input.span8, 632 | textarea.span8, 633 | .uneditable-input.span8 { 634 | width: 760px; 635 | } 636 | input.span7, 637 | textarea.span7, 638 | .uneditable-input.span7 { 639 | width: 660px; 640 | } 641 | input.span6, 642 | textarea.span6, 643 | .uneditable-input.span6 { 644 | width: 560px; 645 | } 646 | input.span5, 647 | textarea.span5, 648 | .uneditable-input.span5 { 649 | width: 460px; 650 | } 651 | input.span4, 652 | textarea.span4, 653 | .uneditable-input.span4 { 654 | width: 360px; 655 | } 656 | input.span3, 657 | textarea.span3, 658 | .uneditable-input.span3 { 659 | width: 260px; 660 | } 661 | input.span2, 662 | textarea.span2, 663 | .uneditable-input.span2 { 664 | width: 160px; 665 | } 666 | input.span1, 667 | textarea.span1, 668 | .uneditable-input.span1 { 669 | width: 60px; 670 | } 671 | .thumbnails { 672 | margin-left: -30px; 673 | } 674 | .thumbnails > li { 675 | margin-left: 30px; 676 | } 677 | .row-fluid .thumbnails { 678 | margin-left: 0; 679 | } 680 | } 681 | 682 | @media (max-width: 979px) { 683 | body { 684 | padding-top: 0; 685 | } 686 | .navbar-fixed-top, 687 | .navbar-fixed-bottom { 688 | position: static; 689 | } 690 | .navbar-fixed-top { 691 | margin-bottom: 18px; 692 | } 693 | .navbar-fixed-bottom { 694 | margin-top: 18px; 695 | } 696 | .navbar-fixed-top .navbar-inner, 697 | .navbar-fixed-bottom .navbar-inner { 698 | padding: 5px; 699 | } 700 | .navbar .container { 701 | width: auto; 702 | padding: 0; 703 | } 704 | .navbar .brand { 705 | padding-right: 10px; 706 | padding-left: 10px; 707 | margin: 0 0 0 -5px; 708 | } 709 | .nav-collapse { 710 | clear: both; 711 | } 712 | .nav-collapse .nav { 713 | float: none; 714 | margin: 0 0 9px; 715 | } 716 | .nav-collapse .nav > li { 717 | float: none; 718 | } 719 | .nav-collapse .nav > li > a { 720 | margin-bottom: 2px; 721 | } 722 | .nav-collapse .nav > .divider-vertical { 723 | display: none; 724 | } 725 | .nav-collapse .nav .nav-header { 726 | color: #999999; 727 | text-shadow: none; 728 | } 729 | .nav-collapse .nav > li > a, 730 | .nav-collapse .dropdown-menu a { 731 | padding: 6px 15px; 732 | font-weight: bold; 733 | color: #999999; 734 | -webkit-border-radius: 3px; 735 | -moz-border-radius: 3px; 736 | border-radius: 3px; 737 | } 738 | .nav-collapse .btn { 739 | padding: 4px 10px 4px; 740 | font-weight: normal; 741 | -webkit-border-radius: 4px; 742 | -moz-border-radius: 4px; 743 | border-radius: 4px; 744 | } 745 | .nav-collapse .dropdown-menu li + li a { 746 | margin-bottom: 2px; 747 | } 748 | .nav-collapse .nav > li > a:hover, 749 | .nav-collapse .dropdown-menu a:hover { 750 | background-color: #222222; 751 | } 752 | .nav-collapse.in .btn-group { 753 | padding: 0; 754 | margin-top: 5px; 755 | } 756 | .nav-collapse .dropdown-menu { 757 | position: static; 758 | top: auto; 759 | left: auto; 760 | display: block; 761 | float: none; 762 | max-width: none; 763 | padding: 0; 764 | margin: 0 15px; 765 | background-color: transparent; 766 | border: none; 767 | -webkit-border-radius: 0; 768 | -moz-border-radius: 0; 769 | border-radius: 0; 770 | -webkit-box-shadow: none; 771 | -moz-box-shadow: none; 772 | box-shadow: none; 773 | } 774 | .nav-collapse .dropdown-menu:before, 775 | .nav-collapse .dropdown-menu:after { 776 | display: none; 777 | } 778 | .nav-collapse .dropdown-menu .divider { 779 | display: none; 780 | } 781 | .nav-collapse .navbar-form, 782 | .nav-collapse .navbar-search { 783 | float: none; 784 | padding: 9px 15px; 785 | margin: 9px 0; 786 | border-top: 1px solid #222222; 787 | border-bottom: 1px solid #222222; 788 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 789 | -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 790 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 791 | } 792 | .navbar .nav-collapse .nav.pull-right { 793 | float: none; 794 | margin-left: 0; 795 | } 796 | .nav-collapse, 797 | .nav-collapse.collapse { 798 | height: 0; 799 | overflow: hidden; 800 | } 801 | .navbar .btn-navbar { 802 | display: block; 803 | } 804 | .navbar-static .navbar-inner { 805 | padding-right: 10px; 806 | padding-left: 10px; 807 | } 808 | } 809 | 810 | @media (min-width: 980px) { 811 | .nav-collapse.collapse { 812 | height: auto !important; 813 | overflow: visible !important; 814 | } 815 | } 816 | -------------------------------------------------------------------------------- /static/css/bootstrap-responsive.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.0.4 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:28px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box}.hidden{display:none;visibility:hidden}.visible-phone{display:none!important}.visible-tablet{display:none!important}.hidden-desktop{display:none!important}@media(max-width:767px){.visible-phone{display:inherit!important}.hidden-phone{display:none!important}.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}}@media(min-width:768px) and (max-width:979px){.visible-tablet{display:inherit!important}.hidden-tablet{display:none!important}.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}}@media(max-width:480px){.nav-collapse{-webkit-transform:translate3d(0,0,0)}.page-header h1 small{display:block;line-height:18px}input[type="checkbox"],input[type="radio"]{border:1px solid #ccc}.form-horizontal .control-group>label{float:none;width:auto;padding-top:0;text-align:left}.form-horizontal .controls{margin-left:0}.form-horizontal .control-list{padding-top:0}.form-horizontal .form-actions{padding-right:10px;padding-left:10px}.modal{position:absolute;top:10px;right:10px;left:10px;width:auto;margin:0}.modal.fade.in{top:auto}.modal-header .close{padding:10px;margin:-10px}.carousel-caption{position:static}}@media(max-width:767px){body{padding-right:20px;padding-left:20px}.navbar-fixed-top,.navbar-fixed-bottom{margin-right:-20px;margin-left:-20px}.container-fluid{padding:0}.dl-horizontal dt{float:none;width:auto;clear:none;text-align:left}.dl-horizontal dd{margin-left:0}.container{width:auto}.row-fluid{width:100%}.row,.thumbnails{margin-left:0}[class*="span"],.row-fluid [class*="span"]{display:block;float:none;width:auto;margin-left:0}.input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:28px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box}.input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto}}@media(min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;content:""}.row:after{clear:both}[class*="span"]{float:left;margin-left:20px}.container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px}.span12{width:724px}.span11{width:662px}.span10{width:600px}.span9{width:538px}.span8{width:476px}.span7{width:414px}.span6{width:352px}.span5{width:290px}.span4{width:228px}.span3{width:166px}.span2{width:104px}.span1{width:42px}.offset12{margin-left:764px}.offset11{margin-left:702px}.offset10{margin-left:640px}.offset9{margin-left:578px}.offset8{margin-left:516px}.offset7{margin-left:454px}.offset6{margin-left:392px}.offset5{margin-left:330px}.offset4{margin-left:268px}.offset3{margin-left:206px}.offset2{margin-left:144px}.offset1{margin-left:82px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:28px;margin-left:2.762430939%;*margin-left:2.709239449638298%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .span12{width:99.999999993%;*width:99.9468085036383%}.row-fluid .span11{width:91.436464082%;*width:91.38327259263829%}.row-fluid .span10{width:82.87292817100001%;*width:82.8197366816383%}.row-fluid .span9{width:74.30939226%;*width:74.25620077063829%}.row-fluid .span8{width:65.74585634900001%;*width:65.6926648596383%}.row-fluid .span7{width:57.182320438000005%;*width:57.129128948638304%}.row-fluid .span6{width:48.618784527%;*width:48.5655930376383%}.row-fluid .span5{width:40.055248616%;*width:40.0020571266383%}.row-fluid .span4{width:31.491712705%;*width:31.4385212156383%}.row-fluid .span3{width:22.928176794%;*width:22.874985304638297%}.row-fluid .span2{width:14.364640883%;*width:14.311449393638298%}.row-fluid .span1{width:5.801104972%;*width:5.747913482638298%}input,textarea,.uneditable-input{margin-left:0}input.span12,textarea.span12,.uneditable-input.span12{width:714px}input.span11,textarea.span11,.uneditable-input.span11{width:652px}input.span10,textarea.span10,.uneditable-input.span10{width:590px}input.span9,textarea.span9,.uneditable-input.span9{width:528px}input.span8,textarea.span8,.uneditable-input.span8{width:466px}input.span7,textarea.span7,.uneditable-input.span7{width:404px}input.span6,textarea.span6,.uneditable-input.span6{width:342px}input.span5,textarea.span5,.uneditable-input.span5{width:280px}input.span4,textarea.span4,.uneditable-input.span4{width:218px}input.span3,textarea.span3,.uneditable-input.span3{width:156px}input.span2,textarea.span2,.uneditable-input.span2{width:94px}input.span1,textarea.span1,.uneditable-input.span1{width:32px}}@media(min-width:1200px){.row{margin-left:-30px;*zoom:1}.row:before,.row:after{display:table;content:""}.row:after{clear:both}[class*="span"]{float:left;margin-left:30px}.container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px}.span12{width:1170px}.span11{width:1070px}.span10{width:970px}.span9{width:870px}.span8{width:770px}.span7{width:670px}.span6{width:570px}.span5{width:470px}.span4{width:370px}.span3{width:270px}.span2{width:170px}.span1{width:70px}.offset12{margin-left:1230px}.offset11{margin-left:1130px}.offset10{margin-left:1030px}.offset9{margin-left:930px}.offset8{margin-left:830px}.offset7{margin-left:730px}.offset6{margin-left:630px}.offset5{margin-left:530px}.offset4{margin-left:430px}.offset3{margin-left:330px}.offset2{margin-left:230px}.offset1{margin-left:130px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:28px;margin-left:2.564102564%;*margin-left:2.510911074638298%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.45299145300001%;*width:91.3997999636383%}.row-fluid .span10{width:82.905982906%;*width:82.8527914166383%}.row-fluid .span9{width:74.358974359%;*width:74.30578286963829%}.row-fluid .span8{width:65.81196581200001%;*width:65.7587743226383%}.row-fluid .span7{width:57.264957265%;*width:57.2117657756383%}.row-fluid .span6{width:48.717948718%;*width:48.6647572286383%}.row-fluid .span5{width:40.170940171000005%;*width:40.117748681638304%}.row-fluid .span4{width:31.623931624%;*width:31.5707401346383%}.row-fluid .span3{width:23.076923077%;*width:23.0237315876383%}.row-fluid .span2{width:14.529914530000001%;*width:14.4767230406383%}.row-fluid .span1{width:5.982905983%;*width:5.929714493638298%}input,textarea,.uneditable-input{margin-left:0}input.span12,textarea.span12,.uneditable-input.span12{width:1160px}input.span11,textarea.span11,.uneditable-input.span11{width:1060px}input.span10,textarea.span10,.uneditable-input.span10{width:960px}input.span9,textarea.span9,.uneditable-input.span9{width:860px}input.span8,textarea.span8,.uneditable-input.span8{width:760px}input.span7,textarea.span7,.uneditable-input.span7{width:660px}input.span6,textarea.span6,.uneditable-input.span6{width:560px}input.span5,textarea.span5,.uneditable-input.span5{width:460px}input.span4,textarea.span4,.uneditable-input.span4{width:360px}input.span3,textarea.span3,.uneditable-input.span3{width:260px}input.span2,textarea.span2,.uneditable-input.span2{width:160px}input.span1,textarea.span1,.uneditable-input.span1{width:60px}.thumbnails{margin-left:-30px}.thumbnails>li{margin-left:30px}.row-fluid .thumbnails{margin-left:0}}@media(max-width:979px){body{padding-top:0}.navbar-fixed-top,.navbar-fixed-bottom{position:static}.navbar-fixed-top{margin-bottom:18px}.navbar-fixed-bottom{margin-top:18px}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px}.navbar .container{width:auto;padding:0}.navbar .brand{padding-right:10px;padding-left:10px;margin:0 0 0 -5px}.nav-collapse{clear:both}.nav-collapse .nav{float:none;margin:0 0 9px}.nav-collapse .nav>li{float:none}.nav-collapse .nav>li>a{margin-bottom:2px}.nav-collapse .nav>.divider-vertical{display:none}.nav-collapse .nav .nav-header{color:#999;text-shadow:none}.nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:6px 15px;font-weight:bold;color:#999;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.nav-collapse .dropdown-menu li+li a{margin-bottom:2px}.nav-collapse .nav>li>a:hover,.nav-collapse .dropdown-menu a:hover{background-color:#222}.nav-collapse.in .btn-group{padding:0;margin-top:5px}.nav-collapse .dropdown-menu{position:static;top:auto;left:auto;display:block;float:none;max-width:none;padding:0;margin:0 15px;background-color:transparent;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none}.nav-collapse .dropdown-menu .divider{display:none}.nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:9px 15px;margin:9px 0;border-top:1px solid #222;border-bottom:1px solid #222;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}.navbar .nav-collapse .nav.pull-right{float:none;margin-left:0}.nav-collapse,.nav-collapse.collapse{height:0;overflow:hidden}.navbar .btn-navbar{display:block}.navbar-static .navbar-inner{padding-right:10px;padding-left:10px}}@media(min-width:980px){.nav-collapse.collapse{height:auto!important;overflow:visible!important}} 10 | -------------------------------------------------------------------------------- /static/css/sniper.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Roboto:400,300,500,700,900); 2 | body{ 3 | font-family: "Roboto", sans-serif; 4 | font-size: 1.75em; 5 | letter-spacing: .013em; 6 | } 7 | 8 | a{ 9 | -webkit-transition: all ease 0.25s; 10 | transition: all ease 0.25s; 11 | } 12 | .btn{ 13 | -webkit-transition: all ease 0.25s; 14 | transition: all ease 0.25s; 15 | } 16 | .btn:hover{ 17 | -webkit-transition: all ease 0.25s; 18 | transition: all ease 0.25s; 19 | } 20 | 21 | .btn-danger{ 22 | font-size:1.15em; 23 | padding: 10px 20px; 24 | margin-bottom: 10px; 25 | } 26 | 27 | .page-header h1{ 28 | font-size: 4em; 29 | font-weight: 700; 30 | } 31 | .page-header h1 a{ 32 | text-transform: none; 33 | text-decoration: none; 34 | color: inherit; 35 | } 36 | 37 | .inLine{ 38 | display: -moz-inline-stack; 39 | display: inline-block; 40 | zoom: 1; 41 | *display: inline; 42 | } 43 | 44 | .navbar { 45 | margin-bottom: 20px; 46 | } 47 | 48 | .content { 49 | padding-top: 50px; 50 | } 51 | 52 | span.red { 53 | color: #cc0033; 54 | } 55 | 56 | footer { 57 | padding: 10px 0px; 58 | font-size: 16px; 59 | } 60 | 61 | input[type="text"] { 62 | width: 100%; 63 | padding: 5px 5px; 64 | } 65 | 66 | label{ 67 | padding: 5px 0px; 68 | margin-top: 5px; 69 | margin-bottom: 0px; 70 | } 71 | dl .alert{ 72 | margin-bottom: 5px; 73 | padding: 10px; 74 | } 75 | 76 | #sniper-test { 77 | margin: 10px; 78 | } 79 | 80 | ol li b { 81 | display: block; 82 | } 83 | 84 | 85 | @media(max-width:767px){ 86 | input[type="text"] { 87 | width: 100%; 88 | } 89 | .page-header h1{ 90 | font-size: 2em; 91 | } 92 | .content{ 93 | padding-top: 0px; 94 | width:100%; 95 | } 96 | footer{ 97 | font-size:14px; 98 | } 99 | #github{ 100 | width:100px; 101 | } 102 | /*Change font size and position on mobile b/c it gets cut off*/ 103 | ::-webkit-input-placeholder { font-size: 14px; padding-top:3px; } 104 | ::-moz-placeholder { font-size: 14px; padding-top:3px; } 105 | :-ms-input-placeholder { font-size: 14px; padding-top:3px; } 106 | input:-moz-placeholder { font-size: 14px; padding-top:3px; } 107 | } 108 | -------------------------------------------------------------------------------- /static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v/sniper/30036b5913ae1544d8932c57b96113e6aa43f38d/static/img/favicon.ico -------------------------------------------------------------------------------- /static/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v/sniper/30036b5913ae1544d8932c57b96113e6aa43f38d/static/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /static/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v/sniper/30036b5913ae1544d8932c57b96113e6aa43f38d/static/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /static/js/app.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | function human(subject) { 3 | result = ""; 4 | lowerDes = subject.description.toLowerCase(); 5 | $.each(lowerDes.split(" "), function(i, part) { 6 | if(part) 7 | result += part[0].toUpperCase() + part.substring(1).toLowerCase() + " "; 8 | }); 9 | result += "("+ subject.code+")"; 10 | return result; 11 | } 12 | 13 | $('#subject').typeahead({ 14 | source: function(part) { 15 | results = []; 16 | if($.isNumeric(part)) { 17 | $.each(subjects, function(i, subject) { 18 | if(subject.code.indexOf(part) != -1) { 19 | results.push(human(subject)); 20 | } 21 | }); 22 | } 23 | else { 24 | $.each(subjects, function(i, subject) { 25 | lowerDesc = subject.description.toLowerCase(); 26 | if(lowerDesc.indexOf(part.toLowerCase()) != -1) 27 | results.push(human(subject)); 28 | }); 29 | } 30 | return results; 31 | } 32 | }); 33 | 34 | $('#sniper-test').button().click(function() { 35 | var that = $(this); 36 | that.button('loading'); 37 | $.ajax({ 38 | url: '/test', 39 | success: function(response) { 40 | that.button('reset'); 41 | if(response.success) { 42 | alert('Yep looks to me like it does. Send me email at sniper@vverma.net if you think it doesn\'t.'); 43 | } else { 44 | alert('Looks like something\'s wrong. I\'ll work on fixing it asap.'); 45 | } 46 | }, 47 | dataType: 'json', 48 | error: function(response) { 49 | alert('Looks like something\'s wrong. I\'ll work on fixing it asap.'); 50 | } 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /static/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | /* =================================================== 2 | * bootstrap-transition.js v2.0.4 3 | * http://twitter.github.com/bootstrap/javascript.html#transitions 4 | * =================================================== 5 | * Copyright 2012 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ========================================================== */ 19 | 20 | 21 | !function ($) { 22 | 23 | $(function () { 24 | 25 | "use strict"; // jshint ;_; 26 | 27 | 28 | /* CSS TRANSITION SUPPORT (http://www.modernizr.com/) 29 | * ======================================================= */ 30 | 31 | $.support.transition = (function () { 32 | 33 | var transitionEnd = (function () { 34 | 35 | var el = document.createElement('bootstrap') 36 | , transEndEventNames = { 37 | 'WebkitTransition' : 'webkitTransitionEnd' 38 | , 'MozTransition' : 'transitionend' 39 | , 'OTransition' : 'oTransitionEnd' 40 | , 'msTransition' : 'MSTransitionEnd' 41 | , 'transition' : 'transitionend' 42 | } 43 | , name 44 | 45 | for (name in transEndEventNames){ 46 | if (el.style[name] !== undefined) { 47 | return transEndEventNames[name] 48 | } 49 | } 50 | 51 | }()) 52 | 53 | return transitionEnd && { 54 | end: transitionEnd 55 | } 56 | 57 | })() 58 | 59 | }) 60 | 61 | }(window.jQuery);/* ========================================================== 62 | * bootstrap-alert.js v2.0.4 63 | * http://twitter.github.com/bootstrap/javascript.html#alerts 64 | * ========================================================== 65 | * Copyright 2012 Twitter, Inc. 66 | * 67 | * Licensed under the Apache License, Version 2.0 (the "License"); 68 | * you may not use this file except in compliance with the License. 69 | * You may obtain a copy of the License at 70 | * 71 | * http://www.apache.org/licenses/LICENSE-2.0 72 | * 73 | * Unless required by applicable law or agreed to in writing, software 74 | * distributed under the License is distributed on an "AS IS" BASIS, 75 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 76 | * See the License for the specific language governing permissions and 77 | * limitations under the License. 78 | * ========================================================== */ 79 | 80 | 81 | !function ($) { 82 | 83 | "use strict"; // jshint ;_; 84 | 85 | 86 | /* ALERT CLASS DEFINITION 87 | * ====================== */ 88 | 89 | var dismiss = '[data-dismiss="alert"]' 90 | , Alert = function (el) { 91 | $(el).on('click', dismiss, this.close) 92 | } 93 | 94 | Alert.prototype.close = function (e) { 95 | var $this = $(this) 96 | , selector = $this.attr('data-target') 97 | , $parent 98 | 99 | if (!selector) { 100 | selector = $this.attr('href') 101 | selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 102 | } 103 | 104 | $parent = $(selector) 105 | 106 | e && e.preventDefault() 107 | 108 | $parent.length || ($parent = $this.hasClass('alert') ? $this : $this.parent()) 109 | 110 | $parent.trigger(e = $.Event('close')) 111 | 112 | if (e.isDefaultPrevented()) return 113 | 114 | $parent.removeClass('in') 115 | 116 | function removeElement() { 117 | $parent 118 | .trigger('closed') 119 | .remove() 120 | } 121 | 122 | $.support.transition && $parent.hasClass('fade') ? 123 | $parent.on($.support.transition.end, removeElement) : 124 | removeElement() 125 | } 126 | 127 | 128 | /* ALERT PLUGIN DEFINITION 129 | * ======================= */ 130 | 131 | $.fn.alert = function (option) { 132 | return this.each(function () { 133 | var $this = $(this) 134 | , data = $this.data('alert') 135 | if (!data) $this.data('alert', (data = new Alert(this))) 136 | if (typeof option == 'string') data[option].call($this) 137 | }) 138 | } 139 | 140 | $.fn.alert.Constructor = Alert 141 | 142 | 143 | /* ALERT DATA-API 144 | * ============== */ 145 | 146 | $(function () { 147 | $('body').on('click.alert.data-api', dismiss, Alert.prototype.close) 148 | }) 149 | 150 | }(window.jQuery);/* ============================================================ 151 | * bootstrap-button.js v2.0.4 152 | * http://twitter.github.com/bootstrap/javascript.html#buttons 153 | * ============================================================ 154 | * Copyright 2012 Twitter, Inc. 155 | * 156 | * Licensed under the Apache License, Version 2.0 (the "License"); 157 | * you may not use this file except in compliance with the License. 158 | * You may obtain a copy of the License at 159 | * 160 | * http://www.apache.org/licenses/LICENSE-2.0 161 | * 162 | * Unless required by applicable law or agreed to in writing, software 163 | * distributed under the License is distributed on an "AS IS" BASIS, 164 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 165 | * See the License for the specific language governing permissions and 166 | * limitations under the License. 167 | * ============================================================ */ 168 | 169 | 170 | !function ($) { 171 | 172 | "use strict"; // jshint ;_; 173 | 174 | 175 | /* BUTTON PUBLIC CLASS DEFINITION 176 | * ============================== */ 177 | 178 | var Button = function (element, options) { 179 | this.$element = $(element) 180 | this.options = $.extend({}, $.fn.button.defaults, options) 181 | } 182 | 183 | Button.prototype.setState = function (state) { 184 | var d = 'disabled' 185 | , $el = this.$element 186 | , data = $el.data() 187 | , val = $el.is('input') ? 'val' : 'html' 188 | 189 | state = state + 'Text' 190 | data.resetText || $el.data('resetText', $el[val]()) 191 | 192 | $el[val](data[state] || this.options[state]) 193 | 194 | // push to event loop to allow forms to submit 195 | setTimeout(function () { 196 | state == 'loadingText' ? 197 | $el.addClass(d).attr(d, d) : 198 | $el.removeClass(d).removeAttr(d) 199 | }, 0) 200 | } 201 | 202 | Button.prototype.toggle = function () { 203 | var $parent = this.$element.parent('[data-toggle="buttons-radio"]') 204 | 205 | $parent && $parent 206 | .find('.active') 207 | .removeClass('active') 208 | 209 | this.$element.toggleClass('active') 210 | } 211 | 212 | 213 | /* BUTTON PLUGIN DEFINITION 214 | * ======================== */ 215 | 216 | $.fn.button = function (option) { 217 | return this.each(function () { 218 | var $this = $(this) 219 | , data = $this.data('button') 220 | , options = typeof option == 'object' && option 221 | if (!data) $this.data('button', (data = new Button(this, options))) 222 | if (option == 'toggle') data.toggle() 223 | else if (option) data.setState(option) 224 | }) 225 | } 226 | 227 | $.fn.button.defaults = { 228 | loadingText: 'loading...' 229 | } 230 | 231 | $.fn.button.Constructor = Button 232 | 233 | 234 | /* BUTTON DATA-API 235 | * =============== */ 236 | 237 | $(function () { 238 | $('body').on('click.button.data-api', '[data-toggle^=button]', function ( e ) { 239 | var $btn = $(e.target) 240 | if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') 241 | $btn.button('toggle') 242 | }) 243 | }) 244 | 245 | }(window.jQuery);/* ========================================================== 246 | * bootstrap-carousel.js v2.0.4 247 | * http://twitter.github.com/bootstrap/javascript.html#carousel 248 | * ========================================================== 249 | * Copyright 2012 Twitter, Inc. 250 | * 251 | * Licensed under the Apache License, Version 2.0 (the "License"); 252 | * you may not use this file except in compliance with the License. 253 | * You may obtain a copy of the License at 254 | * 255 | * http://www.apache.org/licenses/LICENSE-2.0 256 | * 257 | * Unless required by applicable law or agreed to in writing, software 258 | * distributed under the License is distributed on an "AS IS" BASIS, 259 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 260 | * See the License for the specific language governing permissions and 261 | * limitations under the License. 262 | * ========================================================== */ 263 | 264 | 265 | !function ($) { 266 | 267 | "use strict"; // jshint ;_; 268 | 269 | 270 | /* CAROUSEL CLASS DEFINITION 271 | * ========================= */ 272 | 273 | var Carousel = function (element, options) { 274 | this.$element = $(element) 275 | this.options = options 276 | this.options.slide && this.slide(this.options.slide) 277 | this.options.pause == 'hover' && this.$element 278 | .on('mouseenter', $.proxy(this.pause, this)) 279 | .on('mouseleave', $.proxy(this.cycle, this)) 280 | } 281 | 282 | Carousel.prototype = { 283 | 284 | cycle: function (e) { 285 | if (!e) this.paused = false 286 | this.options.interval 287 | && !this.paused 288 | && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) 289 | return this 290 | } 291 | 292 | , to: function (pos) { 293 | var $active = this.$element.find('.active') 294 | , children = $active.parent().children() 295 | , activePos = children.index($active) 296 | , that = this 297 | 298 | if (pos > (children.length - 1) || pos < 0) return 299 | 300 | if (this.sliding) { 301 | return this.$element.one('slid', function () { 302 | that.to(pos) 303 | }) 304 | } 305 | 306 | if (activePos == pos) { 307 | return this.pause().cycle() 308 | } 309 | 310 | return this.slide(pos > activePos ? 'next' : 'prev', $(children[pos])) 311 | } 312 | 313 | , pause: function (e) { 314 | if (!e) this.paused = true 315 | clearInterval(this.interval) 316 | this.interval = null 317 | return this 318 | } 319 | 320 | , next: function () { 321 | if (this.sliding) return 322 | return this.slide('next') 323 | } 324 | 325 | , prev: function () { 326 | if (this.sliding) return 327 | return this.slide('prev') 328 | } 329 | 330 | , slide: function (type, next) { 331 | var $active = this.$element.find('.active') 332 | , $next = next || $active[type]() 333 | , isCycling = this.interval 334 | , direction = type == 'next' ? 'left' : 'right' 335 | , fallback = type == 'next' ? 'first' : 'last' 336 | , that = this 337 | , e = $.Event('slide') 338 | 339 | this.sliding = true 340 | 341 | isCycling && this.pause() 342 | 343 | $next = $next.length ? $next : this.$element.find('.item')[fallback]() 344 | 345 | if ($next.hasClass('active')) return 346 | 347 | if ($.support.transition && this.$element.hasClass('slide')) { 348 | this.$element.trigger(e) 349 | if (e.isDefaultPrevented()) return 350 | $next.addClass(type) 351 | $next[0].offsetWidth // force reflow 352 | $active.addClass(direction) 353 | $next.addClass(direction) 354 | this.$element.one($.support.transition.end, function () { 355 | $next.removeClass([type, direction].join(' ')).addClass('active') 356 | $active.removeClass(['active', direction].join(' ')) 357 | that.sliding = false 358 | setTimeout(function () { that.$element.trigger('slid') }, 0) 359 | }) 360 | } else { 361 | this.$element.trigger(e) 362 | if (e.isDefaultPrevented()) return 363 | $active.removeClass('active') 364 | $next.addClass('active') 365 | this.sliding = false 366 | this.$element.trigger('slid') 367 | } 368 | 369 | isCycling && this.cycle() 370 | 371 | return this 372 | } 373 | 374 | } 375 | 376 | 377 | /* CAROUSEL PLUGIN DEFINITION 378 | * ========================== */ 379 | 380 | $.fn.carousel = function (option) { 381 | return this.each(function () { 382 | var $this = $(this) 383 | , data = $this.data('carousel') 384 | , options = $.extend({}, $.fn.carousel.defaults, typeof option == 'object' && option) 385 | if (!data) $this.data('carousel', (data = new Carousel(this, options))) 386 | if (typeof option == 'number') data.to(option) 387 | else if (typeof option == 'string' || (option = options.slide)) data[option]() 388 | else if (options.interval) data.cycle() 389 | }) 390 | } 391 | 392 | $.fn.carousel.defaults = { 393 | interval: 5000 394 | , pause: 'hover' 395 | } 396 | 397 | $.fn.carousel.Constructor = Carousel 398 | 399 | 400 | /* CAROUSEL DATA-API 401 | * ================= */ 402 | 403 | $(function () { 404 | $('body').on('click.carousel.data-api', '[data-slide]', function ( e ) { 405 | var $this = $(this), href 406 | , $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 407 | , options = !$target.data('modal') && $.extend({}, $target.data(), $this.data()) 408 | $target.carousel(options) 409 | e.preventDefault() 410 | }) 411 | }) 412 | 413 | }(window.jQuery);/* ============================================================= 414 | * bootstrap-collapse.js v2.0.4 415 | * http://twitter.github.com/bootstrap/javascript.html#collapse 416 | * ============================================================= 417 | * Copyright 2012 Twitter, Inc. 418 | * 419 | * Licensed under the Apache License, Version 2.0 (the "License"); 420 | * you may not use this file except in compliance with the License. 421 | * You may obtain a copy of the License at 422 | * 423 | * http://www.apache.org/licenses/LICENSE-2.0 424 | * 425 | * Unless required by applicable law or agreed to in writing, software 426 | * distributed under the License is distributed on an "AS IS" BASIS, 427 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 428 | * See the License for the specific language governing permissions and 429 | * limitations under the License. 430 | * ============================================================ */ 431 | 432 | 433 | !function ($) { 434 | 435 | "use strict"; // jshint ;_; 436 | 437 | 438 | /* COLLAPSE PUBLIC CLASS DEFINITION 439 | * ================================ */ 440 | 441 | var Collapse = function (element, options) { 442 | this.$element = $(element) 443 | this.options = $.extend({}, $.fn.collapse.defaults, options) 444 | 445 | if (this.options.parent) { 446 | this.$parent = $(this.options.parent) 447 | } 448 | 449 | this.options.toggle && this.toggle() 450 | } 451 | 452 | Collapse.prototype = { 453 | 454 | constructor: Collapse 455 | 456 | , dimension: function () { 457 | var hasWidth = this.$element.hasClass('width') 458 | return hasWidth ? 'width' : 'height' 459 | } 460 | 461 | , show: function () { 462 | var dimension 463 | , scroll 464 | , actives 465 | , hasData 466 | 467 | if (this.transitioning) return 468 | 469 | dimension = this.dimension() 470 | scroll = $.camelCase(['scroll', dimension].join('-')) 471 | actives = this.$parent && this.$parent.find('> .accordion-group > .in') 472 | 473 | if (actives && actives.length) { 474 | hasData = actives.data('collapse') 475 | if (hasData && hasData.transitioning) return 476 | actives.collapse('hide') 477 | hasData || actives.data('collapse', null) 478 | } 479 | 480 | this.$element[dimension](0) 481 | this.transition('addClass', $.Event('show'), 'shown') 482 | this.$element[dimension](this.$element[0][scroll]) 483 | } 484 | 485 | , hide: function () { 486 | var dimension 487 | if (this.transitioning) return 488 | dimension = this.dimension() 489 | this.reset(this.$element[dimension]()) 490 | this.transition('removeClass', $.Event('hide'), 'hidden') 491 | this.$element[dimension](0) 492 | } 493 | 494 | , reset: function (size) { 495 | var dimension = this.dimension() 496 | 497 | this.$element 498 | .removeClass('collapse') 499 | [dimension](size || 'auto') 500 | [0].offsetWidth 501 | 502 | this.$element[size !== null ? 'addClass' : 'removeClass']('collapse') 503 | 504 | return this 505 | } 506 | 507 | , transition: function (method, startEvent, completeEvent) { 508 | var that = this 509 | , complete = function () { 510 | if (startEvent.type == 'show') that.reset() 511 | that.transitioning = 0 512 | that.$element.trigger(completeEvent) 513 | } 514 | 515 | this.$element.trigger(startEvent) 516 | 517 | if (startEvent.isDefaultPrevented()) return 518 | 519 | this.transitioning = 1 520 | 521 | this.$element[method]('in') 522 | 523 | $.support.transition && this.$element.hasClass('collapse') ? 524 | this.$element.one($.support.transition.end, complete) : 525 | complete() 526 | } 527 | 528 | , toggle: function () { 529 | this[this.$element.hasClass('in') ? 'hide' : 'show']() 530 | } 531 | 532 | } 533 | 534 | 535 | /* COLLAPSIBLE PLUGIN DEFINITION 536 | * ============================== */ 537 | 538 | $.fn.collapse = function (option) { 539 | return this.each(function () { 540 | var $this = $(this) 541 | , data = $this.data('collapse') 542 | , options = typeof option == 'object' && option 543 | if (!data) $this.data('collapse', (data = new Collapse(this, options))) 544 | if (typeof option == 'string') data[option]() 545 | }) 546 | } 547 | 548 | $.fn.collapse.defaults = { 549 | toggle: true 550 | } 551 | 552 | $.fn.collapse.Constructor = Collapse 553 | 554 | 555 | /* COLLAPSIBLE DATA-API 556 | * ==================== */ 557 | 558 | $(function () { 559 | $('body').on('click.collapse.data-api', '[data-toggle=collapse]', function ( e ) { 560 | var $this = $(this), href 561 | , target = $this.attr('data-target') 562 | || e.preventDefault() 563 | || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 564 | , option = $(target).data('collapse') ? 'toggle' : $this.data() 565 | $(target).collapse(option) 566 | }) 567 | }) 568 | 569 | }(window.jQuery);/* ============================================================ 570 | * bootstrap-dropdown.js v2.0.4 571 | * http://twitter.github.com/bootstrap/javascript.html#dropdowns 572 | * ============================================================ 573 | * Copyright 2012 Twitter, Inc. 574 | * 575 | * Licensed under the Apache License, Version 2.0 (the "License"); 576 | * you may not use this file except in compliance with the License. 577 | * You may obtain a copy of the License at 578 | * 579 | * http://www.apache.org/licenses/LICENSE-2.0 580 | * 581 | * Unless required by applicable law or agreed to in writing, software 582 | * distributed under the License is distributed on an "AS IS" BASIS, 583 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 584 | * See the License for the specific language governing permissions and 585 | * limitations under the License. 586 | * ============================================================ */ 587 | 588 | 589 | !function ($) { 590 | 591 | "use strict"; // jshint ;_; 592 | 593 | 594 | /* DROPDOWN CLASS DEFINITION 595 | * ========================= */ 596 | 597 | var toggle = '[data-toggle="dropdown"]' 598 | , Dropdown = function (element) { 599 | var $el = $(element).on('click.dropdown.data-api', this.toggle) 600 | $('html').on('click.dropdown.data-api', function () { 601 | $el.parent().removeClass('open') 602 | }) 603 | } 604 | 605 | Dropdown.prototype = { 606 | 607 | constructor: Dropdown 608 | 609 | , toggle: function (e) { 610 | var $this = $(this) 611 | , $parent 612 | , selector 613 | , isActive 614 | 615 | if ($this.is('.disabled, :disabled')) return 616 | 617 | selector = $this.attr('data-target') 618 | 619 | if (!selector) { 620 | selector = $this.attr('href') 621 | selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 622 | } 623 | 624 | $parent = $(selector) 625 | $parent.length || ($parent = $this.parent()) 626 | 627 | isActive = $parent.hasClass('open') 628 | 629 | clearMenus() 630 | 631 | if (!isActive) $parent.toggleClass('open') 632 | 633 | return false 634 | } 635 | 636 | } 637 | 638 | function clearMenus() { 639 | $(toggle).parent().removeClass('open') 640 | } 641 | 642 | 643 | /* DROPDOWN PLUGIN DEFINITION 644 | * ========================== */ 645 | 646 | $.fn.dropdown = function (option) { 647 | return this.each(function () { 648 | var $this = $(this) 649 | , data = $this.data('dropdown') 650 | if (!data) $this.data('dropdown', (data = new Dropdown(this))) 651 | if (typeof option == 'string') data[option].call($this) 652 | }) 653 | } 654 | 655 | $.fn.dropdown.Constructor = Dropdown 656 | 657 | 658 | /* APPLY TO STANDARD DROPDOWN ELEMENTS 659 | * =================================== */ 660 | 661 | $(function () { 662 | $('html').on('click.dropdown.data-api', clearMenus) 663 | $('body') 664 | .on('click.dropdown', '.dropdown form', function (e) { e.stopPropagation() }) 665 | .on('click.dropdown.data-api', toggle, Dropdown.prototype.toggle) 666 | }) 667 | 668 | }(window.jQuery);/* ========================================================= 669 | * bootstrap-modal.js v2.0.4 670 | * http://twitter.github.com/bootstrap/javascript.html#modals 671 | * ========================================================= 672 | * Copyright 2012 Twitter, Inc. 673 | * 674 | * Licensed under the Apache License, Version 2.0 (the "License"); 675 | * you may not use this file except in compliance with the License. 676 | * You may obtain a copy of the License at 677 | * 678 | * http://www.apache.org/licenses/LICENSE-2.0 679 | * 680 | * Unless required by applicable law or agreed to in writing, software 681 | * distributed under the License is distributed on an "AS IS" BASIS, 682 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 683 | * See the License for the specific language governing permissions and 684 | * limitations under the License. 685 | * ========================================================= */ 686 | 687 | 688 | !function ($) { 689 | 690 | "use strict"; // jshint ;_; 691 | 692 | 693 | /* MODAL CLASS DEFINITION 694 | * ====================== */ 695 | 696 | var Modal = function (content, options) { 697 | this.options = options 698 | this.$element = $(content) 699 | .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this)) 700 | } 701 | 702 | Modal.prototype = { 703 | 704 | constructor: Modal 705 | 706 | , toggle: function () { 707 | return this[!this.isShown ? 'show' : 'hide']() 708 | } 709 | 710 | , show: function () { 711 | var that = this 712 | , e = $.Event('show') 713 | 714 | this.$element.trigger(e) 715 | 716 | if (this.isShown || e.isDefaultPrevented()) return 717 | 718 | $('body').addClass('modal-open') 719 | 720 | this.isShown = true 721 | 722 | escape.call(this) 723 | backdrop.call(this, function () { 724 | var transition = $.support.transition && that.$element.hasClass('fade') 725 | 726 | if (!that.$element.parent().length) { 727 | that.$element.appendTo(document.body) //don't move modals dom position 728 | } 729 | 730 | that.$element 731 | .show() 732 | 733 | if (transition) { 734 | that.$element[0].offsetWidth // force reflow 735 | } 736 | 737 | that.$element.addClass('in') 738 | 739 | transition ? 740 | that.$element.one($.support.transition.end, function () { that.$element.trigger('shown') }) : 741 | that.$element.trigger('shown') 742 | 743 | }) 744 | } 745 | 746 | , hide: function (e) { 747 | e && e.preventDefault() 748 | 749 | var that = this 750 | 751 | e = $.Event('hide') 752 | 753 | this.$element.trigger(e) 754 | 755 | if (!this.isShown || e.isDefaultPrevented()) return 756 | 757 | this.isShown = false 758 | 759 | $('body').removeClass('modal-open') 760 | 761 | escape.call(this) 762 | 763 | this.$element.removeClass('in') 764 | 765 | $.support.transition && this.$element.hasClass('fade') ? 766 | hideWithTransition.call(this) : 767 | hideModal.call(this) 768 | } 769 | 770 | } 771 | 772 | 773 | /* MODAL PRIVATE METHODS 774 | * ===================== */ 775 | 776 | function hideWithTransition() { 777 | var that = this 778 | , timeout = setTimeout(function () { 779 | that.$element.off($.support.transition.end) 780 | hideModal.call(that) 781 | }, 500) 782 | 783 | this.$element.one($.support.transition.end, function () { 784 | clearTimeout(timeout) 785 | hideModal.call(that) 786 | }) 787 | } 788 | 789 | function hideModal(that) { 790 | this.$element 791 | .hide() 792 | .trigger('hidden') 793 | 794 | backdrop.call(this) 795 | } 796 | 797 | function backdrop(callback) { 798 | var that = this 799 | , animate = this.$element.hasClass('fade') ? 'fade' : '' 800 | 801 | if (this.isShown && this.options.backdrop) { 802 | var doAnimate = $.support.transition && animate 803 | 804 | this.$backdrop = $('',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},i.prototype.init=function(e,i,o){if(this.enabled=!0,this.type=e,this.$element=t(i),this.options=this.getOptions(o),this.$viewport=this.options.viewport&&t(t.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var n=this.options.trigger.split(" "),s=n.length;s--;){var a=n[s];if("click"==a)this.$element.on("click."+this.type,this.options.selector,t.proxy(this.toggle,this));else if("manual"!=a){var r="hover"==a?"mouseenter":"focusin",l="hover"==a?"mouseleave":"focusout";this.$element.on(r+"."+this.type,this.options.selector,t.proxy(this.enter,this)),this.$element.on(l+"."+this.type,this.options.selector,t.proxy(this.leave,this))}}this.options.selector?this._options=t.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},i.prototype.getDefaults=function(){return i.DEFAULTS},i.prototype.getOptions=function(e){return e=t.extend({},this.getDefaults(),this.$element.data(),e),e.delay&&"number"==typeof e.delay&&(e.delay={show:e.delay,hide:e.delay}),e},i.prototype.getDelegateOptions=function(){var e={},i=this.getDefaults();return this._options&&t.each(this._options,function(t,o){i[t]!=o&&(e[t]=o)}),e},i.prototype.enter=function(e){var i=e instanceof this.constructor?e:t(e.currentTarget).data("bs."+this.type);return i||(i=new this.constructor(e.currentTarget,this.getDelegateOptions()),t(e.currentTarget).data("bs."+this.type,i)),e instanceof t.Event&&(i.inState["focusin"==e.type?"focus":"hover"]=!0),i.tip().hasClass("in")||"in"==i.hoverState?void(i.hoverState="in"):(clearTimeout(i.timeout),i.hoverState="in",i.options.delay&&i.options.delay.show?void(i.timeout=setTimeout(function(){"in"==i.hoverState&&i.show()},i.options.delay.show)):i.show())},i.prototype.isInStateTrue=function(){for(var t in this.inState)if(this.inState[t])return!0;return!1},i.prototype.leave=function(e){var i=e instanceof this.constructor?e:t(e.currentTarget).data("bs."+this.type);return i||(i=new this.constructor(e.currentTarget,this.getDelegateOptions()),t(e.currentTarget).data("bs."+this.type,i)),e instanceof t.Event&&(i.inState["focusout"==e.type?"focus":"hover"]=!1),i.isInStateTrue()?void 0:(clearTimeout(i.timeout),i.hoverState="out",i.options.delay&&i.options.delay.hide?void(i.timeout=setTimeout(function(){"out"==i.hoverState&&i.hide()},i.options.delay.hide)):i.hide())},i.prototype.show=function(){var e=t.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(e);var o=t.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(e.isDefaultPrevented()||!o)return;var n=this,s=this.tip(),a=this.getUID(this.type);this.setContent(),s.attr("id",a),this.$element.attr("aria-describedby",a),this.options.animation&&s.addClass("fade");var r="function"==typeof this.options.placement?this.options.placement.call(this,s[0],this.$element[0]):this.options.placement,l=/\s?auto?\s?/i,h=l.test(r);h&&(r=r.replace(l,"")||"top"),s.detach().css({top:0,left:0,display:"block"}).addClass(r).data("bs."+this.type,this),this.options.container?s.appendTo(this.options.container):s.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var d=this.getPosition(),p=s[0].offsetWidth,c=s[0].offsetHeight;if(h){var f=r,u=this.getPosition(this.$viewport);r="bottom"==r&&d.bottom+c>u.bottom?"top":"top"==r&&d.top-cu.width?"left":"left"==r&&d.left-pa.top+a.height&&(n.top=a.top+a.height-l)}else{var h=e.left-s,d=e.left+s+i;ha.right&&(n.left=a.left+a.width-d)}return n},i.prototype.getTitle=function(){var t,e=this.$element,i=this.options;return t=e.attr("data-original-title")||("function"==typeof i.title?i.title.call(e[0]):i.title)},i.prototype.getUID=function(t){do t+=~~(1e6*Math.random());while(document.getElementById(t));return t},i.prototype.tip=function(){if(!this.$tip&&(this.$tip=t(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},i.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},i.prototype.enable=function(){this.enabled=!0},i.prototype.disable=function(){this.enabled=!1},i.prototype.toggleEnabled=function(){this.enabled=!this.enabled},i.prototype.toggle=function(e){var i=this;e&&(i=t(e.currentTarget).data("bs."+this.type),i||(i=new this.constructor(e.currentTarget,this.getDelegateOptions()),t(e.currentTarget).data("bs."+this.type,i))),e?(i.inState.click=!i.inState.click,i.isInStateTrue()?i.enter(i):i.leave(i)):i.tip().hasClass("in")?i.leave(i):i.enter(i)},i.prototype.destroy=function(){var t=this;clearTimeout(this.timeout),this.hide(function(){t.$element.off("."+t.type).removeData("bs."+t.type),t.$tip&&t.$tip.detach(),t.$tip=null,t.$arrow=null,t.$viewport=null})};var o=t.fn.tooltip;t.fn.tooltip=e,t.fn.tooltip.Constructor=i,t.fn.tooltip.noConflict=function(){return t.fn.tooltip=o,this}}(jQuery),+function(t){"use strict";function e(e){return this.each(function(){var o=t(this),n=o.data("bs.popover"),s="object"==typeof e&&e;(n||!/destroy|hide/.test(e))&&(n||o.data("bs.popover",n=new i(this,s)),"string"==typeof e&&n[e]())})}var i=function(t,e){this.init("popover",t,e)};if(!t.fn.tooltip)throw new Error("Popover requires tooltip.js");i.VERSION="3.3.6",i.DEFAULTS=t.extend({},t.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),i.prototype=t.extend({},t.fn.tooltip.Constructor.prototype),i.prototype.constructor=i,i.prototype.getDefaults=function(){return i.DEFAULTS},i.prototype.setContent=function(){var t=this.tip(),e=this.getTitle(),i=this.getContent();t.find(".popover-title")[this.options.html?"html":"text"](e),t.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof i?"html":"append":"text"](i),t.removeClass("fade top bottom left right in"),t.find(".popover-title").html()||t.find(".popover-title").hide()},i.prototype.hasContent=function(){return this.getTitle()||this.getContent()},i.prototype.getContent=function(){var t=this.$element,e=this.options;return t.attr("data-content")||("function"==typeof e.content?e.content.call(t[0]):e.content)},i.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var o=t.fn.popover;t.fn.popover=e,t.fn.popover.Constructor=i,t.fn.popover.noConflict=function(){return t.fn.popover=o,this}}(jQuery),+function(t){"use strict";function e(e){return this.each(function(){var o=t(this),n=o.data("bs.tab");n||o.data("bs.tab",n=new i(this)),"string"==typeof e&&n[e]()})}var i=function(e){this.element=t(e)};i.VERSION="3.3.6",i.TRANSITION_DURATION=150,i.prototype.show=function(){var e=this.element,i=e.closest("ul:not(.dropdown-menu)"),o=e.data("target");if(o||(o=e.attr("href"),o=o&&o.replace(/.*(?=#[^\s]*$)/,"")),!e.parent("li").hasClass("active")){var n=i.find(".active:last a"),s=t.Event("hide.bs.tab",{relatedTarget:e[0]}),a=t.Event("show.bs.tab",{relatedTarget:n[0]});if(n.trigger(s),e.trigger(a),!a.isDefaultPrevented()&&!s.isDefaultPrevented()){var r=t(o);this.activate(e.closest("li"),i),this.activate(r,r.parent(),function(){n.trigger({type:"hidden.bs.tab",relatedTarget:e[0]}),e.trigger({type:"shown.bs.tab",relatedTarget:n[0]})})}}},i.prototype.activate=function(e,o,n){function s(){a.removeClass("active").find("> .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),e.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),r?(e[0].offsetWidth,e.addClass("in")):e.removeClass("fade"),e.parent(".dropdown-menu").length&&e.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),n&&n()}var a=o.find("> .active"),r=n&&t.support.transition&&(a.length&&a.hasClass("fade")||!!o.find("> .fade").length);a.length&&r?a.one("bsTransitionEnd",s).emulateTransitionEnd(i.TRANSITION_DURATION):s(),a.removeClass("in")};var o=t.fn.tab;t.fn.tab=e,t.fn.tab.Constructor=i,t.fn.tab.noConflict=function(){return t.fn.tab=o,this};var n=function(i){i.preventDefault(),e.call(t(this),"show")};t(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',n).on("click.bs.tab.data-api",'[data-toggle="pill"]',n)}(jQuery),+function(t){"use strict";function e(e){return this.each(function(){var o=t(this),n=o.data("bs.affix"),s="object"==typeof e&&e;n||o.data("bs.affix",n=new i(this,s)),"string"==typeof e&&n[e]()})}var i=function(e,o){this.options=t.extend({},i.DEFAULTS,o),this.$target=t(this.options.target).on("scroll.bs.affix.data-api",t.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",t.proxy(this.checkPositionWithEventLoop,this)),this.$element=t(e),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};i.VERSION="3.3.6",i.RESET="affix affix-top affix-bottom",i.DEFAULTS={offset:0,target:window},i.prototype.getState=function(t,e,i,o){var n=this.$target.scrollTop(),s=this.$element.offset(),a=this.$target.height();if(null!=i&&"top"==this.affixed)return i>n?"top":!1;if("bottom"==this.affixed)return null!=i?n+this.unpin<=s.top?!1:"bottom":t-o>=n+a?!1:"bottom";var r=null==this.affixed,l=r?n:s.top,h=r?a:e;return null!=i&&i>=n?"top":null!=o&&l+h>=t-o?"bottom":!1},i.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(i.RESET).addClass("affix");var t=this.$target.scrollTop(),e=this.$element.offset();return this.pinnedOffset=e.top-t},i.prototype.checkPositionWithEventLoop=function(){setTimeout(t.proxy(this.checkPosition,this),1)},i.prototype.checkPosition=function(){if(this.$element.is(":visible")){var e=this.$element.height(),o=this.options.offset,n=o.top,s=o.bottom,a=Math.max(t(document).height(),t(document.body).height());"object"!=typeof o&&(s=n=o),"function"==typeof n&&(n=o.top(this.$element)),"function"==typeof s&&(s=o.bottom(this.$element));var r=this.getState(a,e,n,s);if(this.affixed!=r){null!=this.unpin&&this.$element.css("top","");var l="affix"+(r?"-"+r:""),h=t.Event(l+".bs.affix");if(this.$element.trigger(h),h.isDefaultPrevented())return;this.affixed=r,this.unpin="bottom"==r?this.getPinnedOffset():null,this.$element.removeClass(i.RESET).addClass(l).trigger(l.replace("affix","affixed")+".bs.affix")}"bottom"==r&&this.$element.offset({top:a-e-s})}};var o=t.fn.affix;t.fn.affix=e,t.fn.affix.Constructor=i,t.fn.affix.noConflict=function(){return t.fn.affix=o,this},t(window).on("load",function(){t('[data-spy="affix"]').each(function(){var i=t(this),o=i.data();o.offset=o.offset||{},null!=o.offsetBottom&&(o.offset.bottom=o.offsetBottom),null!=o.offsetTop&&(o.offset.top=o.offsetTop),e.call(i,o)})})}(jQuery),+function(t){"use strict";function e(e){var i,o=e.attr("data-target")||(i=e.attr("href"))&&i.replace(/.*(?=#[^\s]+$)/,"");return t(o)}function i(e){return this.each(function(){var i=t(this),n=i.data("bs.collapse"),s=t.extend({},o.DEFAULTS,i.data(),"object"==typeof e&&e);!n&&s.toggle&&/show|hide/.test(e)&&(s.toggle=!1),n||i.data("bs.collapse",n=new o(this,s)),"string"==typeof e&&n[e]()})}var o=function(e,i){this.$element=t(e),this.options=t.extend({},o.DEFAULTS,i),this.$trigger=t('[data-toggle="collapse"][href="#'+e.id+'"],[data-toggle="collapse"][data-target="#'+e.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};o.VERSION="3.3.6",o.TRANSITION_DURATION=350,o.DEFAULTS={toggle:!0},o.prototype.dimension=function(){var t=this.$element.hasClass("width");return t?"width":"height"},o.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var e,n=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(n&&n.length&&(e=n.data("bs.collapse"),e&&e.transitioning))){var s=t.Event("show.bs.collapse");if(this.$element.trigger(s),!s.isDefaultPrevented()){n&&n.length&&(i.call(n,"hide"),e||n.data("bs.collapse",null));var a=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[a](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var r=function(){this.$element.removeClass("collapsing").addClass("collapse in")[a](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!t.support.transition)return r.call(this);var l=t.camelCase(["scroll",a].join("-"));this.$element.one("bsTransitionEnd",t.proxy(r,this)).emulateTransitionEnd(o.TRANSITION_DURATION)[a](this.$element[0][l]); 12 | }}}},o.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var e=t.Event("hide.bs.collapse");if(this.$element.trigger(e),!e.isDefaultPrevented()){var i=this.dimension();this.$element[i](this.$element[i]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var n=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return t.support.transition?void this.$element[i](0).one("bsTransitionEnd",t.proxy(n,this)).emulateTransitionEnd(o.TRANSITION_DURATION):n.call(this)}}},o.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},o.prototype.getParent=function(){return t(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(t.proxy(function(i,o){var n=t(o);this.addAriaAndCollapsedClass(e(n),n)},this)).end()},o.prototype.addAriaAndCollapsedClass=function(t,e){var i=t.hasClass("in");t.attr("aria-expanded",i),e.toggleClass("collapsed",!i).attr("aria-expanded",i)};var n=t.fn.collapse;t.fn.collapse=i,t.fn.collapse.Constructor=o,t.fn.collapse.noConflict=function(){return t.fn.collapse=n,this},t(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(o){var n=t(this);n.attr("data-target")||o.preventDefault();var s=e(n),a=s.data("bs.collapse"),r=a?"toggle":n.data();i.call(s,r)})}(jQuery),+function(t){"use strict";function e(i,o){this.$body=t(document.body),this.$scrollElement=t(t(i).is(document.body)?window:i),this.options=t.extend({},e.DEFAULTS,o),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",t.proxy(this.process,this)),this.refresh(),this.process()}function i(i){return this.each(function(){var o=t(this),n=o.data("bs.scrollspy"),s="object"==typeof i&&i;n||o.data("bs.scrollspy",n=new e(this,s)),"string"==typeof i&&n[i]()})}e.VERSION="3.3.6",e.DEFAULTS={offset:10},e.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},e.prototype.refresh=function(){var e=this,i="offset",o=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),t.isWindow(this.$scrollElement[0])||(i="position",o=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var e=t(this),n=e.data("target")||e.attr("href"),s=/^#./.test(n)&&t(n);return s&&s.length&&s.is(":visible")&&[[s[i]().top+o,n]]||null}).sort(function(t,e){return t[0]-e[0]}).each(function(){e.offsets.push(this[0]),e.targets.push(this[1])})},e.prototype.process=function(){var t,e=this.$scrollElement.scrollTop()+this.options.offset,i=this.getScrollHeight(),o=this.options.offset+i-this.$scrollElement.height(),n=this.offsets,s=this.targets,a=this.activeTarget;if(this.scrollHeight!=i&&this.refresh(),e>=o)return a!=(t=s[s.length-1])&&this.activate(t);if(a&&e=n[t]&&(void 0===n[t+1]||e0?this.$element.data("active",b[0]):this.$element.data("active",null),this.options.addItem&&b.push(this.options.addItem),"all"==this.options.items?this.render(b).show():this.render(b.slice(0,this.options.items)).show()):this.shown?this.hide():this},matcher:function(a){var b=this.displayText(a);return~b.toLowerCase().indexOf(this.query.toLowerCase())},sorter:function(a){for(var b,c=[],d=[],e=[];b=a.shift();){var f=this.displayText(b);f.toLowerCase().indexOf(this.query.toLowerCase())?~f.indexOf(this.query)?d.push(b):e.push(b):c.push(b)}return c.concat(d,e)},highlighter:function(b){var c,d,e,f,g,h=a("
"),i=this.query,j=b.toLowerCase().indexOf(i.toLowerCase());if(c=i.length,0===c)return h.text(b).html();for(;j>-1;)d=b.substr(0,j),e=b.substr(j,c),f=b.substr(j+c),g=a("").text(e),h.append(document.createTextNode(d)).append(g),b=f,j=b.toLowerCase().indexOf(i.toLowerCase());return h.append(document.createTextNode(b)).html()},render:function(b){var c=this,d=this,e=!1;return b=a(b).map(function(b,f){var g=d.displayText(f);return b=a(c.options.item).data("value",f),b.find("a").html(c.highlighter(g,f)),g==d.$element.val()&&(b.addClass("active"),d.$element.data("active",f),e=!0),b[0]}),this.autoSelect&&!e&&(b.first().addClass("active"),this.$element.data("active",b.first().data("value"))),this.$menu.html(b),this},displayText:function(a){return"undefined"!=typeof a&&"undefined"!=typeof a.name&&a.name||a},next:function(b){var c=this.$menu.find(".active").removeClass("active"),d=c.next();d.length||(d=a(this.$menu.find("li")[0])),d.addClass("active")},prev:function(a){var b=this.$menu.find(".active").removeClass("active"),c=b.prev();c.length||(c=this.$menu.find("li").last()),c.addClass("active")},listen:function(){this.$element.on("focus",a.proxy(this.focus,this)).on("blur",a.proxy(this.blur,this)).on("keypress",a.proxy(this.keypress,this)).on("keyup",a.proxy(this.keyup,this)),this.eventSupported("keydown")&&this.$element.on("keydown",a.proxy(this.keydown,this)),this.$menu.on("click",a.proxy(this.click,this)).on("mouseenter","li",a.proxy(this.mouseenter,this)).on("mouseleave","li",a.proxy(this.mouseleave,this))},destroy:function(){this.$element.data("typeahead",null),this.$element.data("active",null),this.$element.off("focus").off("blur").off("keypress").off("keyup"),this.eventSupported("keydown")&&this.$element.off("keydown"),this.$menu.remove()},eventSupported:function(a){var b=a in this.$element;return b||(this.$element.setAttribute(a,"return;"),b="function"==typeof this.$element[a]),b},move:function(a){if(this.shown)switch(a.keyCode){case 9:case 13:case 27:a.preventDefault();break;case 38:if(a.shiftKey)return;a.preventDefault(),this.prev();break;case 40:if(a.shiftKey)return;a.preventDefault(),this.next()}},keydown:function(b){this.suppressKeyPressRepeat=~a.inArray(b.keyCode,[40,38,9,13,27]),this.shown||40!=b.keyCode?this.move(b):this.lookup()},keypress:function(a){this.suppressKeyPressRepeat||this.move(a)},keyup:function(a){switch(a.keyCode){case 40:case 38:case 16:case 17:case 18:break;case 9:case 13:if(!this.shown)return;this.select();break;case 27:if(!this.shown)return;this.hide();break;default:this.lookup()}a.preventDefault()},focus:function(a){this.focused||(this.focused=!0,this.options.showHintOnFocus&&this.lookup(""))},blur:function(a){this.focused=!1,!this.mousedover&&this.shown&&this.hide()},click:function(a){a.preventDefault(),this.select(),this.hide()},mouseenter:function(b){this.mousedover=!0,this.$menu.find(".active").removeClass("active"),a(b.currentTarget).addClass("active")},mouseleave:function(a){this.mousedover=!1,!this.focused&&this.shown&&this.hide()}};var c=a.fn.typeahead;a.fn.typeahead=function(c){var d=arguments;return"string"==typeof c&&"getActive"==c?this.data("active"):this.each(function(){var e=a(this),f=e.data("typeahead"),g="object"==typeof c&&c;f||e.data("typeahead",f=new b(this,g)),"string"==typeof c&&(d.length>1?f[c].apply(f,Array.prototype.slice.call(d,1)):f[c]())})},a.fn.typeahead.defaults={source:[],items:8,menu:'',item:'
  • ',minLength:1,scrollHeight:0,autoSelect:!0,afterSelect:a.noop,addItem:!1,delay:0},a.fn.typeahead.Constructor=b,a.fn.typeahead.noConflict=function(){return a.fn.typeahead=c,this},a(document).on("focus.typeahead.data-api",'[data-provide="typeahead"]',function(b){var c=a(this);c.data("typeahead")||c.typeahead(c.data())})}); 2 | -------------------------------------------------------------------------------- /templates/_faq.html: -------------------------------------------------------------------------------- 1 | 2 |

    Frequently Asked Questions

    3 |
    4 |
    5 |
      6 |
      7 |
    1. How does Sniper work? You fill out the form on the homepage. Sniper checks the Rutgers Schedule of Classes to see if your class is open every 15 minutes and emails you if it is open.
    2. 8 |
      9 |
    3. Can Sniper automatically register me for my class if it opens up. Nope it cannot do this for you. It will, however, send you a Webreg link that makes it very easy to register for it, once you're notified.
    4. 10 |
      11 |
    5. Sniper did not email me when my class opened up Sniper checks for classes to be open every 15 minutes. If a class opens up for shorter than 15 minutes, there's a chance that Sniper will never know that it's open.
    6. 12 |
      13 |
    7. Why check every 15 minutes and not more frequently? All my lizard classmates are glued to their computers and are checking to see if Orgo Lab is open 15 times a second. If you're checking webreg more frequently than 15 minutes, Sniper is not for you. Running sniper uses up resources on my server, and I think 15 minutes of a time interval is reasonable enough to check for class openings. If you strongly believe that checking more frequently is necessary, let me remind you that Sniper is open source .
    8. 14 |
      15 |
    9. Why does Sniper not watch classes on the Newark/Camden campus or for Winter/Summer semesters? I do not have a great deal of time to spend on developing Sniper. Sniper is Open Source . If you feel strongly about adding support for Newark/Camden or for other terms, feel free to make these changes yourself and submit a pull request.
    10. 16 |
      17 |
      18 | 19 |
      20 |
    11. Why does Sniper no longer send text messages? Sniper is funded by my own wallet. It doesn't cost much to run except for the cost of sending SMS. It costs $0.0075 to send an SMS. If I'm sending out 10,000 messages a week, that's $75 out of a college kid's wallet. Emails are far cheaper to send ($1 / 10,0000 emails). If you think Sniper is good piece of software, you should ask the administration/registrat to support it. If you *really* want text message alerts, you can always enter your carrier email address into Sniper.
    12. 21 |
      22 |
    13. How do I know Sniper is working? Sniper is meant to be a fire-and-forget application, where you don't need to worry about checking Webreg over and over. If you want to confirm that it's working, you can input a section of a class that's already open (640:024:01 is pretty much always open). You should receive an email from sniper in 15 minutes.
    14. 23 |
      24 |
    15. I didn't get into my class after Sniper notified me. Please keep looking for it. Whenever Sniper notifies you, it will also email you a link that you can click to keep sniping the class.
    16. 25 |
      26 |
    17. I already got into my class. Stop the notifications please! Sniper is written in such a way that once it notifies you about a section of a class, it forgets that you were ever watching that section. It will only ever notify you about a section of a class once, unless you decide to snipe the class again. If you are receiving notifications for one class multiple times, please send me email (address in the footer).
    18. 27 |
      28 |
    19. I think there's a bug with sniper. Please send me an email (my email address is in the footer). If you are a developer, you can check out the source code of Sniper and also send me a pull request.
    20. 29 |
      30 |
      31 |
    32 |
    33 |
    34 | -------------------------------------------------------------------------------- /templates/_formhelpers.html: -------------------------------------------------------------------------------- 1 | {% macro render_field(field) %} 2 |
    3 |
    {{ field.label }}
    4 |
    {{ field(**kwargs)|safe }} 5 | {% if field.errors %} 6 |
    7 | {% for error in field.errors %} 8 |

    {{ error }}

    9 | {% endfor %} 10 |
    11 | {% endif %} 12 |
    13 |
    14 | {% endmacro %} 15 | -------------------------------------------------------------------------------- /templates/down.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block content %} 3 |
    4 |

    Sniper is down for the meantime, while course registration is closed. Check back around registration time for the Spring. 5 |
    Contact sniperCantParseMeBro@rutgers.io for any feedback.

    6 |
    7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /templates/faq.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block content %} 3 | {% include '_faq.html' %} 4 | {% endblock %} 5 | -------------------------------------------------------------------------------- /templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block content %} 3 | 6 | {% from "_formhelpers.html" import render_field %} 7 |
    8 |
    9 |

    Enter your details below, and I'll notify you when your class opens up. Sniper is currently watching courses for Spring 2018 on the Rutgers New Brunswick campus.

    10 |
    11 |
    12 |
    13 |
    14 | {{ render_field(form.email, placeholder="you@example.com") }} 15 | {{ render_field(form.subject, placeholder="Department Number. Ex: '640' for Math", autocomplete="off") }} 16 | {{ render_field(form.course_number, placeholder="Ex: '151' for Calculus I") }} 17 | {{ render_field(form.section) }} 18 |
    19 |
    20 |
    21 |
    22 |
    23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %} {% endblock %}Rutgers Course Sniper 6 | 7 | 8 | 9 | 10 | 11 | 12 | 23 | 24 | 25 |
    26 |
    27 | 30 |
    31 |
    32 |
    33 | {% block content %}{% endblock %} 34 |
    35 |
    36 | 39 |
    40 | Fork me on GitHub 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /templates/success.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block title %} Success | {% endblock %} 3 | {% block content %} 4 |
    5 | Got it! I'll notify you at {{ form.email.data | safe }} when your class opens up. 6 |
    7 | {% include '_faq.html' %} 8 | {% endblock %} 9 | --------------------------------------------------------------------------------