diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..37b3833 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +stat-tracker \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..d821048 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..84c8635 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..d502ed4 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/scopes/scope_settings.xml b/.idea/scopes/scope_settings.xml new file mode 100644 index 0000000..922003b --- /dev/null +++ b/.idea/scopes/scope_settings.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/stat-tracker.iml b/.idea/stat-tracker.iml new file mode 100644 index 0000000..9a74b38 --- /dev/null +++ b/.idea/stat-tracker.iml @@ -0,0 +1,16 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..6564d52 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..6eed536 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1425506439543 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.vagrant/machines/default/virtualbox/action_provision b/.vagrant/machines/default/virtualbox/action_provision new file mode 100644 index 0000000..a555ffe --- /dev/null +++ b/.vagrant/machines/default/virtualbox/action_provision @@ -0,0 +1 @@ +1.5:583656cb-8aef-49b9-b771-e88088881bb9 \ No newline at end of file diff --git a/.vagrant/machines/default/virtualbox/action_set_name b/.vagrant/machines/default/virtualbox/action_set_name new file mode 100644 index 0000000..7b09bd6 --- /dev/null +++ b/.vagrant/machines/default/virtualbox/action_set_name @@ -0,0 +1 @@ +1425260887 \ No newline at end of file diff --git a/.vagrant/machines/default/virtualbox/id b/.vagrant/machines/default/virtualbox/id new file mode 100644 index 0000000..b482bc3 --- /dev/null +++ b/.vagrant/machines/default/virtualbox/id @@ -0,0 +1 @@ +583656cb-8aef-49b9-b771-e88088881bb9 \ No newline at end of file diff --git a/.vagrant/machines/default/virtualbox/index_uuid b/.vagrant/machines/default/virtualbox/index_uuid new file mode 100644 index 0000000..e14d46c --- /dev/null +++ b/.vagrant/machines/default/virtualbox/index_uuid @@ -0,0 +1 @@ +2d61492c70164428b5b49e2da828788b \ No newline at end of file diff --git a/.vagrant/machines/default/virtualbox/private_key b/.vagrant/machines/default/virtualbox/private_key new file mode 100644 index 0000000..310833d --- /dev/null +++ b/.vagrant/machines/default/virtualbox/private_key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAs8CAzvQL3wyrY0/Bo0HkMGaVfvB+8wnZS9057/TV9cYLa86g +EnEgWir9W2kooRNAg7RlcwgvFoqrgRDIHt2/UP3M57Hll7W1gtiWiY2/nQNTHCYp +7LDpuuwCccm8LDvTgJPYUYxpzG2WtkxhtHE2mlAvvfEj0nXvY9WDNnMydMCmmbw3 +G9pmtlmDjosR3sHqpWqL6QuGCZP8s/BbqtLMx9evZuqjEDUPD6UcEnZmnxOpzt6P +QQhgwTOvIrNzgE2EVq71WWKmQlI/qnkSBs1kaXDFMQHrPtv+F+DDcBU2wq4yTQcy +m2Irg+4iXnV1RBHPin/zs9ymtd/2ue0a7wedlwIDAQABAoIBAApHpBYx1sv2yYZw +dT6E8ivicgJEdr4nfgncqQeUFPhtTxHnSzhCx/rgIoUrymsL0tC1bGfbu/WqEIzd +DNTXEa3sFd/aSKsYdTz6bgGH3cF5AzPE6vjiVTufa7ReOSi+cyYTTne7dAGA8vGS +BLRxFRJWK6TNOaM7o2kuOMbQZ75NfZbFG7jipdeH9L8G6JhdEFPEsaffdjBlK0JF +DdNtExY6C8B7l01ZiobIgb2my0BruL6jd49geSjXJLnLog0oe7+A4GQauJ2K5wmI +SygdQFm+xyvo21ghWMH41i1hFx/wpuUxd2Kw25UO+nsaw/tXvEa2qNMGO7ByXoRp +rIPtCEECgYEA6sjHcsPejHa3nSwJUVcKFRIEselJ+WQoDH/6+E/uA3CwMRYQ8tkq +UsH4+aD825O3pCLl2+aH71boINuOVhvMpjCu/iBm9PrrS35uY0WZRAyg0hushrHH +JOh4V9Nc451Uf7p2nPKwEchQDroXf2YiabDUIIEekKpTNFWWrqgo8A8CgYEAw/6r +7Bi1uVBy43LsVKk1vO4I3LFEjO3d5DhO/se1E23A51RybcKYio59YSJ62PaqWsC4 +nmUnnMQpTBiso3xfeVV/BEUQrkFsptK/OpyQtrKpFzysmTay+kpY1klwtW6WKDN2 +URJpi0Ii47AHsA3EQZ508N0EqauC/pT00p9I8fkCgYAjexQpE9CeCZLV/cGquSyc +4owzFr5HIgurV+vleq7NdkoqINuO16K9vvKRRvLve/NUqrFx+fSxBsEttgBcovba +ZqELZnooq4LDdefYuLvgvMfQ+L2p3VPrD/afzyFKhIwwgU1LiqFxDoSKtTS33Ad3 +UXG9N8YWtqa8UfLjkiYdoQKBgBIgQmwiWOQbkZSBc2PdBlBDv2QRrOJPuV666mtb +st5qHxYF1FNcVNkzllreYnggT972xdw4RLKV3a/g9E5mWToScx3E+Wb0PAUtpmvY +QPX0jfjNLrU4vtKw/4hJ4HeXE6ICKGYVCtfLNQjTpdJdY5IiAnSoLB848nNy+TVl +b8yZAoGAUvfOjRldbck8E7vfU+8GDoCsEvoB+WVfH9swFEAaBSWP48n2dkbjilRo +R8iziFCfRpq9cK3cU8JeVbb8urGvr+evqZvShzPqQ9VyEONPtQRJ+4ZMYEpw8rMm +YFh5o+Z5Jr/VV/SL0kWV4Ser40dO7GqMlEFwjcrTDSdTpARSMkQ= +-----END RSA PRIVATE KEY----- diff --git a/.vagrant/machines/default/virtualbox/synced_folders b/.vagrant/machines/default/virtualbox/synced_folders new file mode 100644 index 0000000..4fcd2be --- /dev/null +++ b/.vagrant/machines/default/virtualbox/synced_folders @@ -0,0 +1 @@ +{"virtualbox":{"/vagrant":{"guestpath":"/vagrant","hostpath":"/Volumes/Master/Users/grizax/ironyard/sandbox/stat-tracker","disabled":false}}} \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 5e7bab7..0000000 --- a/README.md +++ /dev/null @@ -1,113 +0,0 @@ -# Stat Tracker - -## Description - -Build an application people can use to track any stats they want about themselves. - -## Objectives - -### Learning Objectives - -After completing this assignment, you should understand: - -* ... - -### Performance Objectives - -After completing this assignment, you should be able to: - -* ... - -## Details - -### Deliverables - -* A Git repo called stat-tracker containing at least: - * `README.md` file explaining how to run your project - * a `requirements.txt` file - * a way to seed your application with data -* An instance of your app running on Heroku - -### Requirements - -* No PEP8 or Pyflakes warnings or errors -* Meets API specifications - -## Normal Mode - -You are going to build an application to track personal statistics. A personal statistic is a numerical record for a person in a time series by day. For example, let's say I wanted to track how many flights of stairs I walked up in a day. My last week might look like: - -Date | Flights ----------- | ------- -02/19/2015 | 8 -02/20/2015 | 6 -02/21/2015 | 7 -02/22/2015 | 6 -02/23/2015 | 8 -02/24/2015 | 4 -02/25/2015 | 6 - -Users of your application can create as many different things to track as they want. They should have an easy-to-use interface to track their stats, allowing them to enter the number for the current day or any previous day. - -You should allow for: - -* User registration -* User login -* Creating a new stat to track -* Recording a stat for a day -* Editing a stat for a day -* Showing a chart for a stat for any series of dates, defined by a start and stop date. The default should be the last 30 days. - -For the chart, you can use whatever you like. Matplotlib is our old friend, but can be unwieldy. [Bokeh](http://bokeh.pydata.org/en/latest/) and [Plotly](https://plot.ly/python/) are other good choices to use with HTML. - -You should also have an API. One of the ways people expect to use this application is via their phone, so you'll need a REST API. - -### API Specification - -For your API, I'm specifying the endpoints you'll need and what they should do. The URLs I'm using are not prefixed: yours should be. -All the endpoints require authentication using HTTP Basic Auth. - -Verb | URL | Action ------- | ---- | ------- -GET | /stats | Show a list of all stats I am tracking, and links to their individual pages -POST | /stats | Create a new stat for me to track. -GET | /stats/{id} | Show information about one stat I am tracking, and give me the data I have recorded for that stat. -PUT | /stats/{id} | Update one stat I am tracking, changing attributes such as name or type. Does not allow for changing tracked data. -DELETE | /stats/{id} | Delete a stat I am tracking. This should remove tracked data for that stat as well. -POST or PUT | /stats/{id}/data | Add tracked data for a day. The JSON sent with this should include the day tracked. You can also override the data for a day already recorded. -DELETE | /stats/{id}/data | Remove tracked data for a day. You should send JSON that includes the date to be removed. - -I am not specifying what the JSON these return should look like, but you should feel free to follow one of the many competing standards. [JSON API](http://jsonapi.org/) is very comprehensive. - - -## Hard Mode - -In addition to the requirements from **Normal Mode**: - -* Users should be able to record different types of stats. You can choose the types, but here are some suggestions: - * Clicker-style stats. The UI on these should change so you have a way to increase them by one via a button click. Good for tracking things as you're doing them. - * Time-goal stats. The stat has a beginning value, ending value, and ending date. Track as normal, but you should be able to see if you're on track to meet your goal. Examples: weight loss, building up for a long run. - * Yes-no stats. Did I do this today? This is often called the "Seinfeld calendar" or [chain calendar](http://chaincalendar.com/about). - * Stats on a scale instead of unbounded. Example: On a scale of 1 to 5, what's my happiness level today? - -* Make sure your interface [is responsive](https://developers.google.com/web/fundamentals/layouts/rwd-fundamentals/) and works well via mobile. - - -## Nightmare Mode - -* Give users a way to invite other users to collaborate/compete on a stat with them. Users can only add/edit their own data, but the stat charts will show everyone competing. - - -## Additional Resources - -* [JSON API](http://jsonapi.org/) -* [Bokeh](http://bokeh.pydata.org/en/latest/) -* [Plotly](https://plot.ly/python/) -* [Flask-RESTful](https://flask-restful.readthedocs.org/en/0.3.1/). A Flask plugin that could help or make this much worse. -* [RESTless](http://restless.readthedocs.org/en/latest/). Another Python library that could help or harm. -* [Kube](http://imperavi.com/kube/). A simpler CSS framework I've been using. -* [Peewee](https://peewee.readthedocs.org/en/latest/index.html). A less-featureful, but perhaps easier to use ORM. - -## Credit - -... diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 0000000..1d1a62c --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,70 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +# All Vagrant configuration is done below. The "2" in Vagrant.configure +# configures the configuration version (we support older styles for +# backwards compatibility). Please don't change it unless you know what +# you're doing. +Vagrant.configure(2) do |config| + # The most common configuration options are documented and commented below. + # For a complete reference, please see the online documentation at + # https://docs.vagrantup.com. + + # Every Vagrant development environment requires a box. You can search for + # boxes at https://atlas.hashicorp.com/search. + config.vm.box = "precise64" + + # The url from where the 'config.vm.box' box will be fetched if it + # doesn't already exist on the user's system. + config.vm.box_url = "http://files.vagrantup.com/precise64.box" + + # Create a forwarded port mapping which allows access to a specific port + # within the machine from a port on the host machine. In the example below, + # accessing "localhost:8080" will access port 80 on the guest machine. + config.vm.network "forwarded_port", guest: 80, host: 8080 + + # Create a private network, which allows host-only access to the machine + # using a specific IP. + # config.vm.network "private_network", ip: "192.168.33.10" + + # Create a public network, which generally matched to bridged network. + # Bridged networks make the machine appear as another physical device on + # your network. + # config.vm.network "public_network" + + # Share an additional folder to the guest VM. The first argument is + # the path on the host to the actual folder. The second argument is + # the path on the guest to mount the folder. And the optional third + # argument is a set of non-required options. + # config.vm.synced_folder "/data", "/vagrant_data" + + # Provider-specific configuration so you can fine-tune various + # backing providers for Vagrant. These expose provider-specific options. + # Example for VirtualBox: + # + # config.vm.provider "virtualbox" do |vb| + # # Display the VirtualBox GUI when booting the machine + # vb.gui = true + # + # # Customize the amount of memory on the VM: + # vb.memory = "1024" + # end + # + # View the documentation for the provider you are using for more + # information on available options. + + # Define a Vagrant Push strategy for pushing to Atlas. Other push strategies + # such as FTP and Heroku are also available. See the documentation at + # https://docs.vagrantup.com/v2/push/atlas.html for more information. + # config.push.define "atlas" do |push| + # push.app = "YOUR_ATLAS_USERNAME/YOUR_APPLICATION_NAME" + # end + + # Enable provisioning with a shell script. Additional provisioners such as + # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the + # documentation for more information about their specific syntax and use. + # config.vm.provision "shell", inline: <<-SHELL + # sudo apt-get update + # sudo apt-get install -y apache2 + # SHELL +end diff --git a/stat_track/.buildpacks b/stat_track/.buildpacks new file mode 100644 index 0000000..8c832fe --- /dev/null +++ b/stat_track/.buildpacks @@ -0,0 +1,2 @@ +https://github.com/imkevinxu/heroku-buildpack-nodejs#yoga +https://github.com/heroku/heroku-buildpack-python diff --git a/stat_track/.coveragerc b/stat_track/.coveragerc new file mode 100644 index 0000000..1a5edcd --- /dev/null +++ b/stat_track/.coveragerc @@ -0,0 +1,22 @@ +[run] +source = stat_track +omit = + *config* + *migrations* + *__init__.py + *manage.py + +[report] +exclude_lines = + pragma: no cover + if __name__ == .__main__. + if settings.DEBUG + if self.debug + def __repr__ + def __str__ + def __unicode__ +show_missing = True + +[html] +directory = tests/django +title = Coverage Report diff --git a/stat_track/.gitignore b/stat_track/.gitignore new file mode 100644 index 0000000..08faed8 --- /dev/null +++ b/stat_track/.gitignore @@ -0,0 +1,74 @@ +# Project-specific # +#################### +.env* +.foreman +assets +tmp +tests/* +_SpecRunner.html + +# Python and Django # +##################### +staticfiles +public +build +compressed +*.py[co] +*.egg* +.coverage + +# Node.js # +########### +node_modules +.grunt + +# Text editors # +################ +.svn +*~ +*.sw +*.swp +*.pyc +*.tmproj +*.sublime-project +*.sublime-workspace + +# Compiled source # +################### +*.com +*.class +*.dll +*.exe +*.o +*.so + +# Packages # +############ +*.7z +*.dmg +*.gz +*.iso +*.jar +*.rar +*.tar +*.zip + +# Logs and databases # +###################### +*.db +*.rdb +*.log +*.log.* +*.sql +*.sqlite +*.sqlite3 + +# OS generated files # +###################### +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db diff --git a/stat_track/Gruntfile.coffee b/stat_track/Gruntfile.coffee new file mode 100644 index 0000000..b86fcba --- /dev/null +++ b/stat_track/Gruntfile.coffee @@ -0,0 +1,29 @@ +module.exports = (grunt) -> + + # Initial variable configuration + pkg = grunt.file.readJSON 'package.json' + name = pkg.name.toLowerCase() + paths = + templates: name + '/templates' + css: name + '/static/css' + fonts: name + '/static/fonts' + img: name + '/static/img' + js: name + '/static/js' + config: name + '/config' + tests: 'tests' + + # Loads grunt config automatically via broken up tasks + # https://github.com/firstandthird/load-grunt-config + require('load-grunt-config') grunt, + data: + name: name + paths: paths + loadGruntTasks: + pattern: [ + 'grunt-*' + '!grunt-template-jasmine-istanbul' + ] + + # Times how long tasks take + # https://github.com/sindresorhus/time-grunt + require('time-grunt') grunt diff --git a/stat_track/Procfile b/stat_track/Procfile new file mode 100644 index 0000000..aaab5eb --- /dev/null +++ b/stat_track/Procfile @@ -0,0 +1 @@ +web: gunicorn example.wsgi:application \ No newline at end of file diff --git a/stat_track/Procfile.dev b/stat_track/Procfile.dev new file mode 100644 index 0000000..eb8b172 --- /dev/null +++ b/stat_track/Procfile.dev @@ -0,0 +1,4 @@ +web: django-admin.py runserver_plus +worker: django-admin.py rqworker high default low +grunt: grunt dev +scheduler: rqscheduler diff --git a/stat_track/Procfile.test b/stat_track/Procfile.test new file mode 100644 index 0000000..0b3c27e --- /dev/null +++ b/stat_track/Procfile.test @@ -0,0 +1,3 @@ +test: grunt test +worker: django-admin.py rqworker high default low +scheduler: rqscheduler diff --git a/stat_track/README.md b/stat_track/README.md new file mode 100644 index 0000000..3c5a07e --- /dev/null +++ b/stat_track/README.md @@ -0,0 +1,497 @@ +django-kevin +============ + +![Django 1.7.5](http://img.shields.io/badge/Django-1.7.5-brightgreen.svg) +![Stablility Status](http://img.shields.io/badge/Stability-Stable-brightgreen.svg) +[![Requirements Status](https://requires.io/github/imkevinxu/django-kevin/requirements.svg?branch=master)](https://requires.io/github/imkevinxu/django-kevin/requirements/?branch=master) +[![Dependencies Status](https://david-dm.org/imkevinxu/django-kevin.svg)](https://david-dm.org/imkevinxu/django-kevin) +[![MIT License](https://img.shields.io/cocoapods/l/AFNetworking.svg)](http://opensource.org/licenses/MIT) + +A heavily personalized project template for Django 1.7.5 using Postgres for development and production. Ready to deploy on Heroku with a bunch of other goodies. + +Forked from the original [django-two-scoops-project](https://github.com/twoscoops/django-twoscoops-project) + +Notes Upgrading to Django 1.8 +============================= +- Template configuration has been redesigned to use different settings and functions to take into account multiple engines +- Security enhancements have been integrated from `django-secure`, do they work together? +- `UUIDField` data type has been added naturally, how does it migrate from the same `UUIDField` from `django_extensions`? + +Creating Your Project +===================== + +*Prerequisites: django* + +To create a new Django project, run the following command replacing PROJECT_NAME with your actual project name: + + django-admin.py startproject --template=https://github.com/imkevinxu/django-kevin/archive/master.zip --extension=py,md,html,json,coveragerc PROJECT_NAME + +Afterwards please reference the actual `README.md` you just created in your new project folder, all the references to stat_track will be changed accordingly. + +Make virtual environments +------------------------- + +*Prerequisites: virtualenv, virtualenvwrapper* + + cd stat_track + mkvirtualenv stat_track-dev && add2virtualenv `pwd` + mkvirtualenv stat_track-prod && add2virtualenv `pwd` + mkvirtualenv stat_track-test && add2virtualenv `pwd` + +Install python packages +----------------------- + +For development: + + workon stat_track-dev + pip install --upgrade pip + pip install --upgrade setuptools + sudo env ARCHFLAGS="-arch i386 -arch x86_64" pip install psycopg2 + pip install -r requirements/dev.txt + +For production: + + workon stat_track-prod + pip install --upgrade pip + pip install --upgrade setuptools + sudo env ARCHFLAGS="-arch i386 -arch x86_64" pip install psycopg2 + pip install -r requirements.txt + +For testing: + + workon stat_track-test + pip install --upgrade pip + pip install --upgrade setuptools + sudo env ARCHFLAGS="-arch i386 -arch x86_64" pip install psycopg2 + pip install -r requirements/test.txt + +Install node packages +--------------------- + +*Prerequisites: node* + + sudo npm install + +One-time system installs +------------------------ + +*Prerequisites: homebrew* + +In order to use the grunt task runner you need to install it globally: + + sudo npm install -g grunt-cli + +In order to be able to lint SCSS files locally you need `ruby` on your local system and a certain gem. See [https://github.com/ahmednuaman/grunt-scss-lint#scss-lint-task](https://github.com/ahmednuaman/grunt-scss-lint#scss-lint-task) + + gem install scss-lint + +In order to use django-pipeline for post-processing, you need `yuglify` installed on your local system: + + sudo npm install -g yuglify + +In order for grunt to notify you of warnings and when the build is finished, you need a [notification system](https://github.com/dylang/grunt-notify#notification-systems) installed. Below is the Mac OSX notification command-line tool: + + brew install terminal-notifier + +In order to use Redis for caching and queuing, you need to download it and have it running in the background. This will also set `redis-server` to automatically run at launch: + + brew install redis + ln -sfv /usr/local/opt/redis/*.plist ~/Library/LaunchAgents + launchctl load ~/Library/LaunchAgents/homebrew.mxcl.redis.plist + launchctl start ~/Library/LaunchAgents/homebrew.mxcl.redis.plist + +Development Mode +================ + +Set .env.dev variable for dev +----------------------------- + +The environment variables for development sets the appropriate `DJANGO_SETTINGS_MODULE` and `PYTHONPATH` in order to use `django-admin.py` seemlessly. Necessary for Foreman and other worker processes + +*`.env.dev` is not version controlled so the first person to create this project needs to create a `.env.dev` file for Foreman to read into the environment. Future collaboraters need to email the creator for it.* + + echo DJANGO_SETTINGS_MODULE=config.settings.dev >> .env.dev + echo PYTHONPATH=stat_track >> .env.dev + echo PYTHONUNBUFFERED=True >> .env.dev + echo PYTHONWARNINGS=ignore:RemovedInDjango18Warning >> .env.dev + echo CACHE=dummy >> .env.dev + +Recommended to use foreman to use development environment variables and processes: + + echo "env: .env.dev" > .foreman + echo "procfile: Procfile.dev" >> .foreman + +Create local postgres database for dev +-------------------------------------- + +*Prerequisites: Postgres and Heroku Toolbelt* + +Install Postgres for your OS [here](http://www.postgresql.org/download/). For Max OSX the easiest option is to download and run [Postgres.app](http://postgresapp.com/). + + # Make sure Postgres.app is running + workon stat_track-dev + createdb stat_track-dev + foreman run django-admin.py migrate + +Run project locally in dev environment +-------------------------------------- + +Use the right virtual environment: + + workon stat_track-dev + +Start the server with: + + foreman start + +Create a local super user with: + + foreman run django-admin.py createsuperuser + +To run one-off commands use: + + foreman run django-admin.py COMMAND + +To enable Live Reload, download and turn on a [browser extension](http://feedback.livereload.com/knowledgebase/articles/86242-how-do-i-install-and-use-the-browser-extensions-). + +Production Mode +=============== + +Set .env variable for prod +-------------------------- + +The environment variables for production must contain a separate `SECRET_KEY` for security and the appropriate `DJANGO_SETTINGS_MODULE` and `PYTHONPATH` in order to use `django-admin.py` seemlessly. Hacky use of `date | md5` to generate a pseudo-random string. + +*`.env` is not version controlled so the first person to create this project needs to create a `.env` file for Foreman and Heroku to read into the environment. Future collaboraters need to email the creator for it.* + + echo SECRET_KEY=`date | md5` >> .env + echo DJANGO_SETTINGS_MODULE=config.settings.prod >> .env + echo PYTHONPATH=stat_track >> .env + echo WEB_CONCURRENCY=3 >> .env + echo PYTHONUNBUFFERED=True >> .env + echo PYTHONWARNINGS=ignore:RemovedInDjango18Warning >> .env + echo BUILDPACK_URL=https://github.com/heroku/heroku-buildpack-multi.git >> .env + +Deploy to Heroku +---------------- + +*Prerequisites: Heroku Toolbelt and heroku-config* + +First step is to deploy to Heroku with the `post_compile` script in `/bin` so that node functions can be installed for python to call them. + + git init + git add . + git commit -m "Ready for initial Heroku deploy" + heroku create + heroku config:push + git push heroku master + +After `post_compile` is successful, uncomment the line with the variable `STATICFILES_STORAGE` in `/stat_track/config/settings/base.py` to enable django-pipeline and push again. + + git commit -am "Enabled django-pipeline" + git push heroku master + heroku run django-admin.py migrate + heroku open + +To run one-off commands like `createsuperuser` use: + + heroku run django-admin.py COMMAND + +Run project locally in prod environment +--------------------------------------- + +Set the `.foreman` file to use production environment variables and processes: + + echo "env: .env" > .foreman + echo "procfile: Procfile" >> .foreman + +Use the right virtual environment: + + workon stat_track-prod + +This is meant to mimic production as close as possible using both the production database and environment settings so proceed with caution. + +**WARNING**: If this project has SSL turned on, [localhost:5000](http://localhost:5000) won't work anymore because it will always try to redirect to [https://localhost:5000](https://localhost:5000). To fix this comment out the SECURITY CONFIGURATION section in `/stat_track/config/settings/prod.py` + + heroku config:pull + foreman run django-admin.py collectstatic --noinput + foreman start + +The site will be located at [localhost:5000](http://localhost:5000) + +Testing Mode +============ + +Set .env.test variable for test +------------------------------ + +The environment variables for testing sets the appropriate `DJANGO_SETTINGS_MODULE` and `PYTHONPATH` in order to use `django-admin.py` seemlessly. Necessary for Foreman and other worker processes + +*`.env.test` is not version controlled so the first person to create this project needs to create a `.env.test` file for Foreman to read into the environment. Future collaboraters need to email the creator for it.* + + echo DJANGO_SETTINGS_MODULE=config.settings.test >> .env.test + echo PYTHONPATH=stat_track >> .env.test + echo PYTHONUNBUFFERED=True >> .env.test + echo PYTHONWARNINGS=ignore:RemovedInDjango18Warning >> .env.test + +Run tests locally in test environment +------------------------------------- + +Set the `.foreman` file to use testing environment variables and processes: + + echo "env: .env.test" > .foreman + echo "procfile: Procfile.test" >> .foreman + +Use the right virtual environment: + + workon stat_track-test + +And have static assets prepared (for coverage tests): + + foreman run django-admin.py collectstatic --noinput + +Automatically run all tests and linters and watch files to continuously run tests: + + foreman start + +You can view the results of the tests in HTML at [localhost:9000/tests](http://localhost:9000/tests) + +You can specifically view the results of Django coverage tests at [localhost:9000/tests/django](http://localhost:9000/tests/django) + +Jasmine JS Unit Tests +--------------------- + +Grunt automatically compiles Jasmine tests written in CoffeeScript at `/stat_track/static/js/tests/coffee` and runs the tests upon every save. + +You can specifically view the results of Jasmine JS unit tests at [localhost:9000/tests/jasmine](http://localhost:9000/tests/jasmine) + +You can specifically view the results of JS coverage tests at [localhost:9000/tests/jasmine/coverage.html](http://localhost:9000/tests/jasmine/coverage.html) + +Add-ons & Services +================== + +SSL +--- +Enable SSL via Heroku, Cloudflare, or your DNS provider and then uncomment the SECURITY CONFIGURATION section in `/stat_track/config/settings/prod.py` to enable django-secure and other security best practices for production. + +Invoke +------ +Scripts can be programmed to be run on the command-line using [Invoke](https://github.com/pyinvoke/invoke) for repeated tasks like deployment, building, or cleaning. Write your tasks in `tasks.py`. + +Redis Cloud Caching +------------------- +In order to enable redis for caching and queues, add [Redis Cloud](https://devcenter.heroku.com/articles/rediscloud) to Heroku. + + heroku addons:add rediscloud:25 + +Redis Queue Worker +------------------ +Add a [Redis Queue](https://github.com/ui/django-rq) worker process to Procfile: + + echo "worker: django-admin.py rqworker high default low" >> Procfile + +Push the changed Procfile to Heroku: + + git add Procfile + git commit -m "Added worker process to Procfile, pushing to Heroku" + git push heroku master + +Turn on background job worker with this one-liner: + + heroku scale worker=1 + +Redis Queue Scheduler +--------------------- +Add a [RQ Scheduler](https://github.com/ui/rq-scheduler) process to Procfile: + + echo "scheduler: rqscheduler --url \$REDISCLOUD_URL" >> Procfile + +Push the changed Procfile to Heroku: + + git add Procfile + git commit -m "Added scheduler process to Procfile, pushing to Heroku" + git push heroku master + +Turn on background job scheduler with this one-liner: + + heroku scale scheduler=1 + +Amazon S3 +--------- +To use Amazon S3 as a static and media file storage, create a custom Group and User via IAM and then a custom static bucket and media bucket with public read policies. + +Add the following config variables to Heroku: + + heroku config:set AWS_ACCESS_KEY_ID=INSERT_ACCESS_KEY_ID + heroku config:set AWS_SECRET_ACCESS_KEY=INSERT_SECRET_ACCESS_KEY + heroku config:set AWS_STATIC_STORAGE_BUCKET_NAME=stat_track-static + heroku config:set AWS_MEDIA_STORAGE_BUCKET_NAME=stat_track-media + +PG Backups +---------- +[PG Backups](https://devcenter.heroku.com/articles/pgbackups) is a Heroku add-on for automatic Postgres database backups. Enable with the following one-liner: + + heroku addons:add pgbackups:auto-month + +Monitoring +---------- +- [Librato](https://devcenter.heroku.com/articles/librato) for Heroku performance monitoring +- [New Relic](https://devcenter.heroku.com/articles/newrelic) for server performance monitoring (protip: set [availability monitoring](https://coderwall.com/p/u0x3nw) on to avoid Heroku idling) +- [RedisMonitor](https://devcenter.heroku.com/articles/redismonitor) for Redis server monitoring +- [Logentries](https://devcenter.heroku.com/articles/logentries) provides logging backups as well as search and notifications. Can also additionally backup to S3 +- [Sentry](https://devcenter.heroku.com/articles/sentry) for error tracking with [Raven](http://raven.readthedocs.org/en/latest/index.html) as the client. Make sure to use a [synchronous blocking transport](http://python-rq.org/patterns/sentry/). +- [Ranger](https://devcenter.heroku.com/articles/ranger) to alert you when your app is down + +Testing +------- +- [Rainforest RQ](https://devcenter.heroku.com/articles/rainforest) for simple integration testing +- [Tinfoil Security](https://devcenter.heroku.com/articles/tinfoilsecurity) for regularly scanning your app for security vulnerabilities +- [Loader.io](https://devcenter.heroku.com/articles/loaderio) for load testing + +Continuous Integration +---------------------- +Includes a fancy badge for GitHub README + +- [Travis CI](https://travis-ci.org/) for continuous integration testing +- [Coveralls.io](https://coveralls.io/) for coverage testing +- [Requires.io](https://requires.io/) for dependency management + +Utilities +--------- +- [Filepicker](https://devcenter.heroku.com/articles/filepicker) for file uploading and content management +- [Twilio](http://www.twilio.com/) for sending SMS, MMS, and Voice. Recommended to use [`django-twilio`](http://django-twilio.readthedocs.org/en/latest/) +- [Mailgun](https://devcenter.heroku.com/articles/mailgun) or [Sendgrid](https://devcenter.heroku.com/articles/sendgrid) for email sending. Here are some useful [email templates](http://blog.mailgun.com/transactional-html-email-templates/) +- [MailChimp](http://mailchimp.com/) for email newsletters or create your own [custom newsletter emails](http://zurb.com/playground/responsive-email-templates) + +Libraries +========= + +Python 2.7.9 +============ + +Currently using [Django 1.7.5](https://docs.djangoproject.com/en/1.7/) for the app framework + +base.txt +-------- +- [bpython 0.14](http://docs.bpython-interpreter.org/) - Advanced python interpreter/REPL +- [defusedxml 0.4.1](https://bitbucket.org/tiran/defusedxml) - Secure XML parser protected against XML bombs +- [dj-static 0.0.6](https://github.com/kennethreitz/dj-static) - Serve production static files with Django +- [django-authtools 1.1.0](http://django-authtools.readthedocs.org/en/latest/) - Custom User model classes such as `AbstractEmailUser` and `AbstractNamedUser` +- [django-braces 1.4.0](http://django-braces.readthedocs.org/en/v1.4.0/) - Lots of custom mixins +- [django-extensions 1.5.0](http://django-extensions.readthedocs.org/en/latest/) - Useful command line extensions (`shell_plus`, `create_command`, `export_emails`) +- [django-floppyforms 1.3.0](http://django-floppyforms.readthedocs.org/en/latest/) - Control of output of form rendering +- [django-model-utils 2.2](https://django-model-utils.readthedocs.org/en/latest/) - Useful model mixins and utilities such as `TimeStampedModel` and `Choices` +- [django-pipeline 1.4.6](http://django-pipeline.readthedocs.org/en/latest/) - CSS and JS compressor and compiler. Also minifies HTML +- [django-redis 3.8.3](https://django-redis.readthedocs.org/en/latest/) - Enables redis caching +- [django-rq 0.7.0](https://github.com/ui/django-rq) - Django integration for RQ +- [invoke 0.9.0](https://github.com/pyinvoke/invoke) - Python task execution in `tasks.py` +- [logutils 0.3.3](https://pythonhosted.org/logutils/) - Nifty handlers for the Python standard library’s logging package +- [project-runpy 0.3.1](https://github.com/crccheck/project_runpy) - Helpers for Python projects like ReadableSqlFilter +- [psycopg2 2.6](http://pythonhosted.org/psycopg2/) - PostgreSQL adapter +- [python-magic 0.4.6](https://github.com/ahupp/python-magic) - Library to identify uploaded files' headers +- [pytz 2014.10](http://pytz.sourceforge.net/) - World timezone definitions +- [requests 2.5.3](http://docs.python-requests.org/en/latest/) - HTTP request API +- [rq-scheduler 0.5.1](https://github.com/ui/rq-scheduler) - Job scheduling capabilities to RQ +- [six 1.9.0](http://pythonhosted.org/six/) - Python 2 and 3 compatibility utilities +- [static 1.1.1](https://github.com/lukearno/static) - Serves static and dynamic content +- [unicode-slugify 0.1.3](https://github.com/mozilla/unicode-slugify) - A slugifier that works in unicode + +dev.txt +------- +- [Werkzeug 0.10.1](http://werkzeug.pocoo.org/) - WSGI utility library with powerful debugger +- [django-debug-toolbar 1.2.2](http://django-debug-toolbar.readthedocs.org/en/1.2/) - Debug information in a toolbar +- [django-sslserver 0.14](https://github.com/teddziuba/django-sslserver) - SSL localhost server + +prod.txt +-------- +- [Collectfast 0.2.1](https://github.com/antonagestam/collectfast) - Faster collectstatic +- [boto 2.36.0](https://boto.readthedocs.org/en/latest/) - Python interface to AWS +- [dj-database-url 0.3.0](https://github.com/kennethreitz/dj-database-url) - Allows Django to use database URLs for Heroku +- [django-secure 1.0.1](http://django-secure.readthedocs.org/en/v0.1.2/) - Django security best practices +- [django-storages 1.1.8](http://django-storages.readthedocs.org/en/latest/index.html) - Custom storage backends; using S3 +- [gunicorn 19.2.1](https://github.com/benoitc/gunicorn) - Production WSGI server with workers + +test.txt +-------- +- [coverage 3.7.1](http://nedbatchelder.com/code/coverage/) - Measures code coverage +- [nose-exclude 0.2.0](https://bitbucket.org/kgrandis/nose-exclude) - Easily specify directories to be excluded from testing +- [django-nose 1.3](https://github.com/django-nose/django-nose) - Django test runner using nose +- [factory-boy 2.4.1](https://github.com/rbarrois/factory_boy) - Test fixtures replacement for Python +- [flake8 2.3.0](http://flake8.readthedocs.org/en/latest/) - Python style checker + +config/lib +---------- +- [colorstreamhandler.py](http://stackoverflow.com/questions/384076/how-can-i-color-python-logging-output/1336640#1336640) - Colored stream handler for python logging framework +- [tdaemon.py](https://github.com/brunobord/tdaemon) - Test daemon in Python modified to work with django-admin.py, django-nose, and coverage + +Node 0.10.X +=========== + +post_compile +------------ +Using `post_compile` script for the Heroku python environment to recognize node packages + +- [yuglify 0.1.4](https://github.com/yui/yuglify) - UglifyJS and cssmin compressor + +package.json +------------ +Locally using node and grunt to watch and compile frontend files + +- [coffee-script ^1.8.0](http://coffeescript.org/) - Cleaner JavaScript +- [grunt ~0.4.5](http://gruntjs.com/) - Automatic Task Runner +- [grunt-autoprefixer ^2.0.0](https://github.com/nDmitry/grunt-autoprefixer) - Parse CSS and add vendor-prefixed CSS properties +- [grunt-coffeelint 0.0.13](https://github.com/vojtajina/grunt-coffeelint) - Lint your CoffeeScript +- [grunt-concurrent ^1.0.0](https://github.com/sindresorhus/grunt-concurrent) - Run grunt tasks concurrently +- [grunt-contrib-clean ^0.6.0](https://github.com/gruntjs/grunt-contrib-clean) - Clear files and folders +- [grunt-contrib-coffee ^0.13.0](https://github.com/gruntjs/grunt-contrib-coffee) - Compile CoffeeScript files to JavaScript +- [grunt-contrib-connect ^0.9.0](https://github.com/gruntjs/grunt-contrib-connect) - Start a static web server +- [grunt-contrib-copy ^0.8.0](https://github.com/gruntjs/grunt-contrib-copy) - Copy files and folders +- [grunt-contrib-imagemin ^0.9.1](https://github.com/gruntjs/grunt-contrib-imagemin) - Minify PNG, JPEG, GIF, and SVG images +- [grunt-contrib-jasmine ^0.8.0](https://github.com/gruntjs/grunt-contrib-jasmine) - Run jasmine specs headlessly through PhantomJS +- [grunt-contrib-watch ^0.6.1](https://github.com/gruntjs/grunt-contrib-watch) - Run tasks whenever watched files change +- [grunt-newer ^1.0.0](https://github.com/tschaub/grunt-newer) - Configure Grunt tasks to run with changed files only +- [grunt-notify ^0.4.1](https://github.com/dylang/grunt-notify) - Automatic desktop notifications for Grunt +- [grunt-open ^0.2.3](https://github.com/jsoverson/grunt-open) - Open urls and files from a grunt task +- [grunt-sass ^0.18.0](https://github.com/sindresorhus/grunt-sass) - Compile Sass to CSS +- [grunt-scss-lint ^0.3.3](https://github.com/ahmednuaman/grunt-scss-lint) - Lint your SCSS +- [grunt-shell ^1.1.1](https://github.com/sindresorhus/grunt-shell) - Run shell commands +- [grunt-template-jasmine-istanbul ^0.3.0](https://github.com/maenu/grunt-template-jasmine-istanbul) - Code coverage template mix-in for grunt-contrib-jasmine, using istanbul +- [grunt-text-replace ^0.4.0](https://github.com/yoniholmes/grunt-text-replace) - General purpose text replacement for grunt +- [load-grunt-config ^0.16.0](https://github.com/firstandthird/load-grunt-config) - Grunt plugin that lets you break up your Gruntfile config by task +- [time-grunt ^1.0.0](https://github.com/sindresorhus/time-grunt) - Display the elapsed execution time of grunt tasks + +Static Assets +============= + +Fonts +----- +- [SS-Standard 1.005](https://symbolset.com/icons/standard) - Standard icon library as a font. Documentation located locally at `/stat_track/static/css/fonts/ss-standard/documentation.html` + +CSS +--- +- [Bootstrap 3.3.0](http://getbootstrap.com) - CSS/JS starting framework + +JS +-- +- [jQuery 1.11.1](https://api.jquery.com/) - Useful JS functions +- [Bootstrap 3.3.0](http://getbootstrap.com) - CSS/JS starting framework +- [Underscore.js 1.7.0](http://underscorejs.org) - Very useful functional programming helpers +- [CSRF.js](https://docs.djangoproject.com/en/dev/ref/contrib/csrf/#ajax) - Django Cross Site Request Forgery protection via AJAX + +Jasmine +------- +- [Jasmine-Ajax 2.0.1](http://github.com/pivotal/jasmine-ajax) - Set of helpers for testing AJAX requests with Jasmine +- [Jasmine-jQuery 2.0.5](https://github.com/velesin/jasmine-jquery) - Set of jQuery helpers for Jasmine + +Acknowledgements +================ + +![Two Scoops of Django](http://twoscoops.smugmug.com/Two-Scoops-Press-Media-Kit/i-C8s5jkn/0/O/favicon-152.png "Two Scoops Logo") + +This project follows best practices as espoused in [Two Scoops of Django: Best Practices for Django 1.6](http://twoscoopspress.org/products/two-scoops-of-django-1-6). + +Many thanks to: +--------------- + +- [Daniel Greenfield](https://twitter.com/pydanny) and [Audrey Roy](https://twitter.com/audreyr) for writing the book +- All of the [contributors](https://github.com/twoscoops/django-twoscoops-project/blob/master/CONTRIBUTORS.txt) to the original fork diff --git a/stat_track/bin/common.sh b/stat_track/bin/common.sh new file mode 100644 index 0000000..4a9d17e --- /dev/null +++ b/stat_track/bin/common.sh @@ -0,0 +1,43 @@ +error() { + echo " ! $*" >&2 + exit 1 +} + +status() { + echo "-----> $*" +} + +protip() { + echo + echo "PRO TIP: $*" | indent + echo "See https://devcenter.heroku.com/articles/nodejs-support" | indent + echo +} + +# sed -l basically makes sed replace and buffer through stdin to stdout +# so you get updates while the command runs and dont wait for the end +# e.g. npm install | indent +indent() { + c='s/^/ /' + case $(uname) in + Darwin) sed -l "$c";; # mac/bsd sed: -l buffers on line boundaries + *) sed -u "$c";; # unix/gnu sed: -u unbuffered (arbitrary) chunks of data + esac +} + +cat_npm_debug_log() { + test -f $build_dir/npm-debug.log && cat $build_dir/npm-debug.log +} + +export_env_dir() { + env_dir=$1 + whitelist_regex=${2:-''} + blacklist_regex=${3:-'^(PATH|GIT_DIR|CPATH|CPPATH|LD_PRELOAD|LIBRARY_PATH)$'} + if [ -d "$env_dir" ]; then + for e in $(ls $env_dir); do + echo "$e" | grep -E "$whitelist_regex" | grep -qvE "$blacklist_regex" && + export "$e=$(cat $env_dir/$e)" + : + done + fi +} diff --git a/stat_track/bin/install_nodejs b/stat_track/bin/install_nodejs new file mode 100644 index 0000000..9a4790a --- /dev/null +++ b/stat_track/bin/install_nodejs @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +set -eo pipefail + +NODE_VERSION=$(curl --silent --get https://semver.io/node/stable) +NODE_BASENAME=node-v${NODE_VERSION}-linux-x64 +NODE_ARCHIVE="http://nodejs.org/dist/v${NODE_VERSION}/${NODE_BASENAME}.tar.gz" + +echo "Downloading and installing node-$NODE_VERSION" + +tempdir="$( mktemp -t node_XXXX )" +rm -rf $tempdir +mkdir -p $tempdir + +pushd $tempdir &> /dev/null +curl -s -L -o tmp-nodejs.tar.gz $NODE_ARCHIVE +tar -zxvf tmp-nodejs.tar.gz > /dev/null +rm tmp-nodejs.tar.gz +popd &> /dev/null + +mkdir -v -p $BUILD_DIR/.heroku/vendor &> /dev/null +pushd $BUILD_DIR/.heroku/vendor &> /dev/null +rm -rf node +mv $tempdir/$NODE_BASENAME node +popd &> /dev/null + +ln -sf ../../vendor/node/bin/node .heroku/python/bin/node +ln -sf ../../vendor/node/bin/node-waf .heroku/python/bin/node-waf +ln -sf ../../vendor/node/bin/npm .heroku/python/bin/npm diff --git a/stat_track/bin/post_compile b/stat_track/bin/post_compile new file mode 100644 index 0000000..9dcdfb0 --- /dev/null +++ b/stat_track/bin/post_compile @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +set -eo pipefail +source bin/common.sh + +if ! type node &> /dev/null; then + status "Installing node.js" + chmod +x bin/install_nodejs + bin/install_nodejs 2>&1 | indent + echo +fi + +if ! type yuglify &> /dev/null; then + status "Installing yuglify" + npm install -g yuglify@0.1.4 2>&1 | indent + echo +fi + +status "Post-compile done" +echo diff --git a/stat_track/docs/README.md b/stat_track/docs/README.md new file mode 100644 index 0000000..db7d0c8 --- /dev/null +++ b/stat_track/docs/README.md @@ -0,0 +1,34 @@ +Welcome to stat_track's documentation! +============================================== + +[Setup](setup.md) +------------------------------- +Instructions to get your environment set up and to start coding! + +[Development](development.md) +----------------------------- +Instructions to start your dev server. + +[Deployment](deployment.md) +--------------------------- +Instructions to deploy to production. + +[Architecture](architecture.md) +------------------------------- +High-level overview of how this app is structured. + +[Testing](testing.md) +--------------------- +Instructions to run unit and functional tests. + +[Libraries](libraries.md) +------------------------- +List of what third-party plugins/libraries are used and what they do. + +[Services](services.md) +----------------------- +List of cloud services used. + +[Change Log](changelog.md) +------------------------- +List of changes in each version update. diff --git a/stat_track/docs/architecture.md b/stat_track/docs/architecture.md new file mode 100644 index 0000000..3837742 --- /dev/null +++ b/stat_track/docs/architecture.md @@ -0,0 +1,2 @@ +Architecture +============ diff --git a/stat_track/docs/changelog.md b/stat_track/docs/changelog.md new file mode 100644 index 0000000..a5693d9 --- /dev/null +++ b/stat_track/docs/changelog.md @@ -0,0 +1,2 @@ +Changelog +========= diff --git a/stat_track/docs/deployment.md b/stat_track/docs/deployment.md new file mode 100644 index 0000000..7c3cda7 --- /dev/null +++ b/stat_track/docs/deployment.md @@ -0,0 +1,2 @@ +Deployment +========== diff --git a/stat_track/docs/development.md b/stat_track/docs/development.md new file mode 100644 index 0000000..ab0c3b3 --- /dev/null +++ b/stat_track/docs/development.md @@ -0,0 +1,2 @@ +Development +=========== diff --git a/stat_track/docs/libraries.md b/stat_track/docs/libraries.md new file mode 100644 index 0000000..237e420 --- /dev/null +++ b/stat_track/docs/libraries.md @@ -0,0 +1,2 @@ +Libraries +========= diff --git a/stat_track/docs/services.md b/stat_track/docs/services.md new file mode 100644 index 0000000..14f3e5a --- /dev/null +++ b/stat_track/docs/services.md @@ -0,0 +1,2 @@ +Services +======== diff --git a/stat_track/docs/setup.md b/stat_track/docs/setup.md new file mode 100644 index 0000000..ad9665d --- /dev/null +++ b/stat_track/docs/setup.md @@ -0,0 +1,2 @@ +Setup +===== diff --git a/stat_track/docs/testing.md b/stat_track/docs/testing.md new file mode 100644 index 0000000..96994bb --- /dev/null +++ b/stat_track/docs/testing.md @@ -0,0 +1,2 @@ +Testing +======= diff --git a/stat_track/grunt/aliases.yaml b/stat_track/grunt/aliases.yaml new file mode 100644 index 0000000..3ac3a9e --- /dev/null +++ b/stat_track/grunt/aliases.yaml @@ -0,0 +1,61 @@ +# Aliases for registered tasks +# https://github.com/firstandthird/load-grunt-config#aliases + +lint_stylesheets: + - 'scsslint' + +lint_scripts: + - 'coffeelint:scripts' + +lint: + - 'lint_stylesheets' + - 'lint_scripts' + +build_stylesheets: + - 'clean:stylesheets' + - 'sass' + - 'autoprefixer' + - 'replace:stylesheets' + +build_scripts: + - 'clean:scripts' + - 'coffee:scripts' + +build_images: + - 'clean:images' + - 'imagemin' + +build: + - 'build_stylesheets' + - 'build_scripts' + - 'build_images' + +heroku: + - 'build' + +dev: + - 'lint' + - 'build' + - 'notify:compile' + - 'watch' + +lint_jasmine: + - 'coffeelint:jasmine' + +build_jasmine: + - 'copy:jasmine' + - 'coffee' + - 'jasmine' + - 'copy:coverage' + - 'copy:specRunner' + - 'clean:specRunner' + - 'replace:coverage' + - 'replace:specRunner' + +test: + - 'shell:django' + - 'clean:jasmine' + - 'lint_jasmine' + - 'build_jasmine' + - 'notify:test' + - 'concurrent:tests' diff --git a/stat_track/grunt/autoprefixer.coffee b/stat_track/grunt/autoprefixer.coffee new file mode 100644 index 0000000..8ba476e --- /dev/null +++ b/stat_track/grunt/autoprefixer.coffee @@ -0,0 +1,15 @@ +module.exports = + + # Parse CSS and add vendor-prefixed CSS properties + # https://github.com/nDmitry/grunt-autoprefixer + + stylesheets: + options: + map: true + files: [ + expand: true + cwd: '<%= paths.css %>/build/' + src: ['*.css'] + dest: '<%= paths.css %>/build' + ext: '.css' + ] diff --git a/stat_track/grunt/clean.coffee b/stat_track/grunt/clean.coffee new file mode 100644 index 0000000..adba919 --- /dev/null +++ b/stat_track/grunt/clean.coffee @@ -0,0 +1,27 @@ +module.exports = + + # Clear files and folders + # https://github.com/gruntjs/grunt-contrib-clean + + stylesheets: ['<%= paths.css %>/build'] + + scripts: [ + '<%= paths.js %>/build' + '<%= paths.js %>/tests/build' + ] + + images: ['<%= paths.img %>/compressed'] + + tests: [ + '<%= paths.tests %>/*' + '.coverage' + ] + + jasmine: ['<%= paths.tests %>/jasmine'] + + specRunner: ['_SpecRunner.html'] + + logs: [ + 'logs/*.log' + 'logs/*.log.*' + ] diff --git a/stat_track/grunt/coffee.coffee b/stat_track/grunt/coffee.coffee new file mode 100644 index 0000000..9cda337 --- /dev/null +++ b/stat_track/grunt/coffee.coffee @@ -0,0 +1,26 @@ +module.exports = + + # Compile CoffeeScript files to JavaScript + # https://github.com/gruntjs/grunt-contrib-coffee + + scripts: + options: + sourceMap: true + files: [ + expand: true + cwd: '<%= paths.js %>/coffee' + src: ['*.coffee'] + dest: '<%= paths.js %>/build' + ext: '.js' + ] + + jasmine: + options: + sourceMap: true + files: [ + expand: true + cwd: '<%= paths.js %>/tests/coffee' + src: ['*.coffee'] + dest: '<%= paths.js %>/tests/build' + ext: '.js' + ] diff --git a/stat_track/grunt/coffeelint.coffee b/stat_track/grunt/coffeelint.coffee new file mode 100644 index 0000000..8065bfe --- /dev/null +++ b/stat_track/grunt/coffeelint.coffee @@ -0,0 +1,8 @@ +module.exports = + + # Lint your CoffeeScript + # https://github.com/vojtajina/grunt-coffeelint + + scripts: ['<%= paths.js %>/coffee/*.coffee'] + + jasmine: ['<%= paths.js %>/tests/coffee/*.coffee'] diff --git a/stat_track/grunt/concurrent.coffee b/stat_track/grunt/concurrent.coffee new file mode 100644 index 0000000..f279561 --- /dev/null +++ b/stat_track/grunt/concurrent.coffee @@ -0,0 +1,13 @@ +module.exports = + + # Run grunt tasks concurrently + # https://github.com/sindresorhus/grunt-concurrent + + tests: + options: + logConcurrentOutput: true + tasks: [ + 'connect:tests' + 'watch' + 'shell:tdaemon' + ] diff --git a/stat_track/grunt/connect.coffee b/stat_track/grunt/connect.coffee new file mode 100644 index 0000000..4700a69 --- /dev/null +++ b/stat_track/grunt/connect.coffee @@ -0,0 +1,11 @@ +module.exports = + + # Start a static web server + # https://github.com/gruntjs/grunt-contrib-connect + + tests: + options: + port: 9000 + hostname: 'localhost' + livereload: true + keepalive: true diff --git a/stat_track/grunt/copy.coffee b/stat_track/grunt/copy.coffee new file mode 100644 index 0000000..b480de0 --- /dev/null +++ b/stat_track/grunt/copy.coffee @@ -0,0 +1,18 @@ +module.exports = + + # Copy files and folders + # https://github.com/gruntjs/grunt-contrib-copy + + jasmine: + expand: true + flatten: true + src: '<%= paths.js %>/tests/lib/*.js' + dest: '<%= paths.js %>/tests/build/' + + coverage: + src: '<%= paths.tests %>/jasmine/index.html' + dest: '<%= paths.tests %>/jasmine/coverage.html' + + specRunner: + src: '_SpecRunner.html' + dest: '<%= paths.tests %>/jasmine/index.html' diff --git a/stat_track/grunt/imagemin.coffee b/stat_track/grunt/imagemin.coffee new file mode 100644 index 0000000..422fd43 --- /dev/null +++ b/stat_track/grunt/imagemin.coffee @@ -0,0 +1,15 @@ +module.exports = + + # Minify PNG, JPEG, GIF, and SVG images + # https://github.com/gruntjs/grunt-contrib-imagemin + + images: + files: [ + expand: true + cwd: '<%= paths.img %>/' + src: [ + '**/*.{png,jpg,gif,svg,ico}', + '!compressed/' + ] + dest: '<%= paths.img %>/compressed' + ] diff --git a/stat_track/grunt/jasmine.coffee b/stat_track/grunt/jasmine.coffee new file mode 100644 index 0000000..e0a9596 --- /dev/null +++ b/stat_track/grunt/jasmine.coffee @@ -0,0 +1,31 @@ +module.exports = + + # Run jasmine specs headlessly through PhantomJS + # https://github.com/gruntjs/grunt-contrib-jasmine + + # Code coverage template mix-in for grunt-contrib-jasmine, using istanbul + # https://github.com/maenu/grunt-template-jasmine-istanbul + + coverage: + options: + specs: '<%= paths.js %>/tests/build/*Spec.js' + helpers: '<%= paths.js %>/tests/build/*Helper.js' + vendor: [ + 'https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js' + '<%= paths.js %>/lib/*.js' + ] + keepRunner: true + template: require('grunt-template-jasmine-istanbul') + templateOptions: + coverage: '<%= paths.tests %>/jasmine/coverage.json' + report: [ + { + type: 'html' + options: + dir: '<%= paths.tests %>/jasmine' + } + { type: 'text-summary' } + { type: 'text' } + ] + + src: '<%= paths.js %>/build/*.js' diff --git a/stat_track/grunt/notify.coffee b/stat_track/grunt/notify.coffee new file mode 100644 index 0000000..e912676 --- /dev/null +++ b/stat_track/grunt/notify.coffee @@ -0,0 +1,20 @@ +module.exports = + + # Automatic desktop notifications for Grunt + # https://github.com/dylang/grunt-notify + + lint: + options: + message: "Linting is complete" + + build: + options: + message: "Building is complete" + + test: + options: + message: "Testing is complete" + + compile: + options: + message: "Compiling is complete, now watching..." diff --git a/stat_track/grunt/open.coffee b/stat_track/grunt/open.coffee new file mode 100644 index 0000000..4178d22 --- /dev/null +++ b/stat_track/grunt/open.coffee @@ -0,0 +1,13 @@ +module.exports = + + # Open urls and files from a grunt task + # https://github.com/jsoverson/grunt-open + + jasmine: + path: 'http://localhost:9000/tests/jasmine/' + + django: + path: 'http://localhost:9000/tests/django/' + + dev: + path: 'http://localhost:8000' diff --git a/stat_track/grunt/replace.coffee b/stat_track/grunt/replace.coffee new file mode 100644 index 0000000..1a84483 --- /dev/null +++ b/stat_track/grunt/replace.coffee @@ -0,0 +1,50 @@ +module.exports = + + # General purpose text replacement for grunt + # https://github.com/yoniholmes/grunt-text-replace + + stylesheets: + src: ['<%= paths.css %>/build/*.css.map'] + overwrite: true + replacements: [ + from: '<%= name %>/static' + to: '/static' + ] + + coverage: + src: ['<%= paths.tests %>/jasmine/coverage.html'] + overwrite: true + replacements: [ + from: '
' + to: '
+

+ + Click here for Jasmine Tests + +

+ ' + ] + + specRunner: + src: ['<%= paths.tests %>/jasmine/index.html'] + overwrite: true + replacements: [ + { + from: '.grunt' + to: '../../.grunt' + } + { + from: '<%= name %>' + to: '../../<%= name %>' + } + { + from: '' + to: ' +

+ + Click here for JS Test Coverage + +

+ ' + } + ] diff --git a/stat_track/grunt/sass.coffee b/stat_track/grunt/sass.coffee new file mode 100644 index 0000000..12ba605 --- /dev/null +++ b/stat_track/grunt/sass.coffee @@ -0,0 +1,15 @@ +module.exports = + + # Compile Sass to CSS + # https://github.com/sindresorhus/grunt-sass + + stylesheets: + options: + sourceMap: true + files: [ + expand: true + cwd: '<%= paths.css %>/scss/' + src: ['*.scss'] + dest: '<%= paths.css %>/build' + ext: '.css' + ] diff --git a/stat_track/grunt/scsslint.coffee b/stat_track/grunt/scsslint.coffee new file mode 100644 index 0000000..cbbd27f --- /dev/null +++ b/stat_track/grunt/scsslint.coffee @@ -0,0 +1,9 @@ +module.exports = + + # Lint your SCSS + # https://github.com/ahmednuaman/grunt-scss-lint + + options: + colorizeOutput: true + config: 'node_modules/grunt-scss-lint/.scss-lint.yml' + stylesheets: ['<%= paths.css %>/scss/*.scss'] diff --git a/stat_track/grunt/shell.coffee b/stat_track/grunt/shell.coffee new file mode 100644 index 0000000..f4d0b98 --- /dev/null +++ b/stat_track/grunt/shell.coffee @@ -0,0 +1,12 @@ +module.exports = + + # Run shell commands + # https://github.com/sindresorhus/grunt-shell + + django: + command: 'coverage run $(which django-admin.py) test && coverage combine && coverage report && coverage html; echo && + echo "================================ Flake8 Summary ================================" && flake8 <%= name %>; + echo "================================================================================"' + + tdaemon: + command: 'python <%= paths.config %>/lib/tdaemon.py <%= name %> -t django-nose-coverage --ignore-dirs=static,templates' diff --git a/stat_track/grunt/watch.coffee b/stat_track/grunt/watch.coffee new file mode 100644 index 0000000..ba955d6 --- /dev/null +++ b/stat_track/grunt/watch.coffee @@ -0,0 +1,52 @@ +module.exports = + + # Run tasks whenever watched files change + # https://github.com/gruntjs/grunt-contrib-watch + + options: + spawn: false + + config: + options: + reload: true + files: [ + 'Gruntfile.coffee' + 'grunt/*.{coffee,yaml}' + ] + + livereload: + options: + livereload: true + files: [ + '<%= paths.templates %>/**/*.html' + '<%= paths.css %>/**/*.{scss,css}' + '<%= paths.js %>/**/*.{coffee,js}' + ] + + stylesheets: + files: ['<%= paths.css %>/scss/*.scss'] + tasks: [ + 'newer:scsslint' + 'newer:sass' + 'newer:autoprefixer' + 'replace:stylesheets' + ] + + scripts: + files: ['<%= paths.js %>/coffee/*.coffee'] + tasks: [ + 'newer:coffeelint:scripts' + 'newer:coffee:scripts' + ] + + images: + files: ['<%= paths.img %>/**/*.{png,jpg,gif,svg}'] + tasks: ['newer:imagemin'] + + jasmine: + files: ['<%= paths.js %>/tests/coffee/*.coffee'] + tasks: [ + 'lint_jasmine' + 'build_jasmine' + 'notify:test' + ] diff --git a/stat_track/logs/.gitkeep b/stat_track/logs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/stat_track/manage.py b/stat_track/manage.py new file mode 100644 index 0000000..2605e37 --- /dev/null +++ b/stat_track/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/stat_track/package.json b/stat_track/package.json new file mode 100644 index 0000000..6ba2090 --- /dev/null +++ b/stat_track/package.json @@ -0,0 +1,43 @@ +{ + "name": "stat_track", + "version": "0.0.1", + "description": "Using Grunt to automate linting, compiling, and watching frontend code", + "author": "Kevin Xu", + "homepage": "https://stat_track.com/", + "repository": { + "type": "git", + "url": "https://github.com/imkevinxu/django-kevin" + }, + "main": "Gruntfile.coffee", + "dependencies": { + "coffee-script": "^1.9.1", + "grunt": "~0.4.5", + "grunt-autoprefixer": "^2.2.0", + "grunt-coffeelint": "0.0.13", + "grunt-concurrent": "^1.0.0", + "grunt-contrib-clean": "^0.6.0", + "grunt-contrib-coffee": "^0.13.0", + "grunt-contrib-connect": "^0.9.0", + "grunt-contrib-copy": "^0.8.0", + "grunt-contrib-imagemin": "^0.9.3", + "grunt-contrib-jasmine": "^0.8.2", + "grunt-contrib-watch": "^0.6.1", + "grunt-newer": "^1.1.0", + "grunt-notify": "^0.4.1", + "grunt-open": "^0.2.3", + "grunt-sass": "^0.18.0", + "grunt-scss-lint": "^0.3.4", + "grunt-shell": "^1.1.1", + "grunt-template-jasmine-istanbul": "^0.3.3", + "grunt-text-replace": "^0.4.0", + "load-grunt-config": "^0.16.0", + "time-grunt": "^1.0.0" + }, + "devDependencies": {}, + "engines": { + "node": "0.10.x", + "npm": "2.x" + }, + "private": true, + "scripts": {} +} diff --git a/stat_track/requirements.txt b/stat_track/requirements.txt new file mode 100644 index 0000000..186365a --- /dev/null +++ b/stat_track/requirements.txt @@ -0,0 +1,12 @@ +Django>1.4,<1.7 +South==0.8.4 +dj-database-url==0.3.0 +django-braces==1.4.0 +django-oauth-toolkit==0.7.2 +oauthlib==0.6.3 +psycopg2==2.5.3 +requests==2.3.0 +six==1.7.3 + +# Gunicorn 19.0.0 has a critical bug. Waiting for 19.1.0 release +git+git://github.com/benoitc/gunicorn.git@26df0651bcf1655ef63ac3b968344d737d3a7bfc#egg=gunicorn diff --git a/stat_track/requirements/base.txt b/stat_track/requirements/base.txt new file mode 100644 index 0000000..734294b --- /dev/null +++ b/stat_track/requirements/base.txt @@ -0,0 +1,93 @@ +# See more documentation in /docs/libraries.md + +# Current framework +# https://docs.djangoproject.com/en/1.7/ +Django==1.7.5 + +# Advanced python interpreter/REPL +# http://docs.bpython-interpreter.org/ +bpython==0.14 + +# Secure XML parser protected against XML bombs +# https://bitbucket.org/tiran/defusedxml +defusedxml==0.4.1 + +# Serve production static files with Django +# https://github.com/kennethreitz/dj-static +dj-static==0.0.6 + +# Custom User model classes such as `AbstractEmailUser` and `AbstractNamedUser` +# http://django-authtools.readthedocs.org/en/latest/ +django-authtools==1.1.0 + +# Lots of custom mixins +# http://django-braces.readthedocs.org/en/v1.4.0/ +django-braces==1.4.0 + +# Useful command line extensions (`shell_plus`, `create_command`, `export_emails`) +# http://django-extensions.readthedocs.org/en/latest/ +django-extensions==1.5.0 + +# Control of output of form rendering +# http://django-floppyforms.readthedocs.org/en/latest/ +django-floppyforms==1.3.0 + +# Useful model mixins and utilities such as `TimeStampedModel` and `Choices` +# https://django-model-utils.readthedocs.org/en/latest/ +django-model-utils==2.2 + +# CSS and JS compressor and compiler. Also minifies HTML +# http://django-pipeline.readthedocs.org/en/latest/ +django-pipeline==1.4.6 + +# Enables redis cacheing +# https://django-redis.readthedocs.org/en/latest/ +django-redis==3.8.3 + +# Django integration for RQ +# https://github.com/ui/django-rq +django-rq==0.7.0 + +# Python task execution in `tasks.py` +# http://docs.pyinvoke.org/en/latest/ +invoke==0.9.0 + +# Nifty handlers for the Python standard library’s logging package +# https://pythonhosted.org/logutils/ +logutils==0.3.3 + +# Helpers for Python projects like ReadableSqlFilter +# https://github.com/crccheck/project_runpy +project-runpy==0.3.1 + +# PostgreSQL adapter +# http://pythonhosted.org/psycopg2/ +psycopg2==2.6 + +# Library to identify uploaded file's headers +# https://github.com/ahupp/python-magic +python-magic==0.4.6 + +# World timezone definitions +# http://pytz.sourceforge.net/ +pytz==2014.10 + +# HTTP request API +# http://docs.python-requests.org/en/latest/ +requests==2.5.3 + +# Job scheduling capabilities to RQ +# https://github.com/ui/rq-scheduler +rq-scheduler==0.5.1 + +# Python 2 and 3 compatibility utilities +# http://pythonhosted.org/six/ +six==1.9.0 + +# Serves static and dynamic content +# https://github.com/lukearno/static +static==1.1.1 + +# A slugifier that works in unicode +# https://github.com/mozilla/unicode-slugify +unicode-slugify==0.1.3 diff --git a/stat_track/requirements/dev.txt b/stat_track/requirements/dev.txt new file mode 100644 index 0000000..9b18991 --- /dev/null +++ b/stat_track/requirements/dev.txt @@ -0,0 +1,16 @@ +# See more documentation in /docs/libraries.md + +# Importing from base.txt +-r base.txt + +# WSGI utility library with powerful debugger +# http://werkzeug.pocoo.org/ +Werkzeug==0.10.1 + +# Debug information in a toolbar +# http://django-debug-toolbar.readthedocs.org/en/1.2/ +django-debug-toolbar==1.2.2 + +# SSL localhost server +# https://github.com/teddziuba/django-sslserver +django-sslserver==0.14 diff --git a/stat_track/requirements/prod.txt b/stat_track/requirements/prod.txt new file mode 100644 index 0000000..7770c46 --- /dev/null +++ b/stat_track/requirements/prod.txt @@ -0,0 +1,28 @@ +# See more documentation in /docs/libraries.md + +# Importing from base.txt +-r base.txt + +# Faster collectstatic +# https://github.com/antonagestam/collectfast +Collectfast==0.2.1 + +# Python interface to AWS +# https://boto.readthedocs.org/en/latest/ +boto==2.36.0 + +# Allows Django to use database URLs for Heroku +# https://github.com/kennethreitz/dj-database-url +dj-database-url==0.3.0 + +# Django security best practices +# http://django-secure.readthedocs.org/en/v0.1.2/ +django-secure==1.0.1 + +# Custom storage backends; using S3 +# http://django-storages.readthedocs.org/en/latest/index.html +django-storages==1.1.8 + +# Production WSGI server with workers +# https://github.com/benoitc/gunicorn +gunicorn==19.2.1 diff --git a/stat_track/requirements/test.txt b/stat_track/requirements/test.txt new file mode 100644 index 0000000..4fbef3f --- /dev/null +++ b/stat_track/requirements/test.txt @@ -0,0 +1,24 @@ +# See more documentation in /docs/libraries.md + +# Importing from base.txt +-r base.txt + +# Measures code coverage +# http://nedbatchelder.com/code/coverage/ +coverage==3.7.1 + +# Easily specify directories to be excluded from testing +# https://bitbucket.org/kgrandis/nose-exclude +nose-exclude==0.2.0 + +# Django test runner using nose +# https://github.com/django-nose/django-nose +django-nose==1.3 + +# Test fixtures replacement for Python +# https://github.com/rbarrois/factory_boy +factory-boy==2.4.1 + +# Python style checker +# http://flake8.readthedocs.org/en/latest/ +flake8==2.3.0 diff --git a/stat_track/runtime.txt b/stat_track/runtime.txt new file mode 100644 index 0000000..c47075b --- /dev/null +++ b/stat_track/runtime.txt @@ -0,0 +1 @@ +python-2.7.9 diff --git a/stat_track/setup.cfg b/stat_track/setup.cfg new file mode 100644 index 0000000..d9f1be0 --- /dev/null +++ b/stat_track/setup.cfg @@ -0,0 +1,3 @@ +[flake8] +exclude = *migrations*,*lib* +ignore = E265,E501,F403 diff --git a/stat_track/stat_track/__init__.py b/stat_track/stat_track/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stat_track/stat_track/accounts/__init__.py b/stat_track/stat_track/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stat_track/stat_track/accounts/admin.py b/stat_track/stat_track/accounts/admin.py new file mode 100644 index 0000000..5b13139 --- /dev/null +++ b/stat_track/stat_track/accounts/admin.py @@ -0,0 +1,26 @@ +from django.contrib import admin +from django.contrib.auth.models import Group + +# Unregister Groups model which is not used +admin.site.unregister(Group) + +from authtools.admin import StrippedUserAdmin +from authtools.admin import BASE_FIELDS, DATE_FIELDS, SIMPLE_PERMISSION_FIELDS + +from .models import User + + +class UserAdmin(StrippedUserAdmin): + list_display = ('is_active', 'name', 'email', 'date_joined', 'last_login', 'is_superuser', 'is_staff') + list_display_links = ('name', 'email') + + search_fields = ('name', 'email') + list_filter = ('is_active', 'is_superuser', 'is_staff') + + fieldsets = ( + BASE_FIELDS, + DATE_FIELDS, + SIMPLE_PERMISSION_FIELDS, + ) + +admin.site.register(User, UserAdmin) diff --git a/stat_track/stat_track/accounts/factories.py b/stat_track/stat_track/accounts/factories.py new file mode 100644 index 0000000..6b0a1ff --- /dev/null +++ b/stat_track/stat_track/accounts/factories.py @@ -0,0 +1,29 @@ +from __future__ import absolute_import + +from factory import LazyAttributeSequence, Sequence +from factory.django import DjangoModelFactory + +from .models import User + + +class UserFactory(DjangoModelFactory): + class Meta: + model = User + + email = LazyAttributeSequence(lambda o, n: '%s-user-%s@django.com' % (o.name.split()[0].lower(), n)) + name = Sequence(lambda n: 'User #%s' % n) + password = 'password' + + @classmethod + def _create(cls, target_class, *args, **kwargs): + manager = cls._get_manager(target_class) + return manager.create_user(*args, **kwargs) + + +class AdminUserFactory(UserFactory): + email = LazyAttributeSequence(lambda o, n: '%s-admin-%s@django.com' % (o.name.split()[0].lower(), n)) + + @classmethod + def _create(cls, target_class, *args, **kwargs): + manager = cls._get_manager(target_class) + return manager.create_superuser(*args, **kwargs) diff --git a/stat_track/stat_track/accounts/migrations/0001_initial.py b/stat_track/stat_track/accounts/migrations/0001_initial.py new file mode 100644 index 0000000..8bd9aef --- /dev/null +++ b/stat_track/stat_track/accounts/migrations/0001_initial.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('email', models.EmailField(unique=True, max_length=255, verbose_name='email address', db_index=True)), + ('name', models.CharField(max_length=255, verbose_name='name')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(default=django.utils.timezone.now, verbose_name='last login')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('groups', models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Group', blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of his/her group.', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Permission', blank=True, help_text='Specific permissions for this user.', verbose_name='user permissions')), + ], + options={ + 'ordering': ['name', 'email'], + 'abstract': False, + }, + bases=(models.Model,), + ), + ] diff --git a/stat_track/stat_track/accounts/migrations/__init__.py b/stat_track/stat_track/accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stat_track/stat_track/accounts/models.py b/stat_track/stat_track/accounts/models.py new file mode 100644 index 0000000..97e2497 --- /dev/null +++ b/stat_track/stat_track/accounts/models.py @@ -0,0 +1,6 @@ +from authtools.models import AbstractNamedUser + + +class User(AbstractNamedUser): + USERNAME_FIELD = 'email' + REQUIRED_FIELDS = ['name'] diff --git a/stat_track/stat_track/accounts/tests/__init__.py b/stat_track/stat_track/accounts/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stat_track/stat_track/accounts/tests/test_admin.py b/stat_track/stat_track/accounts/tests/test_admin.py new file mode 100644 index 0000000..284261d --- /dev/null +++ b/stat_track/stat_track/accounts/tests/test_admin.py @@ -0,0 +1,13 @@ +from django.test import TestCase +from django.contrib import admin +from django.contrib.auth.models import Group + +from ..models import User + + +class AccountsAdminTestCase(TestCase): + def test_user_in_admin(self): + self.assertTrue(User in admin.site._registry) + + def test_group_not_in_admin(self): + self.assertFalse(Group in admin.site._registry) diff --git a/stat_track/stat_track/accounts/tests/test_factories.py b/stat_track/stat_track/accounts/tests/test_factories.py new file mode 100644 index 0000000..2dd9c29 --- /dev/null +++ b/stat_track/stat_track/accounts/tests/test_factories.py @@ -0,0 +1,30 @@ +from django.test import TestCase +from django.contrib.auth import get_user_model + +from ..factories import UserFactory, AdminUserFactory + + +class AccountsFactoriesTestCase(TestCase): + def setUp(self): + get_user_model().objects.all().delete() + + def test_user_factory(self): + user = UserFactory(email='test@example.com') + user_from_db = get_user_model().objects.get(email='test@example.com') + self.assertEqual(user, user_from_db) + + def test_admin_user_factory(self): + admin = AdminUserFactory(email='test@example.com') + admin_from_db = get_user_model().objects.get(email='test@example.com') + self.assertEqual(admin, admin_from_db) + self.assertTrue(admin.is_superuser) + + def test_multiple_users_factory(self): + users = [] + for i in range(10): + users.append(UserFactory()) + users.append(AdminUserFactory()) + + self.assertEqual(len(users), 20) + self.assertEqual(len(users), len(get_user_model().objects.all())) + self.assertEqual(len(get_user_model().objects.filter(is_superuser=True)), 10) diff --git a/stat_track/stat_track/accounts/tests/test_models.py b/stat_track/stat_track/accounts/tests/test_models.py new file mode 100644 index 0000000..a3bfaf5 --- /dev/null +++ b/stat_track/stat_track/accounts/tests/test_models.py @@ -0,0 +1,9 @@ +from django.test import TestCase +from django.contrib.auth import get_user_model + +from ..models import User + + +class AccountsModelsTestCase(TestCase): + def test_user_is_auth_user_model(self): + self.assertEqual(get_user_model(), User) diff --git a/stat_track/stat_track/api_v1.py b/stat_track/stat_track/api_v1.py new file mode 100644 index 0000000..647efa6 --- /dev/null +++ b/stat_track/stat_track/api_v1.py @@ -0,0 +1,100 @@ +from oauth2_provider.decorators import protected_resource +from oauth2_provider import VERSION as DOT_VERSION + +import json +from django import get_version +from django.http import HttpResponse +from django.http import HttpResponseBadRequest, HttpResponseNotFound +from django.core import serializers +from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_http_methods + +from oauthlib import __version__ as OAUTHLIB_VERSION +from oauthlib.oauth2 import Server + +from .models import MyApplication + + +class MyServer(Server): + """ + A custom server which bypasses OAuth controls for every GET request + """ + def verify_request(self, uri, http_method='GET', body=None, headers=None, scopes=None): + ok, request = super(MyServer, self).verify_request(uri, http_method, body, headers, scopes) + if request.http_method == 'GET': + ok = True # possibly override failures + return ok, request + + +@csrf_exempt # so we can see 405 errors instead of 403 +@require_http_methods(["GET"]) +def get_system_info(request, *args, **kwargs): + """ + A simple "read only" api endpoint, unprotected + """ + data = { + 'DOT version': DOT_VERSION, + 'oauthlib version': OAUTHLIB_VERSION, + 'Django version': get_version(), + } + + return HttpResponse(json.dumps(data), content_type='application/json', *args, **kwargs) + + +@csrf_exempt +@protected_resource(server_cls=MyServer, scopes=["can_create_application"]) +@require_http_methods(["GET", "POST"]) +def applications_list(request, *args, **kwargs): + """ + List resources with GET, create a new one with POST. With custom server_cls we bypass oauth2 + controls and let everyone list applications. + """ + if request.method == 'GET': + # hide default Application in the playground + data = serializers.serialize("json", MyApplication.objects.exclude(pk=1)) + return HttpResponse(data, content_type='application/json', *args, **kwargs) + elif request.method == 'POST': + if request.is_ajax(): + try: + data = json.loads(request.body) + data['user'] = request.resource_owner + obj = MyApplication.objects.create(**data) + out = serializers.serialize("json", [obj]) + except (ValueError, TypeError): + return HttpResponseBadRequest() + + return HttpResponse(out, content_type='application/json', status=201, *args, **kwargs) + + +@csrf_exempt +@protected_resource() +@require_http_methods(["GET", "PUT", "DELETE"]) +def applications_detail(request, lookup, *args, **kwargs): + """ + Show resource with GET, update it with PUT, destroy with DELETE + """ + try: + resource = MyApplication.objects.filter(user=request.resource_owner)\ + .filter(client_id=lookup).get() + # hide default Application in the playground + if resource.pk == 1: + raise MyApplication.DoesNotExist + except MyApplication.DoesNotExist: + return HttpResponseNotFound() + + if request.method == 'GET': + data = serializers.serialize("json", [resource]) + return HttpResponse(data, content_type='application/json', status=200, *args, **kwargs) + elif request.method == 'PUT': + try: + data = json.loads(request.body) + for k, v in data.iteritems(): + setattr(resource, k, v) + resource.save() + data = serializers.serialize("json", [resource]) + return HttpResponse(data, content_type='application/json') + except (ValueError, TypeError): + return HttpResponseBadRequest() + elif request.method == 'DELETE': + resource.delete() + return HttpResponse(status=204) \ No newline at end of file diff --git a/stat_track/stat_track/apps/__init__.py b/stat_track/stat_track/apps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stat_track/stat_track/config/__init__.py b/stat_track/stat_track/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stat_track/stat_track/config/lib/__init__.py b/stat_track/stat_track/config/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stat_track/stat_track/config/lib/colorstreamhandler.py b/stat_track/stat_track/config/lib/colorstreamhandler.py new file mode 100644 index 0000000..cb2de93 --- /dev/null +++ b/stat_track/stat_track/config/lib/colorstreamhandler.py @@ -0,0 +1,118 @@ +# Colored stream handler for python logging framework based on: +# http://stackoverflow.com/questions/384076/how-can-i-color-python-logging-output/1336640#1336640 + +# Copyright (c) 2014 Markus Pointner +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import logging + +class _AnsiColorStreamHandler(logging.StreamHandler): + DEFAULT = '\x1b[0m' + RED = '\x1b[31m' + GREEN = '\x1b[32m' + YELLOW = '\x1b[33m' + CYAN = '\x1b[36m' + + CRITICAL = RED + ERROR = RED + WARNING = YELLOW + INFO = GREEN + DEBUG = CYAN + + @classmethod + def _get_color(cls, level): + if level >= logging.CRITICAL: return cls.CRITICAL + elif level >= logging.ERROR: return cls.ERROR + elif level >= logging.WARNING: return cls.WARNING + elif level >= logging.INFO: return cls.INFO + elif level >= logging.DEBUG: return cls.DEBUG + else: return cls.DEFAULT + + def __init__(self, stream=None): + logging.StreamHandler.__init__(self, stream) + + def format(self, record): + text = logging.StreamHandler.format(self, record) + color = self._get_color(record.levelno) + return color + text + self.DEFAULT + +class _WinColorStreamHandler(logging.StreamHandler): + # wincon.h + FOREGROUND_BLACK = 0x0000 + FOREGROUND_BLUE = 0x0001 + FOREGROUND_GREEN = 0x0002 + FOREGROUND_CYAN = 0x0003 + FOREGROUND_RED = 0x0004 + FOREGROUND_MAGENTA = 0x0005 + FOREGROUND_YELLOW = 0x0006 + FOREGROUND_GREY = 0x0007 + FOREGROUND_INTENSITY = 0x0008 # foreground color is intensified. + FOREGROUND_WHITE = FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED + + BACKGROUND_BLACK = 0x0000 + BACKGROUND_BLUE = 0x0010 + BACKGROUND_GREEN = 0x0020 + BACKGROUND_CYAN = 0x0030 + BACKGROUND_RED = 0x0040 + BACKGROUND_MAGENTA = 0x0050 + BACKGROUND_YELLOW = 0x0060 + BACKGROUND_GREY = 0x0070 + BACKGROUND_INTENSITY = 0x0080 # background color is intensified. + + DEFAULT = FOREGROUND_WHITE + CRITICAL = BACKGROUND_YELLOW | FOREGROUND_RED | FOREGROUND_INTENSITY | BACKGROUND_INTENSITY + ERROR = FOREGROUND_RED | FOREGROUND_INTENSITY + WARNING = FOREGROUND_YELLOW | FOREGROUND_INTENSITY + INFO = FOREGROUND_GREEN + DEBUG = FOREGROUND_CYAN + + @classmethod + def _get_color(cls, level): + if level >= logging.CRITICAL: return cls.CRITICAL + elif level >= logging.ERROR: return cls.ERROR + elif level >= logging.WARNING: return cls.WARNING + elif level >= logging.INFO: return cls.INFO + elif level >= logging.DEBUG: return cls.DEBUG + else: return cls.DEFAULT + + def _set_color(self, code): + import ctypes + ctypes.windll.kernel32.SetConsoleTextAttribute(self._outhdl, code) + + def __init__(self, stream=None): + logging.StreamHandler.__init__(self, stream) + # get file handle for the stream + import ctypes, ctypes.util + crtname = ctypes.util.find_msvcrt() + crtlib = ctypes.cdll.LoadLibrary(crtname) + self._outhdl = crtlib._get_osfhandle(stream.fileno()) + + def emit(self, record): + color = self._get_color(record.levelno) + self._set_color(color) + logging.StreamHandler.emit(self, record) + self._set_color(self.FOREGROUND_WHITE) + +# select ColorStreamHandler based on platform +import platform +if platform.system() == 'Windows': + ColorStreamHandler = _WinColorStreamHandler +else: + ColorStreamHandler = _AnsiColorStreamHandler diff --git a/stat_track/stat_track/config/lib/tdaemon.py b/stat_track/stat_track/config/lib/tdaemon.py new file mode 100644 index 0000000..e2b299e --- /dev/null +++ b/stat_track/stat_track/config/lib/tdaemon.py @@ -0,0 +1,309 @@ +#!/usr/bin/env python +#-*- coding: utf-8 -*- +""" +Multi-engine Test Daemon in Python + +Original concept by Jeff Winkler in: +http://jeffwinkler.net/nosy-run-python-unit-tests-automatically/ + +The present code is published under the terms of the MIT License. See LICENSE +file for more details. +""" + + +import sys +import os +import optparse +from time import sleep +import hashlib +import subprocess +import datetime +import re + +import logging +from config.lib.colorstreamhandler import ColorStreamHandler + +l = logging.getLogger() +l.addHandler(ColorStreamHandler()) + +SPECIAL_CHARS_REGEX_PATTERN = r'[#&;`|*?~<>^()\[\]{}$\\]+' +IGNORE_EXTENSIONS = ('pyc', 'pyo') +IGNORE_DIRS = ('.bzr', '.git', '.hg', '.darcs', '.svn', '.tox') +IMPLEMENTED_TEST_PROGRAMS = ('django-nose-coverage', 'coverage', 'nose', 'nosetests', 'django', 'py', + 'symfony', 'jelix', 'phpunit', 'sphinx', 'tox' +) + +# -------- Exceptions +class InvalidTestProgram(Exception): + """Raised as soon as an unexpected test program is chosen""" + pass + +class InvalidFilePath(Exception): + """Raised if the path to project/module is unknown/missing.""" + pass + +class CancelDueToUserRequest(Exception): + """Raised when user wants to cancel execution""" + pass + +# -------- Utils +def ask(message='Are you sure? [y/N]'): + """Asks the user his opinion.""" + agree = False + answer = raw_input(message).lower() + if answer.startswith('y'): + agree = True + return agree + +def escapearg(args): + """Escapes characters you don't want in arguments (preventing shell + injection)""" + return re.sub(SPECIAL_CHARS_REGEX_PATTERN, '', args) + +class Watcher(object): + """ + Watcher class. This is the daemon that is watching every file in the + directory and subdirectories, and that runs the test process. + """ + file_list = {} + debug = False + + def __init__(self, file_path, test_program, debug=False, custom_args='', + ignore_dirs=None, quiet=False): + # Safe filter + custom_args = escapearg(custom_args) + + self.file_path = file_path + self.ignore_dirs = list(IGNORE_DIRS) + if ignore_dirs: + self.ignore_dirs.extend([d for d in ignore_dirs.split(',')]) + self.file_list = self.walk(file_path) + self.test_program = test_program + self.custom_args = custom_args + self.quiet = quiet + + # check configuration + self.check_configuration(file_path, test_program, custom_args) + + self.check_dependencies() + self.debug = debug + self.cmd = self.get_cmd() + + + def check_configuration(self, file_path, test_program, custom_args): + """Checks if configuration is ok.""" + # checking filepath + if not os.path.isdir(file_path): + raise InvalidFilePath("INVALID CONFIGURATION: file path %s is not a directory" % + os.path.abspath(file_path) + ) + + if not test_program in IMPLEMENTED_TEST_PROGRAMS: + raise InvalidTestProgram('The `%s` is unknown, or not yet implemented. Please chose another one.' % test_program) + + if custom_args: + if not self.quiet and not ask("WARNING!!!\nYou are about to run the following command\n\n $ %s\n\nAre you sure you still want to proceed [y/N]? " % self.get_cmd()): + raise CancelDueToUserRequest('Test cancelled...') + + def check_dependencies(self): + "Checks if the test program is available in the python environnement" + if self.test_program == 'coverage': + try: + import coverage + except ImportError: + sys.exit('Coverage is not available on your system. Please install it and try to run it again') + if self.test_program == 'nose': + try: + import nose + except ImportError: + sys.exit('Nosetests is not available on your system. Please install it and try to run it again') + if self.test_program == 'py': + try: + import py + except: + sys.exit('py.test is not available on your system. Please install it and try to run it again') + if self.test_program == 'django': + try: + import django + except: + sys.exit('django is not available on your system. Please install it and try to run it again') + if self.test_program == 'phpunit': + try: + process = subprocess.check_call(['phpunit','--version']) + except: + sys.exit('phpunit is not available on your system. Please install it and try to run it again') + if self.test_program == 'tox': + try: + import tox + except ImportError: + sys.exit('tox is not available on your system. Please install it and try to run it again') + + + def get_cmd(self): + """Returns the full command to be executed at runtime""" + + cmd = None + if self.test_program == 'django-nose-coverage': + coverage = "coverage run $(which django-admin.py) test && coverage combine && coverage report && coverage html" + flake8 = "echo && echo '================================ Flake8 Summary ================================' && flake8 %s" % self.file_path + end = "echo '================================================================================'" + cmd = "%s; %s; %s" % (coverage, flake8, end) + elif self.test_program == 'coverage': + cmd = "coverage run %s/manage.py test" % self.file_path + elif self.test_program in ('nose', 'nosetests'): + cmd = "nosetests %s" % self.file_path + elif self.test_program == 'django': + executable = "%s/manage.py" % self.file_path + if os.path.exists(executable): + cmd = "python %s/manage.py test" % self.file_path + else: + cmd = "django-admin.py test" + elif self.test_program == 'py': + cmd = 'py.test %s' % self.file_path + elif self.test_program == 'symfony': + cmd = 'symfony test-all' + elif self.test_program == 'jelix': + # as seen on http://jelix.org/articles/fr/manuel-1.1/tests_unitaires + cmd = 'php tests.php' + elif self.test_program == 'phpunit': + cmd = 'phpunit' + elif self.test_program == 'sphinx': + cmd = 'make html' + elif self.test_program == 'tox': + cmd = 'tox' + + if not cmd: + raise InvalidTestProgram("The test program %s is unknown. Valid options are: `nose`, `django` and `py`" % self.test_program) + + # adding custom args + if self.custom_args: + cmd = '%s %s' % (cmd, self.custom_args) + return cmd + + # Path manipulation + def include(self, path): + """Returns `True` if the file is not ignored""" + for extension in IGNORE_EXTENSIONS: + if path.endswith(extension): + return False + parts = path.split(os.path.sep) + for part in parts: + if part in self.ignore_dirs: + return False + return True + + def walk(self, top, file_list={}): + """Walks the walk. nah, seriously: reads the file and stores a hashkey + corresponding to its content.""" + for root, dirs, files in os.walk(top, topdown=False): + if os.path.basename(root) in self.ignore_dirs: + # Do not dig in ignored dirs + continue + + for name in files: + full_path = os.path.join(root, name) + if self.include(full_path): + if os.path.isfile(full_path): + # preventing fail if the file vanishes + content = open(full_path).read() + hashcode = hashlib.sha224(content).hexdigest() + file_list[full_path] = hashcode + for name in dirs: + if name not in self.ignore_dirs: + self.walk(os.path.join(root, name), file_list) + return file_list + + def file_sizes(self): + """Returns total filesize (in MB)""" + size = sum(map(os.path.getsize, self.file_list)) + return size / 1024 / 1024 + + + def diff_list(self, list1, list2): + """Extracts differences between lists. For debug purposes""" + for key in list1: + if key in list2 and list2[key] != list1[key]: + print key + elif key not in list2: + print key + + def run(self, cmd): + """Runs the appropriate command""" + l.warning(datetime.datetime.now()) + output = subprocess.Popen(cmd, shell=True) + stdoutdata, stderrdata = output.communicate() + print "---------------------------------------------------------------------------------------" + print + + def run_tests(self): + """Execute tests""" + self.run(self.cmd) + + def loop(self): + """Main loop daemon.""" + while True: + sleep(1) + new_file_list = self.walk(self.file_path, {}) + if new_file_list != self.file_list: + if self.debug: + self.diff_list(new_file_list, self.file_list) + self.run_tests() + self.file_list = new_file_list + +def main(prog_args=None): + """ + What do you expect? + """ + if prog_args is None: + prog_args = sys.argv + + parser = optparse.OptionParser() + parser.usage = """Usage: %[prog] [options] []""" + parser.add_option("-t", "--test-program", dest="test_program", + default="nose", help="specifies the test-program to use. Valid values" + " include `coverage`, `nose` (or `nosetests`), `django`, `py` (for `py.test`), " + '`symfony`, `jelix` `phpunit` and `tox`') + parser.add_option("-d", "--debug", dest="debug", action="store_true", + default=False) + parser.add_option('-s', '--size-max', dest='size_max', default=25, + type="int", help="Sets the maximum size (in MB) of files.") + parser.add_option('--custom-args', dest='custom_args', default='', + type="str", + help="Defines custom arguments to pass after the test program command") + parser.add_option('--ignore-dirs', dest='ignore_dirs', default='', + type="str", + help="Defines directories to ignore. Use a comma-separated list.") + parser.add_option('-y', '--quiet', dest='quiet', action="store_true", + default=False, + help="Don't ask for any input.") + + opt, args = parser.parse_args(prog_args) + + if args[1:]: + path = args[1] + else: + path = '.' + + + try: + watcher = Watcher(path, opt.test_program, opt.debug, opt.custom_args, + opt.ignore_dirs, opt.quiet) + watcher_file_size = watcher.file_sizes() + if watcher_file_size > opt.size_max: + message = "It looks like the total file size (%dMb) is larger than the `max size` option (%dMb).\nThis may slow down the file comparison process, and thus the daemon performances.\nDo you wish to continue? [y/N] " % (watcher_file_size, opt.size_max) + + if not opt.quiet and not ask(message): + raise CancelDueToUserRequest('Ok, thx, bye...') + + print "Ready to watch file changes..." + watcher.loop() + except (KeyboardInterrupt, SystemExit): + # Ignore when you exit via Crtl-C + pass + except Exception, msg: + print msg + + print "Bye" + +if __name__ == '__main__': + main() diff --git a/stat_track/stat_track/config/settings/__init__.py b/stat_track/stat_track/config/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stat_track/stat_track/config/settings/base.py b/stat_track/stat_track/config/settings/base.py new file mode 100644 index 0000000..93a2fd8 --- /dev/null +++ b/stat_track/stat_track/config/settings/base.py @@ -0,0 +1,384 @@ +""" +Base settings and globals. + +For more information on this file, see +https://docs.djangoproject.com/en/dev/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/dev/ref/settings/ +""" + +from os.path import abspath, basename, dirname, join, normpath +from sys import path + + +########## PATH CONFIGURATION +# Absolute filesystem path to the config directory: +CONFIG_ROOT = dirname(dirname(abspath(__file__))) + +# Absolute filesystem path to the project directory: +PROJECT_ROOT = dirname(CONFIG_ROOT) + +# Absolute filesystem path to the django repo directory: +DJANGO_ROOT = dirname(PROJECT_ROOT) + +# Project name: +PROJECT_NAME = basename(PROJECT_ROOT).capitalize() + +# Project folder: +PROJECT_FOLDER = basename(PROJECT_ROOT) + +# Project domain: +PROJECT_DOMAIN = '%s.com' % PROJECT_NAME.lower() + +# Add our project to our pythonpath, this way we don't need to type our project +# name in our dotted import paths: +path.append(CONFIG_ROOT) +########## END PATH CONFIGURATION + + +########## EMAIL CONFIGURATION +# https://docs.djangoproject.com/en/dev/ref/settings/#email-subject-prefix +EMAIL_SUBJECT_PREFIX = '[%s] ' % PROJECT_NAME + +# https://docs.djangoproject.com/en/dev/ref/settings/#server-email +SERVER_EMAIL = 'Serverbot ' % PROJECT_DOMAIN +########## END EMAIL CONFIGURATION + + +########## MANAGER CONFIGURATION +# See https://docs.djangoproject.com/en/dev/ref/settings/#admins +ADMINS = ( + ('Dev Team', 'Dev Team ' % PROJECT_DOMAIN), +) + +# https://docs.djangoproject.com/en/dev/ref/settings/#managers +MANAGERS = ADMINS +########## END MANAGER CONFIGURATION + + +########## APP CONFIGURATION +DJANGO_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.humanize', + 'django.contrib.admin', + 'django.contrib.admindocs', +) + +THIRD_PARTY_APPS = ( + 'authtools', + 'django_extensions', + 'django_rq', + 'floppyforms', + 'pipeline', +) + +PROJECT_APPS = ( + 'accounts', +) + +EXTENSION_APPS = ( + 'extensions.authtools', + 'extensions.django_rq', + 'extensions.rq_scheduler', + 'extensions.sites', +) + +# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps +INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + PROJECT_APPS + EXTENSION_APPS +########## END APP CONFIGURATION + + +########## MIDDLEWARE CONFIGURATION +# https://docs.djangoproject.com/en/dev/ref/settings/#middleware-classes +MIDDLEWARE_CLASSES = ( + 'django.middleware.gzip.GZipMiddleware', + 'pipeline.middleware.MinifyHTMLMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +) +########## END MIDDLEWARE CONFIGURATION + + +########## MIGRATIONS CONFIGURATION +MIGRATION_MODULES = { + 'sites': 'extensions.sites.migrations' +} +########## END MIGRATIONS CONFIGURATION + + +########## DEBUG CONFIGURATION +# https://docs.djangoproject.com/en/dev/ref/settings/#debug +DEBUG = False + +# https://docs.djangoproject.com/en/dev/ref/settings/#template-debug +TEMPLATE_DEBUG = DEBUG +########## END DEBUG CONFIGURATION + + +########## SECRET CONFIGURATION +# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key +# Note: This key should only be used for development and testing. +SECRET_KEY = r"lle*l7qn&!tog)$1n$=#op1rst%e!7k8t-k@wm&&v@msnuo6ud" +########## END SECRET CONFIGURATION + + +########## FIXTURE CONFIGURATION +# https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FIXTURE_DIRS +FIXTURE_DIRS = ( + normpath(join(PROJECT_ROOT, 'fixtures')), +) +########## END FIXTURE CONFIGURATION + + +########## GENERAL CONFIGURATION +# https://docs.djangoproject.com/en/dev/ref/settings/#time-zone +TIME_ZONE = 'America/Los_Angeles' + +# https://docs.djangoproject.com/en/dev/ref/settings/#language-code +LANGUAGE_CODE = 'en-us' + +# https://docs.djangoproject.com/en/dev/ref/settings/#site-id +SITE_ID = 1 + +# https://docs.djangoproject.com/en/dev/ref/settings/#use-i18n +USE_I18N = True + +# https://docs.djangoproject.com/en/dev/ref/settings/#use-l10n +USE_L10N = True + +# https://docs.djangoproject.com/en/dev/ref/settings/#use-tz +USE_TZ = True +########## END GENERAL CONFIGURATION + + +########## TEMPLATE CONFIGURATION +# https://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors +TEMPLATE_CONTEXT_PROCESSORS = ( + 'django.contrib.auth.context_processors.auth', + 'django.core.context_processors.debug', + 'django.core.context_processors.i18n', + 'django.core.context_processors.media', + 'django.core.context_processors.static', + 'django.core.context_processors.tz', + 'django.contrib.messages.context_processors.messages', + 'django.core.context_processors.request', +) + +# https://docs.djangoproject.com/en/dev/ref/settings/#template-loaders +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', +) + +# https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs +TEMPLATE_DIRS = ( + normpath(join(PROJECT_ROOT, 'templates')), + normpath(join(PROJECT_ROOT, 'extensions')), +) +########## END TEMPLATE CONFIGURATION + + +########## MEDIA CONFIGURATION +# https://docs.djangoproject.com/en/dev/ref/settings/#media-root +MEDIA_ROOT = normpath(join(PROJECT_ROOT, 'media')) + +# https://docs.djangoproject.com/en/dev/ref/settings/#media-url +MEDIA_URL = '/media/' +########## END MEDIA CONFIGURATION + + +########## STATIC FILE CONFIGURATION +# https://docs.djangoproject.com/en/dev/ref/settings/#static-root +STATIC_ROOT = normpath(join(PROJECT_ROOT, 'public')) + +# https://docs.djangoproject.com/en/dev/ref/settings/#static-url +STATIC_URL = '/static/' + +# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS +STATICFILES_DIRS = ( + normpath(join(PROJECT_ROOT, 'static')), +) + +# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + 'pipeline.finders.PipelineFinder', + 'pipeline.finders.CachedFileFinder', +) + +# STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage' +########## END STATIC FILE CONFIGURATION + + +########## PIPELINE CONFIGURATION +# https://django-pipeline.readthedocs.org/en/latest/configuration.html +PIPELINE_CSS = { + 'master': { + 'source_filenames': ( + 'css/lib/*.css', + 'css/build/*.css', + ), + 'output_filename': 'css/master.css', + 'variant': 'datauri', + }, +} + +PIPELINE_CSS_COMPRESSOR = 'pipeline.compressors.yuglify.YuglifyCompressor' + +PIPELINE_JS = { + 'master': { + 'source_filenames': ( + 'js/lib/*.js', + 'js/build/*.js', + ), + 'output_filename': 'js/master.js', + 'extra_context': { + 'async': True, + }, + } +} + +PIPELINE_JS_COMPRESSOR = 'pipeline.compressors.yuglify.YuglifyCompressor' +########## END PIPELINE CONFIGURATION + + +########## URL CONFIGURATION +# https://docs.djangoproject.com/en/dev/ref/settings/#root-urlconf +ROOT_URLCONF = 'config.urls' +########## END URL CONFIGURATION + + +########## LOGIN/LOGOUT CONFIGURATION +# https://docs.djangoproject.com/en/dev/ref/settings/#login-redirect-url +LOGIN_REDIRECT_URL = '/' + +# https://docs.djangoproject.com/en/dev/ref/settings/#login-url +LOGIN_URL = '/login/' + +# https://docs.djangoproject.com/en/dev/ref/settings/#logout-url +LOGOUT_URL = '/logout/' +########## END LOGIN/LOGOUT CONFIGURATION + + +########## WSGI CONFIGURATION +# https://docs.djangoproject.com/en/dev/ref/settings/#wsgi-application +WSGI_APPLICATION = 'config.wsgi.application' +########## END WSGI CONFIGURATION + + +########## USER MODEL CONFIGURATION +AUTH_USER_MODEL = 'accounts.User' +########## END USER MODEL CONFIGURATION + + +########## TESTING CONFIGURATION +TEST_RUNNER = 'django.test.runner.DiscoverRunner' +########## END TESTING CONFIGURATION + + +########## LOGGING CONFIGURATION +# https://docs.djangoproject.com/en/dev/ref/settings/#logging +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'filters': { + 'production_only': { + '()': 'django.utils.log.RequireDebugFalse', + }, + 'development_only': { + '()': 'django.utils.log.RequireDebugTrue', + }, + 'readable_sql': { + '()': 'project_runpy.ReadableSqlFilter', + }, + }, + 'formatters': { + 'verbose': { + 'format': '[%(asctime)s] %(levelname)-8s [%(name)s:%(lineno)s] %(message)s', + 'datefmt': '%m/%d/%Y %H:%M:%S', + }, + 'simple': { + 'format': '%(levelname)-8s [%(name)s:%(lineno)s] %(message)s', + }, + }, + 'handlers': { + 'null': { + 'level': 'DEBUG', + 'class': 'logging.NullHandler', + }, + 'default': { + 'level': 'DEBUG', + 'class': 'config.lib.colorstreamhandler.ColorStreamHandler', + }, + 'console_dev': { + 'level': 'DEBUG', + 'filters': ['development_only'], + 'class': 'config.lib.colorstreamhandler.ColorStreamHandler', + 'formatter': 'simple', + }, + 'console_prod': { + 'level': 'INFO', + 'filters': ['production_only'], + 'class': 'config.lib.colorstreamhandler.ColorStreamHandler', + 'formatter': 'simple', + }, + 'file_log': { + 'level': 'DEBUG', + 'filters': ['development_only'], + 'class': 'logging.handlers.RotatingFileHandler', + 'filename': join(DJANGO_ROOT, 'logs/log.log'), + 'maxBytes': 1024 * 1024, + 'backupCount': 3, + 'formatter': 'verbose', + }, + 'file_sql': { + 'level': 'DEBUG', + 'filters': ['development_only'], + 'class': 'logging.handlers.RotatingFileHandler', + 'filename': join(DJANGO_ROOT, 'logs/sql.log'), + 'maxBytes': 1024 * 1024, + 'backupCount': 3, + 'formatter': 'verbose', + }, + 'mail_admins': { + 'level': 'ERROR', + 'filters': ['production_only'], + 'class': 'django.utils.log.AdminEmailHandler', + 'include_html': True, + }, + }, + # Catch-all modules that use logging + # Writes to console and file on development, only to console on production + 'root': { + 'handlers': ['console_dev', 'console_prod', 'file_log'], + 'level': 'DEBUG', + }, + 'loggers': { + # Write all SQL queries to a file + 'django.db.backends': { + 'handlers': ['file_sql'], + 'filters': ['readable_sql'], + 'level': 'DEBUG', + 'propagate': False, + }, + # Email admins when 500 error occurs + 'django.request': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': False, + }, + } +} +########## END LOGGING CONFIGURATION diff --git a/stat_track/stat_track/config/settings/dev.py b/stat_track/stat_track/config/settings/dev.py new file mode 100644 index 0000000..30d1911 --- /dev/null +++ b/stat_track/stat_track/config/settings/dev.py @@ -0,0 +1,126 @@ +"""Development settings and globals.""" + +from __future__ import absolute_import + +from .base import * + +import os + + +########## DEBUG CONFIGURATION +# https://docs.djangoproject.com/en/dev/ref/settings/#debug +DEBUG = True + +# https://docs.djangoproject.com/en/dev/ref/settings/#template-debug +TEMPLATE_DEBUG = DEBUG +########## END DEBUG CONFIGURATION + + +########## DATABASE CONFIGURATION +# https://docs.djangoproject.com/en/dev/ref/settings/#databases +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': '%s-dev' % PROJECT_NAME.lower(), + 'USER': '', + 'PASSWORD': '', + 'HOST': 'localhost', + 'PORT': '5432', + } +} +########## END DATABASE CONFIGURATION + + +########## EMAIL CONFIGURATION +# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' +########## END EMAIL CONFIGURATION + + +########## CACHE CONFIGURATION +# https://docs.djangoproject.com/en/dev/ref/settings/#caches +CACHE_ENGINES = { + 'redis': { + 'BACKEND': 'django_redis.cache.RedisCache', + 'LOCATION': 'localhost:6379:0', + }, + 'dummy': { + 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', + } +} + +CACHES = { + 'redis': { + 'BACKEND': 'django_redis.cache.RedisCache', + 'LOCATION': 'localhost:6379:0', + } +} + +CACHES['default'] = CACHE_ENGINES[os.getenv('CACHE', 'dummy')] + +SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db' +########## END CACHE CONFIGURATION + + +########## REDIS QUEUE CONFIGURATION +# https://github.com/ui/django-rq#support-for-django-redis-and-django-redis-cache +RQ_QUEUES = { + 'default': { + 'USE_REDIS_CACHE': 'redis' + }, + 'high': { + 'USE_REDIS_CACHE': 'redis' + }, + 'low': { + 'USE_REDIS_CACHE': 'redis' + } +} + +RQ_SHOW_ADMIN_LINK = True +########## END REDIS QUEUE CONFIGURATION + + +########## LOGGING CONFIGURATION +# https://docs.djangoproject.com/en/dev/ref/settings/#logging +LOGGERS = { + # Log requests locally without [INFO] tag + 'werkzeug': { + 'handlers': ['default'], + 'level': 'DEBUG', + 'propagate': False, + }, + # Log queue workers to console and file on development + 'rq.worker': { + 'handlers': ['default', 'file_log'], + 'level': 'DEBUG', + 'propagate': False, + }, +} + +LOGGING['loggers'].update(LOGGERS) +########## END LOGGING CONFIGURATION + + +########## TOOLBAR CONFIGURATION +# http://django-debug-toolbar.readthedocs.org/en/latest/installation.html#explicit-setup +INSTALLED_APPS += ( + 'debug_toolbar', +) + +MIDDLEWARE_CLASSES += ( + 'debug_toolbar.middleware.DebugToolbarMiddleware', +) + +DEBUG_TOOLBAR_PATCH_SETTINGS = False + +# http://django-debug-toolbar.readthedocs.org/en/latest/installation.html +INTERNAL_IPS = ('127.0.0.1',) +########## END TOOLBAR CONFIGURATION + + +########## SSL SERVER CONFIGURATION +# https://github.com/teddziuba/django-sslserver#getting-started +INSTALLED_APPS += ( + 'sslserver', +) +########## END SSL SERVER CONFIGURATION diff --git a/stat_track/stat_track/config/settings/prod.py b/stat_track/stat_track/config/settings/prod.py new file mode 100644 index 0000000..f27ac7b --- /dev/null +++ b/stat_track/stat_track/config/settings/prod.py @@ -0,0 +1,187 @@ +"""Production settings and globals.""" + +from __future__ import absolute_import + +from .base import * + +import os + + +########## HOST CONFIGURATION +# https://docs.djangoproject.com/en/1.5/releases/1.5/#allowed-hosts-required-in-production +ALLOWED_HOSTS = [PROJECT_DOMAIN, '.herokuapp.com', 'localhost', '127.0.0.1'] +########## END HOST CONFIGURATION + + +########## SECRET CONFIGURATION +# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key +SECRET_KEY = os.environ['SECRET_KEY'] +########## END SECRET CONFIGURATION + + +########## EMAIL CONFIGURATION +# https://docs.djangoproject.com/en/dev/ref/settings/#email-use-tls +EMAIL_USE_TLS = True + +# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' + +# https://docs.djangoproject.com/en/dev/ref/settings/#default-from-email +DEFAULT_FROM_EMAIL = '%s Team ' % (PROJECT_NAME, PROJECT_DOMAIN) + +# https://docs.djangoproject.com/en/dev/ref/settings/#email-host +EMAIL_HOST = 'smtp.gmail.com' + +# https://docs.djangoproject.com/en/dev/ref/settings/#email-port +EMAIL_PORT = 587 + +# https://docs.djangoproject.com/en/dev/ref/settings/#email-host-user +EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', '') + +# https://docs.djangoproject.com/en/dev/ref/settings/#email-host-password +EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', '') +########## END EMAIL CONFIGURATION + + +########## DATABASE CONFIGURATION +# Parse database configuration from $DATABASE_URL +import dj_database_url +DATABASES = {} +DATABASES['default'] = dj_database_url.config() +########## END DATABASE CONFIGURATION + + +########## TEMPLATE CONFIGURATION +# https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs +TEMPLATE_LOADERS = ( + ('django.template.loaders.cached.Loader', ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + )), +) +########## END TEMPLATE CONFIGURATION + + +########## LOGGING CONFIGURATION +# https://docs.djangoproject.com/en/dev/ref/settings/#logging +LOGGERS = { + # Log queue workers to console and file on development + 'rq.worker': { + 'handlers': ['default', 'file_log'], + 'level': 'INFO', + 'propagate': False, + } +} + +LOGGING['loggers'].update(LOGGERS) +########## END LOGGING CONFIGURATION + + +########## SECURITY CONFIGURATION +# http://django-secure.readthedocs.org/en/v0.1.2/settings.html +# INSTALLED_APPS += ( +# 'djangosecure', +# ) +# +# MIDDLEWARE_CLASSES += ( +# 'djangosecure.middleware.SecurityMiddleware', +# ) +# +# # Honor the 'X-Forwarded-Proto' header for request.is_secure() +# # https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-SECURE_PROXY_SSL_HEADER +# SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +# +# # Use this setting if SSL is being served through CloudFlare proxy +# SECURE_PROXY_SSL_HEADER = ('HTTP_CF_VISITOR', '{"scheme":"https"}',) +# +# # Set this to 30 seconds and then to 518400 when you can prove it works +# SECURE_HSTS_SECONDS = 30 +# SECURE_HSTS_INCLUDE_SUBDOMAINS = True +# SECURE_FRAME_DENY = True +# SECURE_CONTENT_TYPE_NOSNIFF = True +# SECURE_BROWSER_XSS_FILTER = True +# SESSION_COOKIE_SECURE = True +# SESSION_COOKIE_HTTPONLY = True +# CSRF_COOKIE_SECURE = True +# SECURE_SSL_REDIRECT = True +########## END SECURITY CONFIGURATION + + +########## CACHE/QUEUE CONFIGURATION +# https://docs.djangoproject.com/en/dev/ref/settings/#caches +RQ_QUEUES = {} + +if 'REDISCLOUD_URL' in os.environ: + redis_url = os.environ['REDISCLOUD_URL'] + + CACHES = { + 'default': { + 'BACKEND': 'django_redis.cache.RedisCache', + 'LOCATION': '%s/%s' % (redis_url, 0), + 'OPTIONS': { + 'CLIENT_CLASS': 'django_redis.client.DefaultClient', + } + } + } + + RQ_QUEUES = { + 'default': { + 'USE_REDIS_CACHE': 'default' + }, + 'high': { + 'USE_REDIS_CACHE': 'default' + }, + 'low': { + 'USE_REDIS_CACHE': 'default' + } + } + + RQ_SHOW_ADMIN_LINK = True + + SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db' +########## END CACHE/QUEUE CONFIGURATION + + +########## AMAZON S3 CONFIGURATION +# http://django-storages.readthedocs.org/en/latest/backends/amazon-S3.html +if 'AWS_ACCESS_KEY_ID' in os.environ: + AWS_ACCESS_KEY_ID = os.environ['AWS_ACCESS_KEY_ID'] + AWS_SECRET_ACCESS_KEY = os.environ['AWS_SECRET_ACCESS_KEY'] + + INSTALLED_APPS += ( + 'storages', + 'collectfast' + ) + + AWS_S3_SECURE_URLS = True + AWS_QUERYSTRING_AUTH = False + AWS_PRELOAD_METADATA = True + + AWS_EXPIREY = 60 * 60 * 24 * 7 + AWS_HEADERS = { + 'Cache-Control': 'max-age=%d, s-maxage=%d, must-revalidate' % (AWS_EXPIREY, AWS_EXPIREY) + } + + # Separate buckets for static files and media files + AWS_STATIC_STORAGE_BUCKET_NAME = '%s-static' % PROJECT_NAME.lower() + AWS_MEDIA_STORAGE_BUCKET_NAME = '%s-media' % PROJECT_NAME.lower() + S3_STATIC_URL = '//%s.s3.amazonaws.com/' % AWS_STATIC_STORAGE_BUCKET_NAME + S3_MEDIA_URL = '//%s.s3.amazonaws.com/' % AWS_MEDIA_STORAGE_BUCKET_NAME + + # Using django-pipeline along with S3 storage for staticfiles + # https://django-pipeline.readthedocs.org/en/latest/storages.html#using-with-other-storages + from django.contrib.staticfiles.storage import CachedFilesMixin + from pipeline.storage import PipelineMixin + from storages.backends.s3boto import S3BotoStorage + + class S3PipelineCachedStorage(PipelineMixin, CachedFilesMixin, S3BotoStorage): + pass + + StaticRootS3BotoStorage = lambda: S3PipelineCachedStorage(bucket=AWS_STATIC_STORAGE_BUCKET_NAME) + STATICFILES_STORAGE = 'config.settings.prod.StaticRootS3BotoStorage' + STATIC_URL = S3_STATIC_URL + + MediaRootS3BotoStorage = lambda: S3BotoStorage(bucket=AWS_MEDIA_STORAGE_BUCKET_NAME) + DEFAULT_FILE_STORAGE = 'config.settings.prod.MediaRootS3BotoStorage' + MEDIA_URL = S3_MEDIA_URL +########## END AMAZON S3 CONFIGURATION diff --git a/stat_track/stat_track/config/settings/test.py b/stat_track/stat_track/config/settings/test.py new file mode 100644 index 0000000..ee84295 --- /dev/null +++ b/stat_track/stat_track/config/settings/test.py @@ -0,0 +1,97 @@ +from __future__ import absolute_import + +from .base import * + + +########## TEST RUNNER CONFIGURATION +# https://github.com/django-nose/django-nose +INSTALLED_APPS += ( + 'django_nose', +) + +TEST = DEBUG = True + +TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' + +NOSE_ARGS = [ + '--where=%s' % PROJECT_FOLDER, + '--exclude-dir=%s' % join(CONFIG_ROOT, 'settings'), +] +########## END TEST RUNNER CONFIGURATION + + +########## IN-MEMORY TEST DATABASE +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': join(PROJECT_ROOT, 'test.sqlite3'), + 'USER': '', + 'PASSWORD': '', + 'HOST': '', + 'PORT': '', + }, +} + +PASSWORD_HASHERS = ( + 'django.contrib.auth.hashers.MD5PasswordHasher', +) +########## END IN-MEMORY TEST DATABASE + + +########## EMAIL CONFIGURATION +# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend +EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' +########## END EMAIL CONFIGURATION + + +########## CACHE CONFIGURATION +# https://docs.djangoproject.com/en/dev/ref/settings/#caches +CACHES = { + 'default': { + 'BACKEND': 'django_redis.cache.RedisCache', + 'LOCATION': 'localhost:6379:0', + } +} +########## END CACHE CONFIGURATION + + +########## REDIS QUEUE CONFIGURATION +# https://github.com/ui/django-rq#support-for-django-redis-and-django-redis-cache +RQ_QUEUES = { + 'default': { + 'USE_REDIS_CACHE': 'default', + 'ASYNC': False + }, + 'high': { + 'USE_REDIS_CACHE': 'default', + 'ASYNC': False + }, + 'low': { + 'USE_REDIS_CACHE': 'default', + 'ASYNC': False + } +} + +RQ_SHOW_ADMIN_LINK = True +########## END REDIS QUEUE CONFIGURATION + + +########## LOGGING CONFIGURATION +# https://docs.djangoproject.com/en/dev/ref/settings/#logging +LOGGERS = { + # Log queue workers to null but file while testing + 'rq.worker': { + 'handlers': ['null', 'file_log'], + 'level': 'INFO', + 'propagate': False, + }, + # Log nose test runner only INFO level and above + 'nose.plugins.manager': { + 'handlers': ['default', 'file_log'], + 'level': 'INFO', + 'propagate': False, + }, +} + +LOGGING['loggers'].update(LOGGERS) +########## END LOGGING CONFIGURATION diff --git a/stat_track/stat_track/config/urls.py b/stat_track/stat_track/config/urls.py new file mode 100644 index 0000000..3d13107 --- /dev/null +++ b/stat_track/stat_track/config/urls.py @@ -0,0 +1,67 @@ +from django.conf.urls import patterns, include, url +from django.contrib import admin +from django.core.urlresolvers import reverse_lazy +from django.views.generic import TemplateView + +from .views import ( + ConsumerView, ConsumerExchangeView, ConsumerDoneView, ApiEndpoint, ApiClientView +) +from .api_v1 import get_system_info, applications_list, applications_detail + +admin.autodiscover() + +urlpatterns = patterns( + '', + url( + regex=r'^$', + view=TemplateView.as_view(template_name='example/home.html'), + name='home' + ), + url( + regex=r'^accounts/login/$', + view='django.contrib.auth.views.login', + kwargs={'template_name': 'example/login.html'} + ), + url( + regex='^accounts/logout/$', + view='django.contrib.auth.views.logout', + kwargs={'next_page': reverse_lazy('home')} + ), + + # the Django admin + url(r'^admin/', include(admin.site.urls)), + + # consumer logic + url( + regex=r'^consumer/$', + view=ConsumerView.as_view(), + name="consumer" + ), + url( + regex=r'^consumer/exchange/', + view=ConsumerExchangeView.as_view(), + name='consumer-exchange' + ), + url( + regex=r'^consumer/done/', + view=ConsumerDoneView.as_view(), + name='consumer-done' + ), + url( + regex=r'^consumer/client/', + view=TemplateView.as_view(template_name='example/consumer-client.html'), + name='consumer-client' + ), + + # oauth2 urls + url(r'^o/', include('oauth2_provider.urls', namespace='oauth2_provider')), + + # api stuff to test server functionalities + url(r'^apiclient$', ApiClientView.as_view(), name='api-client'), + url(r'^api/hello$', ApiEndpoint.as_view(), name='Hello'), + + # api v1 + url(r'^api/v1/system_info$', get_system_info, name="System Info"), + url(r'^api/v1/applications$', applications_list, name="Application List"), + url(r'^api/v1/applications/(?P\w+)/$', applications_detail, name="Application Detail"), +) diff --git a/stat_track/stat_track/config/wsgi.py b/stat_track/stat_track/config/wsgi.py new file mode 100644 index 0000000..9f3e21b --- /dev/null +++ b/stat_track/stat_track/config/wsgi.py @@ -0,0 +1,32 @@ +""" +WSGI config for example project. + +This module contains the WSGI application used by Django's development server +and any production WSGI deployments. It should expose a module-level variable +named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover +this application via the ``WSGI_APPLICATION`` setting. + +Usually you will have the standard Django WSGI application here, but it also +might make sense to replace the whole Django WSGI application with a custom one +that later delegates to the Django one. For example, you could introduce WSGI +middleware here, or combine a Django application with an application of another +framework. + +""" +import os + +# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks +# if running multiple sites in the same mod_wsgi process. To fix this, use +# mod_wsgi daemon mode with each site in its own daemon process, or use +# os.environ["DJANGO_SETTINGS_MODULE"] = "example.settings" +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") + +# This application object is used by any WSGI server configured to use this +# file. This includes Django's development server, if the WSGI_APPLICATION +# setting points here. +from django.core.wsgi import get_wsgi_application +application = get_wsgi_application() + +# Apply WSGI middleware here. +# from helloworld.wsgi import HelloWorldApplication +# application = HelloWorldApplication(application) diff --git a/stat_track/stat_track/context_processors.py b/stat_track/stat_track/context_processors.py new file mode 100644 index 0000000..7c60887 --- /dev/null +++ b/stat_track/stat_track/context_processors.py @@ -0,0 +1,5 @@ +from oauth2_provider import VERSION + + +def dot_version(request): + return {'version': VERSION} diff --git a/stat_track/stat_track/core/__init__.py b/stat_track/stat_track/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stat_track/stat_track/core/tests/__init__.py b/stat_track/stat_track/core/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stat_track/stat_track/core/tests/test_views.py b/stat_track/stat_track/core/tests/test_views.py new file mode 100644 index 0000000..213722f --- /dev/null +++ b/stat_track/stat_track/core/tests/test_views.py @@ -0,0 +1,19 @@ +from django.test import TestCase +from django.core.cache import cache +from django.core.urlresolvers import reverse + + +class CoreViewsTestCase(TestCase): + def setUp(self): + """ Reset cache at the beginning of each test """ + cache.clear() + + def test_home_view(self): + response = self.client.get(reverse('core:home')) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'pages/home.html') + + response = self.client.get(reverse('core:home')) + self.assertEqual(response.status_code, 200) + # Cached view doesn't render template again + self.assertTemplateNotUsed(response, 'pages/home.html') diff --git a/stat_track/stat_track/core/urls.py b/stat_track/stat_track/core/urls.py new file mode 100644 index 0000000..22c4f31 --- /dev/null +++ b/stat_track/stat_track/core/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls import url + +from .views import home + +urlpatterns = [ + url(r'^$', home, name='home'), +] diff --git a/stat_track/stat_track/core/utils/__init__.py b/stat_track/stat_track/core/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stat_track/stat_track/core/utils/mail.py b/stat_track/stat_track/core/utils/mail.py new file mode 100644 index 0000000..f19a029 --- /dev/null +++ b/stat_track/stat_track/core/utils/mail.py @@ -0,0 +1,26 @@ +from django.core.mail import send_mail + +from django_rq import enqueue, job + + +@job +def send_mail_job(subject, message, from_email, recipient_list, + fail_silently=False, auth_user=None, auth_password=None, connection=None, html_message=None): + """ + `send_mail` function as django_rq job + + Usage: send_mail_job.delay(args) + """ + send_mail(subject, message, from_email, recipient_list, + fail_silently, auth_user, auth_password, connection, html_message) + + +def enqueue_send_mail(subject, message, from_email, recipient_list, + fail_silently=False, auth_user=None, auth_password=None, connection=None, html_message=None): + """ + `send_mail` function automatically enqueued + + Usage: enqueue_send_mail(args) + """ + enqueue(send_mail, subject, message, from_email, recipient_list, + fail_silently, auth_user, auth_password, connection, html_message) diff --git a/stat_track/stat_track/core/utils/tests/__init__.py b/stat_track/stat_track/core/utils/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stat_track/stat_track/core/utils/tests/test_mail_utils.py b/stat_track/stat_track/core/utils/tests/test_mail_utils.py new file mode 100644 index 0000000..d9079c2 --- /dev/null +++ b/stat_track/stat_track/core/utils/tests/test_mail_utils.py @@ -0,0 +1,35 @@ +from django.test import TestCase +from django.core import mail +from django.conf import settings + +from ..mail import send_mail_job, enqueue_send_mail + + +class CoreEmailUtilsTestCase(TestCase): + def setUp(self): + self.subject = "[Test] CoreEmailUtilsTestCase" + self.message = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean lobortis ornare vestibulum. Sed euismod euismod mattis. Suspendisse potenti. Vestibulum eget faucibus lacus. Quisque in eros augue. Sed diam lorem, finibus congue auctor vel, volutpat a lacus. Proin ut pellentesque nisi, ut dignissim erat. Donec fringilla venenatis est, a tempor turpis tempus a. Praesent eu magna lectus." + self.from_email = "Testbot " % settings.PROJECT_DOMAIN + self.recipient_list = ["test@example.com"] + + def test_send_mail_job(self): + self.assertEqual(len(mail.outbox), 0) + send_mail_job.delay(self.subject, self.message, self.from_email, self.recipient_list) + self.assertEqual(len(mail.outbox), 1) + + testEmail = mail.outbox[0] + self.assertIn("CoreEmailUtilsTestCase", testEmail.subject) + self.assertIn("Lorem ipsum", testEmail.body) + self.assertIn("Testbot", testEmail.from_email) + self.assertIn("test@example.com", testEmail.to) + + def test_enqueue_send_mail(self): + self.assertEqual(len(mail.outbox), 0) + enqueue_send_mail(self.subject, self.message, self.from_email, self.recipient_list) + self.assertEqual(len(mail.outbox), 1) + + testEmail = mail.outbox[0] + self.assertIn("CoreEmailUtilsTestCase", testEmail.subject) + self.assertIn("Lorem ipsum", testEmail.body) + self.assertIn("Testbot", testEmail.from_email) + self.assertIn("test@example.com", testEmail.to) diff --git a/stat_track/stat_track/core/views.py b/stat_track/stat_track/core/views.py new file mode 100644 index 0000000..c992b86 --- /dev/null +++ b/stat_track/stat_track/core/views.py @@ -0,0 +1,8 @@ +from django.views.generic import TemplateView +from django.views.decorators.cache import cache_page + + +class HomeView(TemplateView): + template_name = 'pages/home.html' + +home = cache_page(60 * 10)(HomeView.as_view()) diff --git a/stat_track/stat_track/extensions/__init__.py b/stat_track/stat_track/extensions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stat_track/stat_track/extensions/authtools/__init__.py b/stat_track/stat_track/extensions/authtools/__init__.py new file mode 100644 index 0000000..d7c26be --- /dev/null +++ b/stat_track/stat_track/extensions/authtools/__init__.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + + +class ExtAuthtoolsConfig(AppConfig): + name = 'extensions.authtools' + label = 'Authtools Extensions' + +default_app_config = 'extensions.authtools.ExtAuthtoolsConfig' diff --git a/stat_track/stat_track/extensions/authtools/tests/__init__.py b/stat_track/stat_track/extensions/authtools/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stat_track/stat_track/extensions/authtools/tests/test_views.py b/stat_track/stat_track/extensions/authtools/tests/test_views.py new file mode 100644 index 0000000..b2a508e --- /dev/null +++ b/stat_track/stat_track/extensions/authtools/tests/test_views.py @@ -0,0 +1,33 @@ +from django.test import TestCase +from django.core.urlresolvers import reverse + +from accounts.factories import UserFactory + + +class AuthtoolsViewsExtensionsTestCase(TestCase): + def setUp(self): + """ Create a user and log in """ + self.user = UserFactory(email='test@example.com') + self.client.login(email='test@example.com', password='password') + + def test_logout_view_extension(self): + response = self.client.get(reverse('logout')) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'accounts/logout.html') + + def test_password_reset_view_extension(self): + response = self.client.get(reverse('password_reset')) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'accounts/password_reset.html') + + def test_password_change_view_extension(self): + response = self.client.get(reverse('password_change')) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'accounts/password_change.html') + + password_change_payload = {'old_password': 'password', 'new_password1': '123', 'new_password2': '123'} + response = self.client.post(reverse('password_change'), password_change_payload) + self.assertRedirects(response, reverse('password_change_done')) + + # Test to make sure session is still logged in after password change + self.assertIn('_auth_user_id', self.client.session) diff --git a/stat_track/stat_track/extensions/authtools/urls.py b/stat_track/stat_track/extensions/authtools/urls.py new file mode 100644 index 0000000..87521e8 --- /dev/null +++ b/stat_track/stat_track/extensions/authtools/urls.py @@ -0,0 +1,18 @@ +from django.conf.urls import url + +from .views import login, logout, password_change, password_change_done, password_reset, \ + password_reset_done, password_reset_confirm, password_reset_confirm_uidb36, password_reset_complete + +urlpatterns = [ + url(r'^login/$', login, name='login'), + url(r'^logout/$', logout, name='logout'), + url(r'^password_change/$', password_change, name='password_change'), + url(r'^password_change/done/$', password_change_done, name='password_change_done'), + url(r'^password_reset/$', password_reset, name='password_reset'), + url(r'^password_reset/done/$', password_reset_done, name='password_reset_done'), + url(r'^password_reset/complete/$', password_reset_complete, name='password_reset_complete'), + url(r'^password_reset/(?P[0-9A-Za-z_\-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', + password_reset_confirm, name='password_reset_confirm'), + url(r'^password_reset/(?P[0-9A-Za-z]{1,13})-(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', + password_reset_confirm_uidb36, name='password_reset_confirm_uidb36'), +] diff --git a/stat_track/stat_track/extensions/authtools/views.py b/stat_track/stat_track/extensions/authtools/views.py new file mode 100644 index 0000000..2be3ac1 --- /dev/null +++ b/stat_track/stat_track/extensions/authtools/views.py @@ -0,0 +1,65 @@ +# Subclassing Authtools views +# https://github.com/fusionbox/django-authtools/blob/master/authtools/views.py + +from __future__ import absolute_import + +from authtools.views import LoginView, LogoutView, PasswordChangeView, PasswordChangeDoneView, \ + PasswordResetView, PasswordResetDoneView, PasswordResetConfirmView, PasswordResetCompleteView + + +class ExtLoginView(LoginView): + template_name = 'accounts/login.html' + +login = ExtLoginView.as_view() + + +class ExtLogoutView(LogoutView): + template_name = 'accounts/logout.html' + +logout = ExtLogoutView.as_view() + + +class ExtPasswordChangeView(PasswordChangeView): + template_name = 'accounts/password_change.html' + + def get_success_url(self): + from django.contrib.auth import update_session_auth_hash + # https://docs.djangoproject.com/en/1.7/topics/auth/default/#session-invalidation-on-password-change + + update_session_auth_hash(self.request, self.request.user) + return super(ExtPasswordChangeView, self).get_success_url() + +password_change = ExtPasswordChangeView.as_view() + + +class ExtPasswordChangeDoneView(PasswordChangeDoneView): + template_name = 'accounts/password_change_done.html' + +password_change_done = ExtPasswordChangeDoneView.as_view() + + +class ExtPasswordResetView(PasswordResetView): + template_name = 'accounts/password_reset.html' + subject_template_name = 'accounts/password_reset_subject.txt' + email_template_name = 'accounts/password_reset_email.html' + +password_reset = ExtPasswordResetView.as_view() + + +class ExtPasswordResetDoneView(PasswordResetDoneView): + template_name = 'accounts/password_reset_done.html' + +password_reset_done = ExtPasswordResetDoneView.as_view() + + +class ExtPasswordResetConfirmView(PasswordResetConfirmView): + template_name = 'accounts/password_reset_confirm.html' + +password_reset_confirm = ExtPasswordResetConfirmView.as_view() +password_reset_confirm_uidb36 = ExtPasswordResetConfirmView.as_view() + + +class ExtPasswordResetCompleteView(PasswordResetCompleteView): + template_name = 'accounts/password_reset_complete.html' + +password_reset_complete = ExtPasswordResetCompleteView.as_view() diff --git a/stat_track/stat_track/extensions/django_rq/__init__.py b/stat_track/stat_track/extensions/django_rq/__init__.py new file mode 100644 index 0000000..ee5baa0 --- /dev/null +++ b/stat_track/stat_track/extensions/django_rq/__init__.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + + +class ExtDjangoRQConfig(AppConfig): + name = 'extensions.django_rq' + label = 'Django RQ Extensions' + +default_app_config = 'extensions.django_rq.ExtDjangoRQConfig' diff --git a/stat_track/stat_track/extensions/django_rq/templates/job_detail.html b/stat_track/stat_track/extensions/django_rq/templates/job_detail.html new file mode 100644 index 0000000..73dcf8a --- /dev/null +++ b/stat_track/stat_track/extensions/django_rq/templates/job_detail.html @@ -0,0 +1,155 @@ +{% extends "admin/base_site.html" %} + +{% load url from future %} + +{% block extrastyle %} + {{ block.super }} + + + {% endblock %} + +{% block breadcrumbs %} + + {% endblock %} + +{% block content_title %}

Job Info

{% endblock %} + +{% block content %} + +
+ +
+ +
+
+ +
{{ job.id }}
+
+
+ +
+
+ +
{{ job.origin }}
+
+
+ +
+
+ +
{{ job.timeout }}
+
+
+ +
+
+ +
{{ job.result_ttl }}
+
+
+ +
+
+ +
{{ job.created_at }} ({{ job.created_at|timesince }} ago)
+
+
+ +
+
+ +
+ {{ job.enqueued_at }} + {% if job.enqueued_at|timesince %} + ({{ job.enqueued_at|timesince }} ago) + {% elif job.enqueued_at|timeuntil %} + (in {{ job.enqueued_at|timeuntil }}) + {% endif %} +
+
+
+ +
+
+ +
+ {% if job.get_status %} + {{ job.get_status }} + {% else %} + {{ job.status }} + {% endif %} +
+
+
+ +
+
+ +
{{ job.func_name }}
+
+
+ +
+
+ +
+ {% if job.args %} +
    + {% for arg in job.args %} +
  • {{ arg }}
  • + {% endfor %} +
+ {% endif %} +
+
+
+ +
+
+ +
+ {% if job.kwargs %} +
    + {% for key, value in job.kwargs.items %} +
  • {{ key }}: {{ value|escape }}
  • + {% endfor %} +
+ {% endif %} +
+
+
+ +
+
+ +
{% if job.exc_info %}{{ job.exc_info|linebreaks }}{% endif %}
+
+
+ +
+ +
+ + {% if job.is_failed %} +
+ {% csrf_token %} + +
+ {% endif %} +
+
+ +{% endblock %} diff --git a/stat_track/stat_track/extensions/django_rq/templates/jobs.html b/stat_track/stat_track/extensions/django_rq/templates/jobs.html new file mode 100644 index 0000000..7ef2b7f --- /dev/null +++ b/stat_track/stat_track/extensions/django_rq/templates/jobs.html @@ -0,0 +1,116 @@ +{% extends "admin/base_site.html" %} + +{% load admin_static %} +{% load url from future %} + +{% block extrastyle %} + {{ block.super }} + + {% endblock %} + +{% block extrahead %} + {{ block.super }} + + + + + {% endblock %} + + +{% block breadcrumbs %} + + {% endblock %} + +{% block content_title %}

Jobs in {{ queue.name }}

{% endblock %} + +{% block content %} + +
+ +
+
+ {% csrf_token %} +
+ + +
+
+ + + + + + + + + + + + + {% for job in jobs %} + + + + + + + + + {% endfor %} + +
+
+ +
+
+
ID
Created
Enqueued
Status
Callable
+ + + + {{ job.id }} + + {{ job.created_at }}{{ job.enqueued_at }} + {% if job.get_status %} + {{ job.get_status }} + {% else %} + {{ job.status }} + {% endif %} + {{ job.func_name }}
+
+

+ {% for p in page_range %} + {% if p == page %} + {{ p }} + {% elif forloop.last %} + {{ p }} + {% else %} + {{ p }} + {% endif %} + {% endfor %} + {{ num_jobs }} jobs +

+
+
+
+ +{% endblock %} diff --git a/stat_track/stat_track/extensions/django_rq/tests/test_views.py b/stat_track/stat_track/extensions/django_rq/tests/test_views.py new file mode 100644 index 0000000..1289094 --- /dev/null +++ b/stat_track/stat_track/extensions/django_rq/tests/test_views.py @@ -0,0 +1,104 @@ +from django.test import TestCase +from django.conf import settings +from django.core.mail import send_mail +from django.core.urlresolvers import reverse +from django.template.defaultfilters import date, time + +from django_rq import get_queue + +from accounts.factories import AdminUserFactory + +from datetime import datetime + + +class DjangoRQExtensionsTestCase(TestCase): + def setUp(self): + """ Create a superuser and log in """ + self.user = AdminUserFactory(email='test@example.com') + self.client.login(email='test@example.com', password='password') + + self.subject = "[Test] DjangoRQExtensionsTestCase" + self.message = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean lobortis ornare vestibulum. Sed euismod euismod mattis. Suspendisse potenti. Vestibulum eget faucibus lacus. Quisque in eros augue. Sed diam lorem, finibus congue auctor vel, volutpat a lacus. Proin ut pellentesque nisi, ut dignissim erat. Donec fringilla venenatis est, a tempor turpis tempus a. Praesent eu magna lectus." + self.from_email = "Testbot " % settings.PROJECT_DOMAIN + self.recipient_list = ["test@example.com"] + + self.queue = get_queue(async=True) + for i in range(3): + self.queue.enqueue(send_mail, self.subject, self.message, self.from_email, self.recipient_list) + + def tearDown(self): + self.queue.empty() + + def test_jobs_view_extension(self): + response = self.client.get(reverse('rq_jobs', kwargs={'queue_index': 0})) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'django_rq/templates/jobs.html') + self.assertContains(response, "%s, %s" % (date(datetime.now()), time(datetime.now()))) + + self.queue.empty() + response = self.client.get(reverse('rq_jobs', kwargs={'queue_index': 0})) + self.assertEqual(response.status_code, 200) + + def test_job_detail_view_extension(self): + job_id = self.queue.get_jobs()[0].id + response = self.client.get(reverse('rq_job_detail', kwargs={'queue_index': 0, 'job_id': job_id})) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'django_rq/templates/job_detail.html') + self.assertContains(response, "%s, %s" % (date(datetime.now()), time(datetime.now()))) + + response = self.client.get(reverse('rq_job_detail', kwargs={'queue_index': 0, 'job_id': "fake"})) + self.assertEqual(response.status_code, 404) + + response = self.client.get(reverse('rq_job_detail', kwargs={'queue_index': 0, 'job_id': "empty"})) + self.assertEqual(response.status_code, 200) + + def test_queues_view_extension(self): + response = self.client.get(reverse('rq_home')) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'django_rq/stats.html') + + def test_clear_queue_view_extension(self): + response = self.client.get(reverse('rq_clear', kwargs={'queue_index': 0})) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'django_rq/clear_queue.html') + + response = self.client.post(reverse('rq_clear', kwargs={'queue_index': 0})) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse('rq_jobs', kwargs={'queue_index': 0})) + + def test_delete_job_view_extension(self): + job_id = self.queue.get_jobs()[0].id + response = self.client.get(reverse('rq_delete_job', kwargs={'queue_index': 0, 'job_id': job_id})) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'django_rq/delete_job.html') + + response = self.client.post(reverse('rq_delete_job', kwargs={'queue_index': 0, 'job_id': job_id})) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse('rq_jobs', kwargs={'queue_index': 0})) + + def test_requeue_job_view_extension(self): + job_id = self.queue.get_jobs()[0].id + + response = self.client.get(reverse('rq_requeue_job', kwargs={'queue_index': 0, 'job_id': job_id})) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'django_rq/delete_job.html') + + response = self.client.post(reverse('rq_requeue_job', kwargs={'queue_index': 0, 'job_id': job_id})) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse('rq_jobs', kwargs={'queue_index': 0})) + + def test_action_view_extensions(self): + response = self.client.get(reverse('rq_actions', kwargs={'queue_index': 0})) + self.assertRedirects(response, reverse('rq_jobs', kwargs={'queue_index': 0})) + + def test_action_delete_view_extension(self): + job_ids = [job.id for job in self.queue.get_jobs()] + delete_action_payload = {'action': 'delete', '_selected_action': job_ids} + response = self.client.post(reverse('rq_actions', kwargs={'queue_index': 0}), delete_action_payload) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'django_rq/confirm_action.html') + self.assertContains(response, job_ids[0]) + + delete_action_payload = {'action': 'delete', 'job_ids': job_ids} + response = self.client.post(reverse('rq_actions', kwargs={'queue_index': 0}), delete_action_payload) + self.assertRedirects(response, reverse('rq_jobs', kwargs={'queue_index': 0})) diff --git a/stat_track/stat_track/extensions/django_rq/urls.py b/stat_track/stat_track/extensions/django_rq/urls.py new file mode 100644 index 0000000..314f914 --- /dev/null +++ b/stat_track/stat_track/extensions/django_rq/urls.py @@ -0,0 +1,13 @@ +from django.conf.urls import url + +from .views import jobs, job_detail, queues, clear_queue, delete_job, requeue_job_view, actions + +urlpatterns = [ + url(r'^$', queues, name='rq_home'), + url(r'^queues/(?P[\d]+)/$', jobs, name='rq_jobs'), + url(r'^queues/(?P[\d]+)/empty/$', clear_queue, name='rq_clear'), + url(r'^queues/(?P[\d]+)/(?P[-\w]+)/$', job_detail, name='rq_job_detail'), + url(r'^queues/(?P[\d]+)/(?P[-\w]+)/delete/$', delete_job, name='rq_delete_job'), + url(r'^queues/(?P[\d]+)/(?P[-\w]+)/requeue/$', requeue_job_view, name='rq_requeue_job'), + url(r'^queues/actions/(?P[\d]+)/$', actions, name='rq_actions'), +] diff --git a/stat_track/stat_track/extensions/django_rq/views.py b/stat_track/stat_track/extensions/django_rq/views.py new file mode 100644 index 0000000..a3ff72e --- /dev/null +++ b/stat_track/stat_track/extensions/django_rq/views.py @@ -0,0 +1,207 @@ +# Subclassing Django RQ views +# https://github.com/ui/django-rq/blob/master/django_rq/views.py + +from __future__ import division + +from math import ceil + +from django.contrib import messages +from django.contrib.admin.views.decorators import staff_member_required +from django.http import Http404 +from django.shortcuts import redirect, render +from django.utils.timezone import make_aware, utc + +from redis.exceptions import ResponseError +from rq import requeue_job, Worker +from rq.job import Job +from rq.exceptions import NoSuchJobError, InvalidJobOperationError + +from django_rq.queues import get_connection, get_queue_by_index +from django_rq.settings import QUEUES_LIST + + +# Extended `jobs` and `job_detail` view functions in order to make datetime timezone-aware +@staff_member_required +def jobs(request, queue_index): + queue_index = int(queue_index) + queue = get_queue_by_index(queue_index) + + items_per_page = 100 + num_jobs = queue.count + page = int(request.GET.get('page', 1)) + + if num_jobs > 0: + last_page = int(ceil(num_jobs / items_per_page)) + page_range = range(1, last_page + 1) + offset = items_per_page * (page - 1) + jobs = queue.get_jobs(offset, items_per_page) + for job in jobs: + job.created_at = make_aware(job.created_at, utc) + job.enqueued_at = make_aware(job.enqueued_at, utc) + else: + jobs = [] + page_range = [] + + context_data = { + 'title': 'Jobs in %s' % queue.name, + 'queue': queue, + 'queue_index': queue_index, + 'jobs': jobs, + 'num_jobs': num_jobs, + 'page': page, + 'page_range': page_range, + } + return render(request, 'django_rq/templates/jobs.html', context_data) + + +@staff_member_required +def job_detail(request, queue_index, job_id): + queue_index = int(queue_index) + queue = get_queue_by_index(queue_index) + try: + job = Job.fetch(job_id, connection=queue.connection) + job.created_at = make_aware(job.created_at, utc) + job.enqueued_at = make_aware(job.enqueued_at, utc) + except NoSuchJobError: + raise Http404("Couldn't find job with this ID: %s" % job_id) + + context_data = { + 'title': 'Job Info', + 'queue_index': queue_index, + 'job': job, + 'queue': queue, + } + return render(request, 'django_rq/templates/job_detail.html', context_data) + + +# Extended django_rq view functions with added 'title' context +@staff_member_required +def queues(request): + queues = [] + for index, config in enumerate(QUEUES_LIST): + queue = get_queue_by_index(index) + queue_data = { + 'name': queue.name, + 'jobs': queue.count, + 'index': index, + } + if queue.name == 'failed': + queue_data['workers'] = '-' + else: + connection = get_connection(queue.name) + all_workers = Worker.all(connection=connection) + queue_workers = [worker for worker in all_workers if queue in worker.queues] + queue_data['workers'] = len(queue_workers) + queues.append(queue_data) + + context_data = { + 'title': 'RQ Queues', + 'queues': queues + } + return render(request, 'django_rq/stats.html', context_data) + + +@staff_member_required +def clear_queue(request, queue_index): + queue_index = int(queue_index) + queue = get_queue_by_index(queue_index) + + if request.method == 'POST': + try: + queue.empty() + messages.info(request, 'You have successfully cleared the queue %s' % queue.name) + except ResponseError as e: # pragma: no cover + if 'EVALSHA' in e.message: + messages.error(request, 'This action is not supported on Redis versions < 2.6.0, please use the bulk delete command instead') + else: + raise e + return redirect('rq_jobs', queue_index) + + context_data = { + 'title': 'Are you sure?', + 'queue_index': queue_index, + 'queue': queue, + } + return render(request, 'django_rq/clear_queue.html', context_data) + + +@staff_member_required +def delete_job(request, queue_index, job_id): + queue_index = int(queue_index) + queue = get_queue_by_index(queue_index) + job = Job.fetch(job_id, connection=queue.connection) + + if request.method == 'POST': + # Remove job id from queue and delete the actual job + queue.connection._lrem(queue.key, 0, job.id) + job.delete() + messages.info(request, 'You have successfully deleted %s' % job.id) + return redirect('rq_jobs', queue_index) + + context_data = { + 'title': 'Are you sure?', + 'queue_index': queue_index, + 'job': job, + 'queue': queue, + } + return render(request, 'django_rq/delete_job.html', context_data) + + +@staff_member_required +def requeue_job_view(request, queue_index, job_id): + queue_index = int(queue_index) + queue = get_queue_by_index(queue_index) + job = Job.fetch(job_id, connection=queue.connection) + + if request.method == 'POST': + try: # pragma: no cover + requeue_job(job_id, connection=queue.connection) + messages.info(request, 'You have successfully requeued %s' % job.id) + return redirect('rq_job_detail', queue_index, job_id) + except InvalidJobOperationError as e: + messages.error(request, e.message) + return redirect('rq_jobs', queue_index) + + context_data = { + 'title': 'Are you sure?', + 'queue_index': queue_index, + 'job': job, + 'queue': queue, + } + return render(request, 'django_rq/delete_job.html', context_data) + + +@staff_member_required +def actions(request, queue_index): + queue_index = int(queue_index) + queue = get_queue_by_index(queue_index) + + if request.method == 'POST' and request.POST.get('action', False): + # Confirmation page for selected Action + if request.POST.get('_selected_action', False): + context_data = { + 'title': 'Are you sure?', + 'queue_index': queue_index, + 'action': request.POST['action'], + 'job_ids': request.POST.getlist('_selected_action'), + 'queue': queue, + } + return render(request, 'django_rq/confirm_action.html', context_data) + + # Performing the actual action + elif request.POST.get('job_ids', False): + job_ids = request.POST.getlist('job_ids') + + if request.POST['action'] == 'delete': + for job_id in job_ids: + job = Job.fetch(job_id, connection=queue.connection) + # Remove job id from queue and delete the actual job + queue.connection._lrem(queue.key, 0, job.id) + job.delete() + messages.info(request, 'You have successfully deleted %s jobs!' % len(job_ids)) + elif request.POST['action'] == 'requeue': # pragma: no cover + for job_id in job_ids: + requeue_job(job_id, connection=queue.connection) + messages.info(request, 'You have successfully requeued %d jobs!' % len(job_ids)) + + return redirect('rq_jobs', queue_index) diff --git a/stat_track/stat_track/extensions/rq_scheduler/__init__.py b/stat_track/stat_track/extensions/rq_scheduler/__init__.py new file mode 100644 index 0000000..608a4ef --- /dev/null +++ b/stat_track/stat_track/extensions/rq_scheduler/__init__.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + + +class ExtRQSchedulerConfig(AppConfig): + name = 'extensions.rq_scheduler' + label = 'RQ Scheduler Extensions' + +default_app_config = 'extensions.rq_scheduler.ExtRQSchedulerConfig' diff --git a/stat_track/stat_track/extensions/rq_scheduler/templates/clear_jobs.html b/stat_track/stat_track/extensions/rq_scheduler/templates/clear_jobs.html new file mode 100644 index 0000000..89265c5 --- /dev/null +++ b/stat_track/stat_track/extensions/rq_scheduler/templates/clear_jobs.html @@ -0,0 +1,42 @@ +{% extends "admin/base_site.html" %} + +{% load url from future %} + +{% block extrastyle %} + {{ block.super }} + + + {% endblock %} + +{% block breadcrumbs %} + + {% endblock %} + +{% block content_title %}

Are you sure?

{% endblock %} + +{% block content %} + +
+

+ Are you sure you want to clear all RQ Scheduled Jobs? + This action can not be undone. +

+
+ {% csrf_token %} +
+ +
+
+
+ +{% endblock %} diff --git a/stat_track/stat_track/extensions/rq_scheduler/templates/confirm_action.html b/stat_track/stat_track/extensions/rq_scheduler/templates/confirm_action.html new file mode 100644 index 0000000..4587038 --- /dev/null +++ b/stat_track/stat_track/extensions/rq_scheduler/templates/confirm_action.html @@ -0,0 +1,50 @@ +{% extends "admin/base_site.html" %} + +{% load url from future %} + +{% block extrastyle %} + {{ block.super }} + + + {% endblock %} + +{% block breadcrumbs %} + + {% endblock %} + +{% block content_title %}

Are you sure?

{% endblock %} + +{% block content %} + +
+

+ Are you sure you want to {{ action|capfirst }} the selected jobs from RQ Scheduled Jobs? These jobs are selected: +

+
    + {% for job_id in job_ids %} +
  • {{ job_id }}
  • + {% endfor %} +
+
+ {% csrf_token %} +
+ {% for job_id in job_ids %} + + {% endfor %} + + +
+
+
+ +{% endblock %} diff --git a/stat_track/stat_track/extensions/rq_scheduler/templates/delete_job.html b/stat_track/stat_track/extensions/rq_scheduler/templates/delete_job.html new file mode 100644 index 0000000..367bf22 --- /dev/null +++ b/stat_track/stat_track/extensions/rq_scheduler/templates/delete_job.html @@ -0,0 +1,43 @@ +{% extends "admin/base_site.html" %} + +{% load url from future %} + +{% block extrastyle %} + {{ block.super }} + + + {% endblock %} + +{% block breadcrumbs %} + + {% endblock %} + +{% block content_title %}

Are you sure?

{% endblock %} + +{% block content %} + +
+

+ Are you sure you want to delete {{ job.id }} from RQ Scheduled Jobs? + This action can not be undone. +

+
+ {% csrf_token %} +
+ +
+
+
+ +{% endblock %} diff --git a/stat_track/stat_track/extensions/rq_scheduler/templates/enqueue_job.html b/stat_track/stat_track/extensions/rq_scheduler/templates/enqueue_job.html new file mode 100644 index 0000000..b25a79a --- /dev/null +++ b/stat_track/stat_track/extensions/rq_scheduler/templates/enqueue_job.html @@ -0,0 +1,43 @@ +{% extends "admin/base_site.html" %} + +{% load url from future %} + +{% block extrastyle %} + {{ block.super }} + + + {% endblock %} + +{% block breadcrumbs %} + + {% endblock %} + +{% block content_title %}

Are you sure?

{% endblock %} + +{% block content %} + +
+

+ Are you sure you want to enqueue {{ job.id }} immediately and have it perform? + This action can not be undone. +

+
+ {% csrf_token %} +
+ +
+
+
+ +{% endblock %} diff --git a/stat_track/stat_track/extensions/rq_scheduler/templates/index.html b/stat_track/stat_track/extensions/rq_scheduler/templates/index.html new file mode 100644 index 0000000..5109cc6 --- /dev/null +++ b/stat_track/stat_track/extensions/rq_scheduler/templates/index.html @@ -0,0 +1,46 @@ +{% extends "admin/index.html" %} + +{% load url from future %} + +{% block sidebar %} + + {{ block.super }} + +
+

Redis Apps

+ +
+
+ + + + + + + +
Django RQ
+ + Queues + +
+
+
+ +
+
+ + + + + + + +
RQ Scheduler
+ + Scheduled Jobs + +
+
+
+ + {% endblock %} diff --git a/stat_track/stat_track/extensions/rq_scheduler/templates/job_detail.html b/stat_track/stat_track/extensions/rq_scheduler/templates/job_detail.html new file mode 100644 index 0000000..0177842 --- /dev/null +++ b/stat_track/stat_track/extensions/rq_scheduler/templates/job_detail.html @@ -0,0 +1,159 @@ +{% extends "admin/base_site.html" %} + +{% load url from future %} + +{% block extrastyle %} + {{ block.super }} + + + {% endblock %} + +{% block breadcrumbs %} + + {% endblock %} + +{% block content_title %}

Job Info

{% endblock %} + +{% block content %} + +
+ +
+ +
+
+ +
{{ job.id }}
+
+
+ +
+
+ +
{{ job.origin }}
+
+
+ +
+
+ +
{{ job.timeout }}
+
+
+ +
+
+ +
{{ job.result_ttl }}
+
+
+ +
+
+ +
{{ job.created_at }} ({{ job.created_at|timesince }} ago)
+
+
+ +
+
+ +
+ {% if job.scheduled_at %} + {{ job.scheduled_at }} (in {{ job.scheduled_at|timeuntil }}) + {% else %} + Queued + {% endif %} +
+
+
+ +
+
+ +
+ {% if job.enqueued_at %} + {{ job.enqueued_at }} (in {{ job.enqueued_at|timeuntil }}) + {% else %} + Not queued yet + {% endif %} +
+
+
+ +
+
+ +
{{ job.get_status }}
+
+
+ +
+
+ +
{{ job.func_name }}
+
+
+ +
+
+ +
+ {% if job.args %} +
    + {% for arg in job.args %} +
  • {{ arg }}
  • + {% endfor %} +
+ {% endif %} +
+
+
+ +
+
+ +
+ {% if job.kwargs %} +
    + {% for key, value in job.kwargs.items %} +
  • {{ key }}: {{ value|escape }}
  • + {% endfor %} +
+ {% endif %} +
+
+
+ +
+
+ +
{% if job.exc_info %}{{ job.exc_info|linebreaks }}{% endif %}
+
+
+ +
+ +
+ +
+ {% csrf_token %} + +
+
+ +
+ +{% endblock %} diff --git a/stat_track/stat_track/extensions/rq_scheduler/templates/jobs.html b/stat_track/stat_track/extensions/rq_scheduler/templates/jobs.html new file mode 100644 index 0000000..816f6c1 --- /dev/null +++ b/stat_track/stat_track/extensions/rq_scheduler/templates/jobs.html @@ -0,0 +1,112 @@ +{% extends "admin/base_site.html" %} + +{% load admin_static %} +{% load url from future %} + +{% block extrastyle %} + {{ block.super }} + + {% endblock %} + +{% block extrahead %} + {{ block.super }} + + + + + {% endblock %} + +{% block breadcrumbs %} + + {% endblock %} + +{% block content_title %}

Scheduled Jobs

{% endblock %} + +{% block content %} + +
+ +
+
+ {% csrf_token %} +
+ + +
+
+ + + + + + + + + + + + + {% for job in jobs %} + + + + + + + + + {% endfor %} + +
+
+ +
+
+
ID
Created
Scheduled
Callable
Args
+ + + + {{ job.id }} + + {{ job.created_at }}{{ job.scheduled_at }}{{ job.func_name }} + {% if job.args %} + {% for arg in job.args %} +
  • {{ arg }}
  • + {% endfor %} + {% endif %} +
    +
    +

    + {% for p in page_range %} + {% if p == page %} + {{ p }} + {% elif forloop.last %} + {{ p }} + {% else %} + {{ p }} + {% endif %} + {% endfor %} + {{ num_jobs }} jobs +

    +
    +
    +
    + +{% endblock %} diff --git a/stat_track/stat_track/extensions/rq_scheduler/tests/__init__.py b/stat_track/stat_track/extensions/rq_scheduler/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stat_track/stat_track/extensions/rq_scheduler/tests/test_views.py b/stat_track/stat_track/extensions/rq_scheduler/tests/test_views.py new file mode 100644 index 0000000..96bb595 --- /dev/null +++ b/stat_track/stat_track/extensions/rq_scheduler/tests/test_views.py @@ -0,0 +1,129 @@ +from django.test import TestCase +from django.conf import settings +from django.core.mail import send_mail +from django.core.urlresolvers import reverse + +from django_rq import get_scheduler + +from accounts.factories import AdminUserFactory + +from datetime import datetime +from pytz import timezone + + +class RQSchedulerViewExtensionsTestCase(TestCase): + def setUp(self): + """ Create a superuser and log in """ + self.user = AdminUserFactory(email='test@example.com') + self.client.login(email='test@example.com', password='password') + + self.subject = "[Test] RQSchedulerViewExtensionsTestCase" + self.message = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean lobortis ornare vestibulum. Sed euismod euismod mattis. Suspendisse potenti. Vestibulum eget faucibus lacus. Quisque in eros augue. Sed diam lorem, finibus congue auctor vel, volutpat a lacus. Proin ut pellentesque nisi, ut dignissim erat. Donec fringilla venenatis est, a tempor turpis tempus a. Praesent eu magna lectus." + self.from_email = "Testbot " % settings.PROJECT_DOMAIN + self.recipient_list = ["test@example.com"] + + self.scheduler = get_scheduler() + self.western = timezone('America/Los_Angeles') + self.scheduled_time = self.western.localize(datetime(2020, 1, 1)) + self.scheduler.enqueue_at(self.scheduled_time, send_mail, self.subject, self.message, self.from_email, self.recipient_list) + + def tearDown(self): + for job in self.scheduler.get_jobs(): + job.cancel() + + def test_jobs_view_extension(self): + response = self.client.get(reverse('rq_scheduler:jobs')) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'rq_scheduler/templates/jobs.html') + self.assertContains(response, "Jan. 1, 2020, midnight") + + def test_job_detail_view_extension(self): + job_id = self.scheduler.get_jobs()[0].id + response = self.client.get(reverse('rq_scheduler:job_detail', kwargs={'job_id': job_id})) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'rq_scheduler/templates/job_detail.html') + self.assertContains(response, "Jan. 1, 2020, midnight") + self.assertContains(response, "Not queued yet") + + response = self.client.get(reverse('rq_scheduler:job_detail', kwargs={'job_id': "fake"})) + self.assertEqual(response.status_code, 404) + + def test_delete_job_view_extension(self): + job_id = self.scheduler.get_jobs()[0].id + response = self.client.get(reverse('rq_scheduler:delete_job', kwargs={'job_id': job_id})) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'rq_scheduler/templates/delete_job.html') + self.assertContains(response, job_id) + self.assertEqual(len(self.scheduler.get_jobs()), 1) + + response = self.client.post(reverse('rq_scheduler:delete_job', kwargs={'job_id': job_id})) + self.assertRedirects(response, reverse('rq_scheduler:jobs')) + self.assertEqual(len(self.scheduler.get_jobs()), 0) + + def test_enqueue_job_view_extension(self): + job_id = self.scheduler.get_jobs()[0].id + response = self.client.get(reverse('rq_scheduler:enqueue_job', kwargs={'job_id': job_id})) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'rq_scheduler/templates/enqueue_job.html') + self.assertContains(response, job_id) + self.assertEqual(len(self.scheduler.get_jobs()), 1) + + response = self.client.post(reverse('rq_scheduler:enqueue_job', kwargs={'job_id': job_id})) + self.assertRedirects(response, reverse('rq_scheduler:jobs')) + self.assertEqual(len(self.scheduler.get_jobs()), 0) + + response = self.client.get(reverse('rq_scheduler:job_detail', kwargs={'job_id': job_id})) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'rq_scheduler/templates/job_detail.html') + self.assertContains(response, "django.core.mail.send_mail") + + def test_clear_jobs_view_extension(self): + for i in range(2): + self.scheduler.enqueue_at(self.scheduled_time, send_mail, self.subject, self.message, self.from_email, self.recipient_list) + + response = self.client.get(reverse('rq_scheduler:clear_jobs')) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'rq_scheduler/templates/clear_jobs.html') + self.assertEqual(len(self.scheduler.get_jobs()), 3) + + response = self.client.post(reverse('rq_scheduler:clear_jobs')) + self.assertRedirects(response, reverse('rq_scheduler:jobs')) + self.assertEqual(len(self.scheduler.get_jobs()), 0) + + def test_action_view_extension(self): + response = self.client.get(reverse('rq_scheduler:actions')) + self.assertRedirects(response, reverse('rq_scheduler:jobs')) + + def test_action_delete_view_extension(self): + for i in range(2): + self.scheduler.enqueue_at(self.scheduled_time, send_mail, self.subject, self.message, self.from_email, self.recipient_list) + + job_ids = [job.id for job in self.scheduler.get_jobs()] + delete_action_payload = {'action': 'delete', '_selected_action': job_ids} + response = self.client.post(reverse('rq_scheduler:actions'), delete_action_payload) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'rq_scheduler/templates/confirm_action.html') + self.assertContains(response, job_ids[0]) + self.assertEqual(len(self.scheduler.get_jobs()), 3) + + delete_action_payload = {'action': 'delete', 'job_ids': job_ids} + response = self.client.post(reverse('rq_scheduler:actions'), delete_action_payload) + self.assertRedirects(response, reverse('rq_scheduler:jobs')) + self.assertEqual(len(self.scheduler.get_jobs()), 0) + + def test_action_enqueue_view_extension(self): + for i in range(2): + self.scheduler.enqueue_at(self.scheduled_time, send_mail, self.subject, self.message, self.from_email, self.recipient_list) + + job_ids = [job.id for job in self.scheduler.get_jobs()] + enqueue_action_payload = {'action': 'enqueue', '_selected_action': job_ids} + response = self.client.post(reverse('rq_scheduler:actions'), enqueue_action_payload) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'rq_scheduler/templates/confirm_action.html') + self.assertContains(response, job_ids[0]) + self.assertEqual(len(self.scheduler.get_jobs()), 3) + + enqueue_action_payload = {'action': 'enqueue', 'job_ids': job_ids} + response = self.client.post(reverse('rq_scheduler:actions'), enqueue_action_payload) + self.assertRedirects(response, reverse('rq_scheduler:jobs')) + self.assertEqual(len(self.scheduler.get_jobs()), 0) diff --git a/stat_track/stat_track/extensions/rq_scheduler/urls.py b/stat_track/stat_track/extensions/rq_scheduler/urls.py new file mode 100644 index 0000000..6f94e7c --- /dev/null +++ b/stat_track/stat_track/extensions/rq_scheduler/urls.py @@ -0,0 +1,12 @@ +from django.conf.urls import url + +from .views import jobs, job_detail, delete_job, enqueue_job, clear_jobs, actions + +urlpatterns = [ + url(r'^$', jobs, name='jobs'), + url(r'^jobs/actions/$', actions, name='actions'), + url(r'^jobs/clear/$', clear_jobs, name='clear_jobs'), + url(r'^jobs/(?P[-\w]+)/$', job_detail, name='job_detail'), + url(r'^jobs/(?P[-\w]+)/delete/$', delete_job, name='delete_job'), + url(r'^jobs/(?P[-\w]+)/enqueue/$', enqueue_job, name='enqueue_job'), +] diff --git a/stat_track/stat_track/extensions/rq_scheduler/views.py b/stat_track/stat_track/extensions/rq_scheduler/views.py new file mode 100644 index 0000000..dd2aa46 --- /dev/null +++ b/stat_track/stat_track/extensions/rq_scheduler/views.py @@ -0,0 +1,159 @@ +from __future__ import division + +from django.contrib import admin +from django_rq.settings import SHOW_ADMIN_LINK + +# Change default admin template to include Redis links +if SHOW_ADMIN_LINK: + admin.site.index_template = 'rq_scheduler/templates/index.html' + +from math import ceil + +from django.contrib import messages +from django.contrib.admin.views.decorators import staff_member_required +from django.http import Http404 +from django.shortcuts import redirect, render +from django.utils.timezone import make_aware, utc + +from rq.job import Job +from rq.exceptions import NoSuchJobError + +from django_rq import get_scheduler + + +# Heavily modified views originally from +# https://github.com/ui/django-rq/blob/master/django_rq/views.py +@staff_member_required +def jobs(request): + scheduler = get_scheduler() + jobs_list = scheduler.get_jobs(with_times=True) + items_per_page = 100 + num_jobs = len(jobs_list) + page = int(request.GET.get('page', 1)) + + if num_jobs > 0: + last_page = int(ceil(num_jobs / items_per_page)) + page_range = range(1, last_page + 1) + offset = items_per_page * (page - 1) + + jobs_list = jobs_list[offset:offset + items_per_page] + for job, scheduled_at in jobs_list: + job.scheduled_at = make_aware(scheduled_at, utc) + job.created_at = make_aware(job.created_at, utc) + jobs = [job for job, scheduled_time in jobs_list] + else: + jobs = [] + page_range = [] + + context_data = { + 'title': 'Scheduled Jobs', + 'jobs': jobs, + 'num_jobs': num_jobs, + 'page': page, + 'page_range': page_range, + } + return render(request, 'rq_scheduler/templates/jobs.html', context_data) + + +@staff_member_required +def job_detail(request, job_id): + scheduler = get_scheduler() + try: + job = Job.fetch(job_id, connection=scheduler.connection) + except NoSuchJobError: + raise Http404("Couldn't find job with this ID: %s" % job_id) + + if job in scheduler: + jobs_list_with_times = scheduler.get_jobs(with_times=True) + jobs_list = [j for j, scheduled_at in jobs_list_with_times] + job.scheduled_at = jobs_list_with_times[jobs_list.index(job)][1] + job.scheduled_at = make_aware(job.scheduled_at, utc) + + job.created_at = make_aware(job.created_at, utc) + job.enqueued_at = make_aware(job.enqueued_at, utc) if job.enqueued_at else None + + context_data = { + 'title': 'Job Info', + 'job': job, + } + return render(request, 'rq_scheduler/templates/job_detail.html', context_data) + + +@staff_member_required +def delete_job(request, job_id): + scheduler = get_scheduler() + job = Job.fetch(job_id, connection=scheduler.connection) + + if request.method == 'POST': + job.cancel() + messages.info(request, 'You have successfully deleted %s' % job.id) + return redirect('rq_scheduler:jobs') + + context_data = { + 'title': 'Are you sure?', + 'job': job, + } + return render(request, 'rq_scheduler/templates/delete_job.html', context_data) + + +@staff_member_required +def enqueue_job(request, job_id): + scheduler = get_scheduler() + job = Job.fetch(job_id, connection=scheduler.connection) + + if request.method == 'POST': + scheduler.enqueue_job(job) + messages.info(request, 'You have successfully enqueued %s' % job.id) + return redirect('rq_scheduler:jobs') + + context_data = { + 'title': 'Are you sure?', + 'job': job, + } + return render(request, 'rq_scheduler/templates/enqueue_job.html', context_data) + + +@staff_member_required +def clear_jobs(request): + scheduler = get_scheduler() + if request.method == 'POST': + for job in scheduler.get_jobs(): + job.cancel() + messages.info(request, 'You have successfully cleared all scheduled jobs') + return redirect('rq_scheduler:jobs') + + context_data = { + 'title': 'Are you sure?', + } + return render(request, 'rq_scheduler/templates/clear_jobs.html', context_data) + + +@staff_member_required +def actions(request): + scheduler = get_scheduler() + + if request.method == 'POST' and request.POST.get('action', False): + # Confirmation page for selected Action + if request.POST.get('_selected_action', False): + context_data = { + 'action': request.POST['action'], + 'job_ids': request.POST.getlist('_selected_action'), + } + return render(request, 'rq_scheduler/templates/confirm_action.html', context_data) + + # Performing the actual action + elif request.POST.get('job_ids', False): + job_ids = request.POST.getlist('job_ids') + + if request.POST['action'] == 'delete': + for job_id in job_ids: + job = Job.fetch(job_id, connection=scheduler.connection) + job.cancel() + messages.info(request, 'You have successfully deleted %s jobs!' % len(job_ids)) + elif request.POST['action'] == 'enqueue': + for job_id in job_ids: + job = Job.fetch(job_id, connection=scheduler.connection) + scheduler.enqueue_job(job) + messages.info(request, 'You have successfully enqueued %d jobs!' % len(job_ids)) + + return redirect('rq_scheduler:jobs') diff --git a/stat_track/stat_track/extensions/sites/__init__.py b/stat_track/stat_track/extensions/sites/__init__.py new file mode 100644 index 0000000..b9aab01 --- /dev/null +++ b/stat_track/stat_track/extensions/sites/__init__.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + + +class ExtSitesConfig(AppConfig): + name = 'extensions.sites' + label = 'Sites Extensions' + +default_app_config = 'extensions.sites.ExtSitesConfig' diff --git a/stat_track/stat_track/extensions/sites/migrations/0001_initial.py b/stat_track/stat_track/extensions/sites/migrations/0001_initial.py new file mode 100644 index 0000000..87d55c7 --- /dev/null +++ b/stat_track/stat_track/extensions/sites/migrations/0001_initial.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import django.contrib.sites.models + + +class Migration(migrations.Migration): + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Site', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('domain', models.CharField(max_length=100, verbose_name='domain name', validators=[django.contrib.sites.models._simple_domain_name_validator])), + ('name', models.CharField(max_length=50, verbose_name='display name')), + ], + options={ + 'ordering': ('domain',), + 'db_table': 'django_site', + 'verbose_name': 'site', + 'verbose_name_plural': 'sites', + }, + bases=(models.Model,), + ), + ] diff --git a/stat_track/stat_track/extensions/sites/migrations/0002_set_site_domain_and_name.py b/stat_track/stat_track/extensions/sites/migrations/0002_set_site_domain_and_name.py new file mode 100644 index 0000000..9049e56 --- /dev/null +++ b/stat_track/stat_track/extensions/sites/migrations/0002_set_site_domain_and_name.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.conf import settings +from django.db import models, migrations + + +def update_site_forward(apps, schema_editor): + """Set site domain and name.""" + if settings.DEBUG: + defaults={ + "domain": "localhost:8000", + "name": "localhost" + } + else: + defaults={ + "domain": settings.PROJECT_DOMAIN, + "name": settings.PROJECT_NAME + } + + Site = apps.get_model("sites", "Site") + Site.objects.update_or_create( + id=settings.SITE_ID, + defaults=defaults + ) + + +def update_site_backward(apps, schema_editor): + """Revert site domain and name to default.""" + Site = apps.get_model("sites", "Site") + Site.objects.update_or_create( + id=settings.SITE_ID, + defaults={ + "domain": "example.com", + "name": "example" + } + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('sites', '0001_initial'), + ] + + operations = [ + migrations.RunPython(update_site_forward, update_site_backward), + ] diff --git a/stat_track/stat_track/extensions/sites/migrations/__init__.py b/stat_track/stat_track/extensions/sites/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stat_track/stat_track/fixtures/oauth2_provider_fixtures.json b/stat_track/stat_track/fixtures/oauth2_provider_fixtures.json new file mode 100644 index 0000000..a4e6516 --- /dev/null +++ b/stat_track/stat_track/fixtures/oauth2_provider_fixtures.json @@ -0,0 +1,46 @@ +[ + { + "pk": 2, + "model": "auth.user", + "fields": { + "username": "test", + "first_name": "", + "last_name": "", + "is_active": true, + "is_superuser": false, + "is_staff": false, + "groups": [], + "user_permissions": [], + "password": "pbkdf2_sha256$10000$bkV6jtZeivL0$EKBSPj1VvcysvZzejhcTvlUkWC+qFWlw2lLtStU9+GE=", + "email": "", + "date_joined": "2013-08-31T14:16:25.509Z" + } + }, + + { + "pk": 1, + "model": "example.myapplication", + "fields": { + "redirect_uris": "", + "name": "test_app", + "description": "", + "client_type": "public", + "user": 1, + "client_id": "client_id", + "client_secret": "client_secret", + "authorization_grant_type": "password" + } + }, + + { + "pk": 1, + "model": "oauth2_provider.accesstoken", + "fields": { + "application": 1, + "token": "test_access_token", + "expires": "2023-09-06T20:29:49Z", + "user": 2, + "scope": "can_create_application" + } + } +] diff --git a/stat_track/stat_track/forms.py b/stat_track/stat_track/forms.py new file mode 100644 index 0000000..b95e5b5 --- /dev/null +++ b/stat_track/stat_track/forms.py @@ -0,0 +1,26 @@ +from django import forms + + +class ConsumerForm(forms.Form): + client_id = forms.CharField() + authorization_url = forms.URLField() + + +class ConsumerExchangeForm(forms.Form): + code = forms.CharField(widget=forms.TextInput(attrs={'readonly': 'readonly'})) + state = forms.CharField(widget=forms.TextInput(attrs={'readonly': 'readonly'})) + token_url = forms.URLField() + grant_type = forms.CharField(widget=forms.HiddenInput(), initial='authorization_code') + redirect_url = forms.CharField(widget=forms.TextInput(attrs={'readonly': 'readonly'})) + client_id = forms.CharField() + client_secret = forms.CharField() + + +class AccessTokenDataForm(forms.Form): + access_token = forms.CharField(widget=forms.TextInput(attrs={'readonly': 'readonly'})) + token_type = forms.CharField(widget=forms.TextInput(attrs={'readonly': 'readonly'})) + expires_in = forms.CharField(widget=forms.TextInput(attrs={'readonly': 'readonly'})) + refresh_token = forms.CharField(widget=forms.TextInput(attrs={'readonly': 'readonly'})) + token_url = forms.URLField() + client_id = forms.CharField() + client_secret = forms.CharField() diff --git a/stat_track/stat_track/manage.py b/stat_track/stat_track/manage.py new file mode 100644 index 0000000..2ffeba8 --- /dev/null +++ b/stat_track/stat_track/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.dev") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/stat_track/stat_track/middleware.py b/stat_track/stat_track/middleware.py new file mode 100644 index 0000000..7524035 --- /dev/null +++ b/stat_track/stat_track/middleware.py @@ -0,0 +1,48 @@ +""" +All responses will have Access-Control-Allow-Origin, and Access-Control-Allow-Methods +header items. + +If a request has Access-Control-Request-Methods in the header, then an +HttpResponse object is returned with header containing Access-Control-Allow-Origin, +Access-Control-Allow-Methods, and Access-Control-Allow-Headers items. + +""" +from django import http +from django.conf import settings + + +XS_SHARING_ALLOWED_ORIGINS = getattr(settings, "XS_SHARING_ALLOWED_ORIGINS", '*') +XS_SHARING_ALLOWED_METHODS = getattr(settings, "XS_SHARING_ALLOWED_METHODS", ['POST', 'GET', 'OPTIONS', 'PUT', 'DELETE']) +XS_SHARING_ALLOWED_HEADERS = getattr(settings, "XS_SHARING_ALLOWED_HEADERS", ['x-requested-with', 'content-type', 'accept', 'origin', 'authorization']) + + +class XsSharingMiddleware(object): + """ + This middleware allows cross-domain XHR using the html5 postMessage API. + + eg. + Access-Control-Allow-Origin: http://api.example.com + Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE + Access-Control-Allow-Headers: ["Content-Type"] + + """ + def process_request(self, request): + + if 'HTTP_ACCESS_CONTROL_REQUEST_METHOD' in request.META: + response = http.HttpResponse() + response['Access-Control-Allow-Origin'] = XS_SHARING_ALLOWED_ORIGINS + response['Access-Control-Allow-Methods'] = ",".join(XS_SHARING_ALLOWED_METHODS) + response['Access-Control-Allow-Headers'] = ",".join(XS_SHARING_ALLOWED_HEADERS) + return response + + return None + + def process_response(self, request, response): + # Avoid unnecessary work + if response.has_header('Access-Control-Allow-Origin'): + return response + + response['Access-Control-Allow-Origin'] = XS_SHARING_ALLOWED_ORIGINS + response['Access-Control-Allow-Methods'] = ",".join(XS_SHARING_ALLOWED_METHODS) + + return response diff --git a/stat_track/stat_track/models.py b/stat_track/stat_track/models.py new file mode 100644 index 0000000..5b1ead9 --- /dev/null +++ b/stat_track/stat_track/models.py @@ -0,0 +1,10 @@ +from oauth2_provider.models import AbstractApplication + +from django.db import models + + +class MyApplication(AbstractApplication): + """ + Custom Application model which adds description field + """ + description = models.TextField(blank=True) diff --git a/stat_track/stat_track/oauth2_provider/base.html b/stat_track/stat_track/oauth2_provider/base.html new file mode 100644 index 0000000..eb86975 --- /dev/null +++ b/stat_track/stat_track/oauth2_provider/base.html @@ -0,0 +1 @@ +{% extends "example/base.html" %} \ No newline at end of file diff --git a/stat_track/stat_track/settings/__init__.py b/stat_track/stat_track/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stat_track/stat_track/settings/base.py b/stat_track/stat_track/settings/base.py new file mode 100644 index 0000000..dbc6427 --- /dev/null +++ b/stat_track/stat_track/settings/base.py @@ -0,0 +1,192 @@ +# Django settings for example project. +import os +from os.path import join, abspath, dirname + +import django.conf.global_settings as DEFAULT_SETTINGS + +# Root directory of our project +PROJECT_ROOT = abspath(join(abspath(dirname(__file__)), "..",)) + +DEBUG = os.environ.get('DJANGO_DEBUG', True) +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( + # ('Your Name', 'your_email@example.com'), +) + +DATABASES = {} + +MANAGERS = ADMINS + +# Hosts/domain names that are valid for this site; required if DEBUG is False +# See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts +ALLOWED_HOSTS = [] + +# Local time zone for this installation. Choices can be found here: +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# In a Windows environment this must be set to your system time zone. +TIME_ZONE = 'America/Chicago' + +# Language code for this installation. All choices can be found here: +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = 'en-us' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True + +# If you set this to False, Django will not format dates, numbers and +# calendars according to the current locale. +USE_L10N = True + +# If you set this to False, Django will not use timezone-aware datetimes. +USE_TZ = True + +# Absolute filesystem path to the directory that will hold user-uploaded files. +# Example: "/var/www/example.com/media/" +MEDIA_ROOT = join(PROJECT_ROOT, "media") + +# URL that handles the media served from MEDIA_ROOT. Make sure to use a +# trailing slash. +# Examples: "http://example.com/media/", "http://media.example.com/" +MEDIA_URL = '/media/' + +# Absolute path to the directory static files should be collected to. +# Don't put anything in this directory yourself; store your static files +# in apps' "static/" subdirectories and in STATICFILES_DIRS. +# Example: "/var/www/example.com/static/" +STATIC_ROOT = join(PROJECT_ROOT, "static") + +# URL prefix for static files. +# Example: "http://example.com/static/", "http://static.example.com/" +STATIC_URL = '/static/' + +# Additional locations of static files +STATICFILES_DIRS = ( + # Put strings here, like "/home/html/static" or "C:/www/django/static". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. +) + +# List of finder classes that know how to find static files in +# various locations. +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + # 'django.contrib.staticfiles.finders.DefaultStorageFinder', +) + +# Make this unique, and don't share it with anybody. +SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'do_not_use_this_key') + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + # 'django.template.loaders.eggs.Loader', +) + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'example.middleware.XsSharingMiddleware', + # Uncomment the next line for simple clickjacking protection: + # 'django.middleware.clickjacking.XFrameOptionsMiddleware', +) + +TEMPLATE_CONTEXT_PROCESSORS = DEFAULT_SETTINGS.TEMPLATE_CONTEXT_PROCESSORS + ( + "django.core.context_processors.request", + "example.context_processors.dot_version", +) + +ROOT_URLCONF = 'example.urls' + +# Python dotted path to the WSGI application used by Django's runserver. +WSGI_APPLICATION = 'example.wsgi.application' + +TEMPLATE_DIRS = ( + # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. + os.path.join(os.path.dirname(os.path.realpath(__file__)), 'templates'), +) + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.admin', + 'oauth2_provider', + 'south', + 'example', +) + +# A sample logging configuration. The only tangible logging +# performed by this configuration is to send an email to +# the site admins on every HTTP 500 error when DEBUG=False. +# See http://docs.djangoproject.com/en/dev/topics/logging for +# more details on how to customize your logging configuration. +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' + }, + 'simple': { + 'format': '%(levelname)s %(message)s' + }, + }, + 'filters': { + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse' + } + }, + 'handlers': { + 'mail_admins': { + 'level': 'ERROR', + 'filters': ['require_debug_false'], + 'class': 'django.utils.log.AdminEmailHandler' + }, + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'simple' + } + }, + 'loggers': { + 'django.request': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': True, + }, + 'oauth2_provider': { + 'handlers': ['console'], + 'level': 'DEBUG', + 'propagate': True, + }, + 'oauthlib': { + 'handlers': ['console'], + 'level': 'DEBUG', + 'propagate': True, + } + } +} + +OAUTH2_PROVIDER = { + 'SCOPES': {'example': 'This is an example scope'}, + 'APPLICATION_MODEL': 'example.MyApplication' +} + +from django.core.urlresolvers import reverse_lazy + +LOGIN_REDIRECT_URL = reverse_lazy('home') diff --git a/stat_track/stat_track/settings/dev.py b/stat_track/stat_track/settings/dev.py new file mode 100644 index 0000000..1875869 --- /dev/null +++ b/stat_track/stat_track/settings/dev.py @@ -0,0 +1,8 @@ +from .base import * + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': 'example.sqlite', + } +} diff --git a/stat_track/stat_track/settings/production.py b/stat_track/stat_track/settings/production.py new file mode 100644 index 0000000..bcc72cf --- /dev/null +++ b/stat_track/stat_track/settings/production.py @@ -0,0 +1,5 @@ +from .base import * + +# Parse database configuration from $DATABASE_URL +import dj_database_url +DATABASES['default'] = dj_database_url.config() diff --git a/stat_track/stat_track/static/css/lib/bootstrap.css b/stat_track/stat_track/static/css/lib/bootstrap.css new file mode 100644 index 0000000..372474f --- /dev/null +++ b/stat_track/stat_track/static/css/lib/bootstrap.css @@ -0,0 +1,5730 @@ +/*! + * Bootstrap v3.3.0 (http://getbootstrap.com) + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ + +/*! normalize.css v3.0.2 | MIT License | git.io/normalize */ +html { + font-family: sans-serif; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} +body { + margin: 0; +} +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +menu, +nav, +section, +summary { + display: block; +} +audio, +canvas, +progress, +video { + display: inline-block; + vertical-align: baseline; +} +audio:not([controls]) { + display: none; + height: 0; +} +[hidden], +template { + display: none; +} +a { + background-color: transparent; +} +a:active, +a:hover { + outline: 0; +} +abbr[title] { + border-bottom: 1px dotted; +} +b, +strong { + font-weight: bold; +} +dfn { + font-style: italic; +} +h1 { + margin: .67em 0; + font-size: 2em; +} +mark { + color: #000; + background: #ff0; +} +small { + font-size: 80%; +} +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} +sup { + top: -.5em; +} +sub { + bottom: -.25em; +} +img { + border: 0; +} +svg:not(:root) { + overflow: hidden; +} +figure { + margin: 1em 40px; +} +hr { + height: 0; + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; +} +pre { + overflow: auto; +} +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} +button, +input, +optgroup, +select, +textarea { + margin: 0; + font: inherit; + color: inherit; +} +button { + overflow: visible; +} +button, +select { + text-transform: none; +} +button, +html input[type="button"], +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; + cursor: pointer; +} +button[disabled], +html input[disabled] { + cursor: default; +} +button::-moz-focus-inner, +input::-moz-focus-inner { + padding: 0; + border: 0; +} +input { + line-height: normal; +} +input[type="checkbox"], +input[type="radio"] { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + padding: 0; +} +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} +input[type="search"] { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + -webkit-appearance: textfield; +} +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} +fieldset { + padding: .35em .625em .75em; + margin: 0 2px; + border: 1px solid #c0c0c0; +} +legend { + padding: 0; + border: 0; +} +textarea { + overflow: auto; +} +optgroup { + font-weight: bold; +} +table { + border-spacing: 0; + border-collapse: collapse; +} +td, +th { + padding: 0; +} +/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */ +@media print { + *, + *:before, + *:after { + color: #000 !important; + text-shadow: none !important; + background: transparent !important; + -webkit-box-shadow: none !important; + box-shadow: none !important; + } + a, + a:visited { + text-decoration: underline; + } + a[href]:after { + content: " (" attr(href) ")"; + } + abbr[title]:after { + content: " (" attr(title) ")"; + } + a[href^="#"]:after, + a[href^="javascript:"]:after { + content: ""; + } + pre, + blockquote { + border: 1px solid #999; + + page-break-inside: avoid; + } + thead { + display: table-header-group; + } + tr, + img { + page-break-inside: avoid; + } + img { + max-width: 100% !important; + } + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + h2, + h3 { + page-break-after: avoid; + } + select { + background: #fff !important; + } + .navbar { + display: none; + } + .btn > .caret, + .dropup > .btn > .caret { + border-top-color: #000 !important; + } + .label { + border: 1px solid #000; + } + .table { + border-collapse: collapse !important; + } + .table td, + .table th { + background-color: #fff !important; + } + .table-bordered th, + .table-bordered td { + border: 1px solid #ddd !important; + } +} +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +*:before, +*:after { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +html { + font-size: 10px; + + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} +body { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 1.42857143; + color: #333; + background-color: #fff; +} +input, +button, +select, +textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; +} +a { + color: #428bca; + text-decoration: none; +} +a:hover, +a:focus { + color: #2a6496; + text-decoration: underline; +} +a:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +figure { + margin: 0; +} +img { + vertical-align: middle; +} +.img-responsive, +.thumbnail > img, +.thumbnail a > img, +.carousel-inner > .item > img, +.carousel-inner > .item > a > img { + display: block; + max-width: 100%; + height: auto; +} +.img-rounded { + border-radius: 6px; +} +.img-thumbnail { + display: inline-block; + max-width: 100%; + height: auto; + padding: 4px; + line-height: 1.42857143; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 4px; + -webkit-transition: all .2s ease-in-out; + -o-transition: all .2s ease-in-out; + transition: all .2s ease-in-out; +} +.img-circle { + border-radius: 50%; +} +hr { + margin-top: 20px; + margin-bottom: 20px; + border: 0; + border-top: 1px solid #eee; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} +.sr-only-focusable:active, +.sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; +} +h1, +h2, +h3, +h4, +h5, +h6, +.h1, +.h2, +.h3, +.h4, +.h5, +.h6 { + font-family: inherit; + font-weight: 500; + line-height: 1.1; + color: inherit; +} +h1 small, +h2 small, +h3 small, +h4 small, +h5 small, +h6 small, +.h1 small, +.h2 small, +.h3 small, +.h4 small, +.h5 small, +.h6 small, +h1 .small, +h2 .small, +h3 .small, +h4 .small, +h5 .small, +h6 .small, +.h1 .small, +.h2 .small, +.h3 .small, +.h4 .small, +.h5 .small, +.h6 .small { + font-weight: normal; + line-height: 1; + color: #777; +} +h1, +.h1, +h2, +.h2, +h3, +.h3 { + margin-top: 20px; + margin-bottom: 10px; +} +h1 small, +.h1 small, +h2 small, +.h2 small, +h3 small, +.h3 small, +h1 .small, +.h1 .small, +h2 .small, +.h2 .small, +h3 .small, +.h3 .small { + font-size: 65%; +} +h4, +.h4, +h5, +.h5, +h6, +.h6 { + margin-top: 10px; + margin-bottom: 10px; +} +h4 small, +.h4 small, +h5 small, +.h5 small, +h6 small, +.h6 small, +h4 .small, +.h4 .small, +h5 .small, +.h5 .small, +h6 .small, +.h6 .small { + font-size: 75%; +} +h1, +.h1 { + font-size: 36px; +} +h2, +.h2 { + font-size: 30px; +} +h3, +.h3 { + font-size: 24px; +} +h4, +.h4 { + font-size: 18px; +} +h5, +.h5 { + font-size: 14px; +} +h6, +.h6 { + font-size: 12px; +} +p { + margin: 0 0 10px; +} +.lead { + margin-bottom: 20px; + font-size: 16px; + font-weight: 300; + line-height: 1.4; +} +@media (min-width: 768px) { + .lead { + font-size: 21px; + } +} +small, +.small { + font-size: 85%; +} +mark, +.mark { + padding: .2em; + background-color: #fcf8e3; +} +.text-left { + text-align: left; +} +.text-right { + text-align: right; +} +.text-center { + text-align: center; +} +.text-justify { + text-align: justify; +} +.text-nowrap { + white-space: nowrap; +} +.text-lowercase { + text-transform: lowercase; +} +.text-uppercase { + text-transform: uppercase; +} +.text-capitalize { + text-transform: capitalize; +} +.text-muted { + color: #777; +} +.text-primary { + color: #428bca; +} +a.text-primary:hover { + color: #3071a9; +} +.text-success { + color: #3c763d; +} +a.text-success:hover { + color: #2b542c; +} +.text-info { + color: #31708f; +} +a.text-info:hover { + color: #245269; +} +.text-warning { + color: #8a6d3b; +} +a.text-warning:hover { + color: #66512c; +} +.text-danger { + color: #a94442; +} +a.text-danger:hover { + color: #843534; +} +.bg-primary { + color: #fff; + background-color: #428bca; +} +a.bg-primary:hover { + background-color: #3071a9; +} +.bg-success { + background-color: #dff0d8; +} +a.bg-success:hover { + background-color: #c1e2b3; +} +.bg-info { + background-color: #d9edf7; +} +a.bg-info:hover { + background-color: #afd9ee; +} +.bg-warning { + background-color: #fcf8e3; +} +a.bg-warning:hover { + background-color: #f7ecb5; +} +.bg-danger { + background-color: #f2dede; +} +a.bg-danger:hover { + background-color: #e4b9b9; +} +.page-header { + padding-bottom: 9px; + margin: 40px 0 20px; + border-bottom: 1px solid #eee; +} +ul, +ol { + margin-top: 0; + margin-bottom: 10px; +} +ul ul, +ol ul, +ul ol, +ol ol { + margin-bottom: 0; +} +.list-unstyled { + padding-left: 0; + list-style: none; +} +.list-inline { + padding-left: 0; + margin-left: -5px; + list-style: none; +} +.list-inline > li { + display: inline-block; + padding-right: 5px; + padding-left: 5px; +} +dl { + margin-top: 0; + margin-bottom: 20px; +} +dt, +dd { + line-height: 1.42857143; +} +dt { + font-weight: bold; +} +dd { + margin-left: 0; +} +@media (min-width: 768px) { + .dl-horizontal dt { + float: left; + width: 160px; + overflow: hidden; + clear: left; + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; + } + .dl-horizontal dd { + margin-left: 180px; + } +} +abbr[title], +abbr[data-original-title] { + cursor: help; + border-bottom: 1px dotted #777; +} +.initialism { + font-size: 90%; + text-transform: uppercase; +} +blockquote { + padding: 10px 20px; + margin: 0 0 20px; + font-size: 17.5px; + border-left: 5px solid #eee; +} +blockquote p:last-child, +blockquote ul:last-child, +blockquote ol:last-child { + margin-bottom: 0; +} +blockquote footer, +blockquote small, +blockquote .small { + display: block; + font-size: 80%; + line-height: 1.42857143; + color: #777; +} +blockquote footer:before, +blockquote small:before, +blockquote .small:before { + content: '\2014 \00A0'; +} +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + text-align: right; + border-right: 5px solid #eee; + border-left: 0; +} +.blockquote-reverse footer:before, +blockquote.pull-right footer:before, +.blockquote-reverse small:before, +blockquote.pull-right small:before, +.blockquote-reverse .small:before, +blockquote.pull-right .small:before { + content: ''; +} +.blockquote-reverse footer:after, +blockquote.pull-right footer:after, +.blockquote-reverse small:after, +blockquote.pull-right small:after, +.blockquote-reverse .small:after, +blockquote.pull-right .small:after { + content: '\00A0 \2014'; +} +address { + margin-bottom: 20px; + font-style: normal; + line-height: 1.42857143; +} +code, +kbd, +pre, +samp { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; +} +code { + padding: 2px 4px; + font-size: 90%; + color: #c7254e; + background-color: #f9f2f4; + border-radius: 4px; +} +kbd { + padding: 2px 4px; + font-size: 90%; + color: #fff; + background-color: #333; + border-radius: 3px; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25); +} +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: bold; + -webkit-box-shadow: none; + box-shadow: none; +} +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 1.42857143; + color: #333; + word-break: break-all; + word-wrap: break-word; + background-color: #f5f5f5; + border: 1px solid #ccc; + border-radius: 4px; +} +pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; +} +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} +.container { + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} +@media (min-width: 768px) { + .container { + width: 750px; + } +} +@media (min-width: 992px) { + .container { + width: 970px; + } +} +@media (min-width: 1200px) { + .container { + width: 1170px; + } +} +.container-fluid { + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} +.row { + margin-right: -15px; + margin-left: -15px; +} +.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; +} +.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 { + float: left; +} +.col-xs-12 { + width: 100%; +} +.col-xs-11 { + width: 91.66666667%; +} +.col-xs-10 { + width: 83.33333333%; +} +.col-xs-9 { + width: 75%; +} +.col-xs-8 { + width: 66.66666667%; +} +.col-xs-7 { + width: 58.33333333%; +} +.col-xs-6 { + width: 50%; +} +.col-xs-5 { + width: 41.66666667%; +} +.col-xs-4 { + width: 33.33333333%; +} +.col-xs-3 { + width: 25%; +} +.col-xs-2 { + width: 16.66666667%; +} +.col-xs-1 { + width: 8.33333333%; +} +.col-xs-pull-12 { + right: 100%; +} +.col-xs-pull-11 { + right: 91.66666667%; +} +.col-xs-pull-10 { + right: 83.33333333%; +} +.col-xs-pull-9 { + right: 75%; +} +.col-xs-pull-8 { + right: 66.66666667%; +} +.col-xs-pull-7 { + right: 58.33333333%; +} +.col-xs-pull-6 { + right: 50%; +} +.col-xs-pull-5 { + right: 41.66666667%; +} +.col-xs-pull-4 { + right: 33.33333333%; +} +.col-xs-pull-3 { + right: 25%; +} +.col-xs-pull-2 { + right: 16.66666667%; +} +.col-xs-pull-1 { + right: 8.33333333%; +} +.col-xs-pull-0 { + right: auto; +} +.col-xs-push-12 { + left: 100%; +} +.col-xs-push-11 { + left: 91.66666667%; +} +.col-xs-push-10 { + left: 83.33333333%; +} +.col-xs-push-9 { + left: 75%; +} +.col-xs-push-8 { + left: 66.66666667%; +} +.col-xs-push-7 { + left: 58.33333333%; +} +.col-xs-push-6 { + left: 50%; +} +.col-xs-push-5 { + left: 41.66666667%; +} +.col-xs-push-4 { + left: 33.33333333%; +} +.col-xs-push-3 { + left: 25%; +} +.col-xs-push-2 { + left: 16.66666667%; +} +.col-xs-push-1 { + left: 8.33333333%; +} +.col-xs-push-0 { + left: auto; +} +.col-xs-offset-12 { + margin-left: 100%; +} +.col-xs-offset-11 { + margin-left: 91.66666667%; +} +.col-xs-offset-10 { + margin-left: 83.33333333%; +} +.col-xs-offset-9 { + margin-left: 75%; +} +.col-xs-offset-8 { + margin-left: 66.66666667%; +} +.col-xs-offset-7 { + margin-left: 58.33333333%; +} +.col-xs-offset-6 { + margin-left: 50%; +} +.col-xs-offset-5 { + margin-left: 41.66666667%; +} +.col-xs-offset-4 { + margin-left: 33.33333333%; +} +.col-xs-offset-3 { + margin-left: 25%; +} +.col-xs-offset-2 { + margin-left: 16.66666667%; +} +.col-xs-offset-1 { + margin-left: 8.33333333%; +} +.col-xs-offset-0 { + margin-left: 0; +} +@media (min-width: 768px) { + .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 { + float: left; + } + .col-sm-12 { + width: 100%; + } + .col-sm-11 { + width: 91.66666667%; + } + .col-sm-10 { + width: 83.33333333%; + } + .col-sm-9 { + width: 75%; + } + .col-sm-8 { + width: 66.66666667%; + } + .col-sm-7 { + width: 58.33333333%; + } + .col-sm-6 { + width: 50%; + } + .col-sm-5 { + width: 41.66666667%; + } + .col-sm-4 { + width: 33.33333333%; + } + .col-sm-3 { + width: 25%; + } + .col-sm-2 { + width: 16.66666667%; + } + .col-sm-1 { + width: 8.33333333%; + } + .col-sm-pull-12 { + right: 100%; + } + .col-sm-pull-11 { + right: 91.66666667%; + } + .col-sm-pull-10 { + right: 83.33333333%; + } + .col-sm-pull-9 { + right: 75%; + } + .col-sm-pull-8 { + right: 66.66666667%; + } + .col-sm-pull-7 { + right: 58.33333333%; + } + .col-sm-pull-6 { + right: 50%; + } + .col-sm-pull-5 { + right: 41.66666667%; + } + .col-sm-pull-4 { + right: 33.33333333%; + } + .col-sm-pull-3 { + right: 25%; + } + .col-sm-pull-2 { + right: 16.66666667%; + } + .col-sm-pull-1 { + right: 8.33333333%; + } + .col-sm-pull-0 { + right: auto; + } + .col-sm-push-12 { + left: 100%; + } + .col-sm-push-11 { + left: 91.66666667%; + } + .col-sm-push-10 { + left: 83.33333333%; + } + .col-sm-push-9 { + left: 75%; + } + .col-sm-push-8 { + left: 66.66666667%; + } + .col-sm-push-7 { + left: 58.33333333%; + } + .col-sm-push-6 { + left: 50%; + } + .col-sm-push-5 { + left: 41.66666667%; + } + .col-sm-push-4 { + left: 33.33333333%; + } + .col-sm-push-3 { + left: 25%; + } + .col-sm-push-2 { + left: 16.66666667%; + } + .col-sm-push-1 { + left: 8.33333333%; + } + .col-sm-push-0 { + left: auto; + } + .col-sm-offset-12 { + margin-left: 100%; + } + .col-sm-offset-11 { + margin-left: 91.66666667%; + } + .col-sm-offset-10 { + margin-left: 83.33333333%; + } + .col-sm-offset-9 { + margin-left: 75%; + } + .col-sm-offset-8 { + margin-left: 66.66666667%; + } + .col-sm-offset-7 { + margin-left: 58.33333333%; + } + .col-sm-offset-6 { + margin-left: 50%; + } + .col-sm-offset-5 { + margin-left: 41.66666667%; + } + .col-sm-offset-4 { + margin-left: 33.33333333%; + } + .col-sm-offset-3 { + margin-left: 25%; + } + .col-sm-offset-2 { + margin-left: 16.66666667%; + } + .col-sm-offset-1 { + margin-left: 8.33333333%; + } + .col-sm-offset-0 { + margin-left: 0; + } +} +@media (min-width: 992px) { + .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 { + float: left; + } + .col-md-12 { + width: 100%; + } + .col-md-11 { + width: 91.66666667%; + } + .col-md-10 { + width: 83.33333333%; + } + .col-md-9 { + width: 75%; + } + .col-md-8 { + width: 66.66666667%; + } + .col-md-7 { + width: 58.33333333%; + } + .col-md-6 { + width: 50%; + } + .col-md-5 { + width: 41.66666667%; + } + .col-md-4 { + width: 33.33333333%; + } + .col-md-3 { + width: 25%; + } + .col-md-2 { + width: 16.66666667%; + } + .col-md-1 { + width: 8.33333333%; + } + .col-md-pull-12 { + right: 100%; + } + .col-md-pull-11 { + right: 91.66666667%; + } + .col-md-pull-10 { + right: 83.33333333%; + } + .col-md-pull-9 { + right: 75%; + } + .col-md-pull-8 { + right: 66.66666667%; + } + .col-md-pull-7 { + right: 58.33333333%; + } + .col-md-pull-6 { + right: 50%; + } + .col-md-pull-5 { + right: 41.66666667%; + } + .col-md-pull-4 { + right: 33.33333333%; + } + .col-md-pull-3 { + right: 25%; + } + .col-md-pull-2 { + right: 16.66666667%; + } + .col-md-pull-1 { + right: 8.33333333%; + } + .col-md-pull-0 { + right: auto; + } + .col-md-push-12 { + left: 100%; + } + .col-md-push-11 { + left: 91.66666667%; + } + .col-md-push-10 { + left: 83.33333333%; + } + .col-md-push-9 { + left: 75%; + } + .col-md-push-8 { + left: 66.66666667%; + } + .col-md-push-7 { + left: 58.33333333%; + } + .col-md-push-6 { + left: 50%; + } + .col-md-push-5 { + left: 41.66666667%; + } + .col-md-push-4 { + left: 33.33333333%; + } + .col-md-push-3 { + left: 25%; + } + .col-md-push-2 { + left: 16.66666667%; + } + .col-md-push-1 { + left: 8.33333333%; + } + .col-md-push-0 { + left: auto; + } + .col-md-offset-12 { + margin-left: 100%; + } + .col-md-offset-11 { + margin-left: 91.66666667%; + } + .col-md-offset-10 { + margin-left: 83.33333333%; + } + .col-md-offset-9 { + margin-left: 75%; + } + .col-md-offset-8 { + margin-left: 66.66666667%; + } + .col-md-offset-7 { + margin-left: 58.33333333%; + } + .col-md-offset-6 { + margin-left: 50%; + } + .col-md-offset-5 { + margin-left: 41.66666667%; + } + .col-md-offset-4 { + margin-left: 33.33333333%; + } + .col-md-offset-3 { + margin-left: 25%; + } + .col-md-offset-2 { + margin-left: 16.66666667%; + } + .col-md-offset-1 { + margin-left: 8.33333333%; + } + .col-md-offset-0 { + margin-left: 0; + } +} +@media (min-width: 1200px) { + .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 { + float: left; + } + .col-lg-12 { + width: 100%; + } + .col-lg-11 { + width: 91.66666667%; + } + .col-lg-10 { + width: 83.33333333%; + } + .col-lg-9 { + width: 75%; + } + .col-lg-8 { + width: 66.66666667%; + } + .col-lg-7 { + width: 58.33333333%; + } + .col-lg-6 { + width: 50%; + } + .col-lg-5 { + width: 41.66666667%; + } + .col-lg-4 { + width: 33.33333333%; + } + .col-lg-3 { + width: 25%; + } + .col-lg-2 { + width: 16.66666667%; + } + .col-lg-1 { + width: 8.33333333%; + } + .col-lg-pull-12 { + right: 100%; + } + .col-lg-pull-11 { + right: 91.66666667%; + } + .col-lg-pull-10 { + right: 83.33333333%; + } + .col-lg-pull-9 { + right: 75%; + } + .col-lg-pull-8 { + right: 66.66666667%; + } + .col-lg-pull-7 { + right: 58.33333333%; + } + .col-lg-pull-6 { + right: 50%; + } + .col-lg-pull-5 { + right: 41.66666667%; + } + .col-lg-pull-4 { + right: 33.33333333%; + } + .col-lg-pull-3 { + right: 25%; + } + .col-lg-pull-2 { + right: 16.66666667%; + } + .col-lg-pull-1 { + right: 8.33333333%; + } + .col-lg-pull-0 { + right: auto; + } + .col-lg-push-12 { + left: 100%; + } + .col-lg-push-11 { + left: 91.66666667%; + } + .col-lg-push-10 { + left: 83.33333333%; + } + .col-lg-push-9 { + left: 75%; + } + .col-lg-push-8 { + left: 66.66666667%; + } + .col-lg-push-7 { + left: 58.33333333%; + } + .col-lg-push-6 { + left: 50%; + } + .col-lg-push-5 { + left: 41.66666667%; + } + .col-lg-push-4 { + left: 33.33333333%; + } + .col-lg-push-3 { + left: 25%; + } + .col-lg-push-2 { + left: 16.66666667%; + } + .col-lg-push-1 { + left: 8.33333333%; + } + .col-lg-push-0 { + left: auto; + } + .col-lg-offset-12 { + margin-left: 100%; + } + .col-lg-offset-11 { + margin-left: 91.66666667%; + } + .col-lg-offset-10 { + margin-left: 83.33333333%; + } + .col-lg-offset-9 { + margin-left: 75%; + } + .col-lg-offset-8 { + margin-left: 66.66666667%; + } + .col-lg-offset-7 { + margin-left: 58.33333333%; + } + .col-lg-offset-6 { + margin-left: 50%; + } + .col-lg-offset-5 { + margin-left: 41.66666667%; + } + .col-lg-offset-4 { + margin-left: 33.33333333%; + } + .col-lg-offset-3 { + margin-left: 25%; + } + .col-lg-offset-2 { + margin-left: 16.66666667%; + } + .col-lg-offset-1 { + margin-left: 8.33333333%; + } + .col-lg-offset-0 { + margin-left: 0; + } +} +table { + background-color: transparent; +} +caption { + padding-top: 8px; + padding-bottom: 8px; + color: #777; + text-align: left; +} +th { + text-align: left; +} +.table { + width: 100%; + max-width: 100%; + margin-bottom: 20px; +} +.table > thead > tr > th, +.table > tbody > tr > th, +.table > tfoot > tr > th, +.table > thead > tr > td, +.table > tbody > tr > td, +.table > tfoot > tr > td { + padding: 8px; + line-height: 1.42857143; + vertical-align: top; + border-top: 1px solid #ddd; +} +.table > thead > tr > th { + vertical-align: bottom; + border-bottom: 2px solid #ddd; +} +.table > caption + thead > tr:first-child > th, +.table > colgroup + thead > tr:first-child > th, +.table > thead:first-child > tr:first-child > th, +.table > caption + thead > tr:first-child > td, +.table > colgroup + thead > tr:first-child > td, +.table > thead:first-child > tr:first-child > td { + border-top: 0; +} +.table > tbody + tbody { + border-top: 2px solid #ddd; +} +.table .table { + background-color: #fff; +} +.table-condensed > thead > tr > th, +.table-condensed > tbody > tr > th, +.table-condensed > tfoot > tr > th, +.table-condensed > thead > tr > td, +.table-condensed > tbody > tr > td, +.table-condensed > tfoot > tr > td { + padding: 5px; +} +.table-bordered { + border: 1px solid #ddd; +} +.table-bordered > thead > tr > th, +.table-bordered > tbody > tr > th, +.table-bordered > tfoot > tr > th, +.table-bordered > thead > tr > td, +.table-bordered > tbody > tr > td, +.table-bordered > tfoot > tr > td { + border: 1px solid #ddd; +} +.table-bordered > thead > tr > th, +.table-bordered > thead > tr > td { + border-bottom-width: 2px; +} +.table-striped > tbody > tr:nth-child(odd) { + background-color: #f9f9f9; +} +.table-hover > tbody > tr:hover { + background-color: #f5f5f5; +} +table col[class*="col-"] { + position: static; + display: table-column; + float: none; +} +table td[class*="col-"], +table th[class*="col-"] { + position: static; + display: table-cell; + float: none; +} +.table > thead > tr > td.active, +.table > tbody > tr > td.active, +.table > tfoot > tr > td.active, +.table > thead > tr > th.active, +.table > tbody > tr > th.active, +.table > tfoot > tr > th.active, +.table > thead > tr.active > td, +.table > tbody > tr.active > td, +.table > tfoot > tr.active > td, +.table > thead > tr.active > th, +.table > tbody > tr.active > th, +.table > tfoot > tr.active > th { + background-color: #f5f5f5; +} +.table-hover > tbody > tr > td.active:hover, +.table-hover > tbody > tr > th.active:hover, +.table-hover > tbody > tr.active:hover > td, +.table-hover > tbody > tr:hover > .active, +.table-hover > tbody > tr.active:hover > th { + background-color: #e8e8e8; +} +.table > thead > tr > td.success, +.table > tbody > tr > td.success, +.table > tfoot > tr > td.success, +.table > thead > tr > th.success, +.table > tbody > tr > th.success, +.table > tfoot > tr > th.success, +.table > thead > tr.success > td, +.table > tbody > tr.success > td, +.table > tfoot > tr.success > td, +.table > thead > tr.success > th, +.table > tbody > tr.success > th, +.table > tfoot > tr.success > th { + background-color: #dff0d8; +} +.table-hover > tbody > tr > td.success:hover, +.table-hover > tbody > tr > th.success:hover, +.table-hover > tbody > tr.success:hover > td, +.table-hover > tbody > tr:hover > .success, +.table-hover > tbody > tr.success:hover > th { + background-color: #d0e9c6; +} +.table > thead > tr > td.info, +.table > tbody > tr > td.info, +.table > tfoot > tr > td.info, +.table > thead > tr > th.info, +.table > tbody > tr > th.info, +.table > tfoot > tr > th.info, +.table > thead > tr.info > td, +.table > tbody > tr.info > td, +.table > tfoot > tr.info > td, +.table > thead > tr.info > th, +.table > tbody > tr.info > th, +.table > tfoot > tr.info > th { + background-color: #d9edf7; +} +.table-hover > tbody > tr > td.info:hover, +.table-hover > tbody > tr > th.info:hover, +.table-hover > tbody > tr.info:hover > td, +.table-hover > tbody > tr:hover > .info, +.table-hover > tbody > tr.info:hover > th { + background-color: #c4e3f3; +} +.table > thead > tr > td.warning, +.table > tbody > tr > td.warning, +.table > tfoot > tr > td.warning, +.table > thead > tr > th.warning, +.table > tbody > tr > th.warning, +.table > tfoot > tr > th.warning, +.table > thead > tr.warning > td, +.table > tbody > tr.warning > td, +.table > tfoot > tr.warning > td, +.table > thead > tr.warning > th, +.table > tbody > tr.warning > th, +.table > tfoot > tr.warning > th { + background-color: #fcf8e3; +} +.table-hover > tbody > tr > td.warning:hover, +.table-hover > tbody > tr > th.warning:hover, +.table-hover > tbody > tr.warning:hover > td, +.table-hover > tbody > tr:hover > .warning, +.table-hover > tbody > tr.warning:hover > th { + background-color: #faf2cc; +} +.table > thead > tr > td.danger, +.table > tbody > tr > td.danger, +.table > tfoot > tr > td.danger, +.table > thead > tr > th.danger, +.table > tbody > tr > th.danger, +.table > tfoot > tr > th.danger, +.table > thead > tr.danger > td, +.table > tbody > tr.danger > td, +.table > tfoot > tr.danger > td, +.table > thead > tr.danger > th, +.table > tbody > tr.danger > th, +.table > tfoot > tr.danger > th { + background-color: #f2dede; +} +.table-hover > tbody > tr > td.danger:hover, +.table-hover > tbody > tr > th.danger:hover, +.table-hover > tbody > tr.danger:hover > td, +.table-hover > tbody > tr:hover > .danger, +.table-hover > tbody > tr.danger:hover > th { + background-color: #ebcccc; +} +.table-responsive { + min-height: .01%; + overflow-x: auto; +} +@media screen and (max-width: 767px) { + .table-responsive { + width: 100%; + margin-bottom: 15px; + overflow-y: hidden; + -ms-overflow-style: -ms-autohiding-scrollbar; + border: 1px solid #ddd; + } + .table-responsive > .table { + margin-bottom: 0; + } + .table-responsive > .table > thead > tr > th, + .table-responsive > .table > tbody > tr > th, + .table-responsive > .table > tfoot > tr > th, + .table-responsive > .table > thead > tr > td, + .table-responsive > .table > tbody > tr > td, + .table-responsive > .table > tfoot > tr > td { + white-space: nowrap; + } + .table-responsive > .table-bordered { + border: 0; + } + .table-responsive > .table-bordered > thead > tr > th:first-child, + .table-responsive > .table-bordered > tbody > tr > th:first-child, + .table-responsive > .table-bordered > tfoot > tr > th:first-child, + .table-responsive > .table-bordered > thead > tr > td:first-child, + .table-responsive > .table-bordered > tbody > tr > td:first-child, + .table-responsive > .table-bordered > tfoot > tr > td:first-child { + border-left: 0; + } + .table-responsive > .table-bordered > thead > tr > th:last-child, + .table-responsive > .table-bordered > tbody > tr > th:last-child, + .table-responsive > .table-bordered > tfoot > tr > th:last-child, + .table-responsive > .table-bordered > thead > tr > td:last-child, + .table-responsive > .table-bordered > tbody > tr > td:last-child, + .table-responsive > .table-bordered > tfoot > tr > td:last-child { + border-right: 0; + } + .table-responsive > .table-bordered > tbody > tr:last-child > th, + .table-responsive > .table-bordered > tfoot > tr:last-child > th, + .table-responsive > .table-bordered > tbody > tr:last-child > td, + .table-responsive > .table-bordered > tfoot > tr:last-child > td { + border-bottom: 0; + } +} +fieldset { + min-width: 0; + padding: 0; + margin: 0; + border: 0; +} +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 20px; + font-size: 21px; + line-height: inherit; + color: #333; + border: 0; + border-bottom: 1px solid #e5e5e5; +} +label { + display: inline-block; + max-width: 100%; + margin-bottom: 5px; + font-weight: bold; +} +input[type="search"] { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +input[type="radio"], +input[type="checkbox"] { + margin: 4px 0 0; + margin-top: 1px \9; + line-height: normal; +} +input[type="file"] { + display: block; +} +input[type="range"] { + display: block; + width: 100%; +} +select[multiple], +select[size] { + height: auto; +} +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +output { + display: block; + padding-top: 7px; + font-size: 14px; + line-height: 1.42857143; + color: #555; +} +.form-control { + display: block; + width: 100%; + height: 34px; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857143; + color: #555; + background-color: #fff; + background-image: none; + border: 1px solid #ccc; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; + -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; +} +.form-control:focus { + border-color: #66afe9; + outline: 0; + -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6); + box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6); +} +.form-control::-moz-placeholder { + color: #999; + opacity: 1; +} +.form-control:-ms-input-placeholder { + color: #999; +} +.form-control::-webkit-input-placeholder { + color: #999; +} +.form-control[disabled], +.form-control[readonly], +fieldset[disabled] .form-control { + cursor: not-allowed; + background-color: #eee; + opacity: 1; +} +textarea.form-control { + height: auto; +} +input[type="search"] { + -webkit-appearance: none; +} +input[type="date"], +input[type="time"], +input[type="datetime-local"], +input[type="month"] { + line-height: 34px; + line-height: 1.42857143 \0; +} +input[type="date"].input-sm, +input[type="time"].input-sm, +input[type="datetime-local"].input-sm, +input[type="month"].input-sm { + line-height: 30px; + line-height: 1.5 \0; +} +input[type="date"].input-lg, +input[type="time"].input-lg, +input[type="datetime-local"].input-lg, +input[type="month"].input-lg { + line-height: 46px; + line-height: 1.33 \0; +} +_:-ms-fullscreen, +:root input[type="date"], +_:-ms-fullscreen, +:root input[type="time"], +_:-ms-fullscreen, +:root input[type="datetime-local"], +_:-ms-fullscreen, +:root input[type="month"] { + line-height: 1.42857143; +} +_:-ms-fullscreen.input-sm, +:root input[type="date"].input-sm, +_:-ms-fullscreen.input-sm, +:root input[type="time"].input-sm, +_:-ms-fullscreen.input-sm, +:root input[type="datetime-local"].input-sm, +_:-ms-fullscreen.input-sm, +:root input[type="month"].input-sm { + line-height: 1.5; +} +_:-ms-fullscreen.input-lg, +:root input[type="date"].input-lg, +_:-ms-fullscreen.input-lg, +:root input[type="time"].input-lg, +_:-ms-fullscreen.input-lg, +:root input[type="datetime-local"].input-lg, +_:-ms-fullscreen.input-lg, +:root input[type="month"].input-lg { + line-height: 1.33; +} +.form-group { + margin-bottom: 15px; +} +.radio, +.checkbox { + position: relative; + display: block; + margin-top: 10px; + margin-bottom: 10px; +} +.radio label, +.checkbox label { + min-height: 20px; + padding-left: 20px; + margin-bottom: 0; + font-weight: normal; + cursor: pointer; +} +.radio input[type="radio"], +.radio-inline input[type="radio"], +.checkbox input[type="checkbox"], +.checkbox-inline input[type="checkbox"] { + position: absolute; + margin-top: 4px \9; + margin-left: -20px; +} +.radio + .radio, +.checkbox + .checkbox { + margin-top: -5px; +} +.radio-inline, +.checkbox-inline { + display: inline-block; + padding-left: 20px; + margin-bottom: 0; + font-weight: normal; + vertical-align: middle; + cursor: pointer; +} +.radio-inline + .radio-inline, +.checkbox-inline + .checkbox-inline { + margin-top: 0; + margin-left: 10px; +} +input[type="radio"][disabled], +input[type="checkbox"][disabled], +input[type="radio"].disabled, +input[type="checkbox"].disabled, +fieldset[disabled] input[type="radio"], +fieldset[disabled] input[type="checkbox"] { + cursor: not-allowed; +} +.radio-inline.disabled, +.checkbox-inline.disabled, +fieldset[disabled] .radio-inline, +fieldset[disabled] .checkbox-inline { + cursor: not-allowed; +} +.radio.disabled label, +.checkbox.disabled label, +fieldset[disabled] .radio label, +fieldset[disabled] .checkbox label { + cursor: not-allowed; +} +.form-control-static { + padding-top: 7px; + padding-bottom: 7px; + margin-bottom: 0; +} +.form-control-static.input-lg, +.form-control-static.input-sm { + padding-right: 0; + padding-left: 0; +} +.input-sm, +.form-group-sm .form-control { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +select.input-sm, +select.form-group-sm .form-control { + height: 30px; + line-height: 30px; +} +textarea.input-sm, +textarea.form-group-sm .form-control, +select[multiple].input-sm, +select[multiple].form-group-sm .form-control { + height: auto; +} +.input-lg, +.form-group-lg .form-control { + height: 46px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.33; + border-radius: 6px; +} +select.input-lg, +select.form-group-lg .form-control { + height: 46px; + line-height: 46px; +} +textarea.input-lg, +textarea.form-group-lg .form-control, +select[multiple].input-lg, +select[multiple].form-group-lg .form-control { + height: auto; +} +.has-feedback { + position: relative; +} +.has-feedback .form-control { + padding-right: 42.5px; +} +.form-control-feedback { + position: absolute; + top: 0; + right: 0; + z-index: 2; + display: block; + width: 34px; + height: 34px; + line-height: 34px; + text-align: center; + pointer-events: none; +} +.input-lg + .form-control-feedback { + width: 46px; + height: 46px; + line-height: 46px; +} +.input-sm + .form-control-feedback { + width: 30px; + height: 30px; + line-height: 30px; +} +.has-success .help-block, +.has-success .control-label, +.has-success .radio, +.has-success .checkbox, +.has-success .radio-inline, +.has-success .checkbox-inline, +.has-success.radio label, +.has-success.checkbox label, +.has-success.radio-inline label, +.has-success.checkbox-inline label { + color: #3c763d; +} +.has-success .form-control { + border-color: #3c763d; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); +} +.has-success .form-control:focus { + border-color: #2b542c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168; +} +.has-success .input-group-addon { + color: #3c763d; + background-color: #dff0d8; + border-color: #3c763d; +} +.has-success .form-control-feedback { + color: #3c763d; +} +.has-warning .help-block, +.has-warning .control-label, +.has-warning .radio, +.has-warning .checkbox, +.has-warning .radio-inline, +.has-warning .checkbox-inline, +.has-warning.radio label, +.has-warning.checkbox label, +.has-warning.radio-inline label, +.has-warning.checkbox-inline label { + color: #8a6d3b; +} +.has-warning .form-control { + border-color: #8a6d3b; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); +} +.has-warning .form-control:focus { + border-color: #66512c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b; +} +.has-warning .input-group-addon { + color: #8a6d3b; + background-color: #fcf8e3; + border-color: #8a6d3b; +} +.has-warning .form-control-feedback { + color: #8a6d3b; +} +.has-error .help-block, +.has-error .control-label, +.has-error .radio, +.has-error .checkbox, +.has-error .radio-inline, +.has-error .checkbox-inline, +.has-error.radio label, +.has-error.checkbox label, +.has-error.radio-inline label, +.has-error.checkbox-inline label { + color: #a94442; +} +.has-error .form-control { + border-color: #a94442; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); +} +.has-error .form-control:focus { + border-color: #843534; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483; +} +.has-error .input-group-addon { + color: #a94442; + background-color: #f2dede; + border-color: #a94442; +} +.has-error .form-control-feedback { + color: #a94442; +} +.has-feedback label ~ .form-control-feedback { + top: 25px; +} +.has-feedback label.sr-only ~ .form-control-feedback { + top: 0; +} +.help-block { + display: block; + margin-top: 5px; + margin-bottom: 10px; + color: #737373; +} +@media (min-width: 768px) { + .form-inline .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .form-control { + display: inline-block; + width: auto; + vertical-align: middle; + } + .form-inline .form-control-static { + display: inline-block; + } + .form-inline .input-group { + display: inline-table; + vertical-align: middle; + } + .form-inline .input-group .input-group-addon, + .form-inline .input-group .input-group-btn, + .form-inline .input-group .form-control { + width: auto; + } + .form-inline .input-group > .form-control { + width: 100%; + } + .form-inline .control-label { + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .radio, + .form-inline .checkbox { + display: inline-block; + margin-top: 0; + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .radio label, + .form-inline .checkbox label { + padding-left: 0; + } + .form-inline .radio input[type="radio"], + .form-inline .checkbox input[type="checkbox"] { + position: relative; + margin-left: 0; + } + .form-inline .has-feedback .form-control-feedback { + top: 0; + } +} +.form-horizontal .radio, +.form-horizontal .checkbox, +.form-horizontal .radio-inline, +.form-horizontal .checkbox-inline { + padding-top: 7px; + margin-top: 0; + margin-bottom: 0; +} +.form-horizontal .radio, +.form-horizontal .checkbox { + min-height: 27px; +} +.form-horizontal .form-group { + margin-right: -15px; + margin-left: -15px; +} +@media (min-width: 768px) { + .form-horizontal .control-label { + padding-top: 7px; + margin-bottom: 0; + text-align: right; + } +} +.form-horizontal .has-feedback .form-control-feedback { + right: 15px; +} +@media (min-width: 768px) { + .form-horizontal .form-group-lg .control-label { + padding-top: 14.3px; + } +} +@media (min-width: 768px) { + .form-horizontal .form-group-sm .control-label { + padding-top: 6px; + } +} +.btn { + display: inline-block; + padding: 6px 12px; + margin-bottom: 0; + font-size: 14px; + font-weight: normal; + line-height: 1.42857143; + text-align: center; + white-space: nowrap; + vertical-align: middle; + -ms-touch-action: manipulation; + touch-action: manipulation; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-image: none; + border: 1px solid transparent; + border-radius: 4px; +} +.btn:focus, +.btn:active:focus, +.btn.active:focus, +.btn.focus, +.btn:active.focus, +.btn.active.focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +.btn:hover, +.btn:focus, +.btn.focus { + color: #333; + text-decoration: none; +} +.btn:active, +.btn.active { + background-image: none; + outline: 0; + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); +} +.btn.disabled, +.btn[disabled], +fieldset[disabled] .btn { + pointer-events: none; + cursor: not-allowed; + filter: alpha(opacity=65); + -webkit-box-shadow: none; + box-shadow: none; + opacity: .65; +} +.btn-default { + color: #333; + background-color: #fff; + border-color: #ccc; +} +.btn-default:hover, +.btn-default:focus, +.btn-default.focus, +.btn-default:active, +.btn-default.active, +.open > .dropdown-toggle.btn-default { + color: #333; + background-color: #e6e6e6; + border-color: #adadad; +} +.btn-default:active, +.btn-default.active, +.open > .dropdown-toggle.btn-default { + background-image: none; +} +.btn-default.disabled, +.btn-default[disabled], +fieldset[disabled] .btn-default, +.btn-default.disabled:hover, +.btn-default[disabled]:hover, +fieldset[disabled] .btn-default:hover, +.btn-default.disabled:focus, +.btn-default[disabled]:focus, +fieldset[disabled] .btn-default:focus, +.btn-default.disabled.focus, +.btn-default[disabled].focus, +fieldset[disabled] .btn-default.focus, +.btn-default.disabled:active, +.btn-default[disabled]:active, +fieldset[disabled] .btn-default:active, +.btn-default.disabled.active, +.btn-default[disabled].active, +fieldset[disabled] .btn-default.active { + background-color: #fff; + border-color: #ccc; +} +.btn-default .badge { + color: #fff; + background-color: #333; +} +.btn-primary { + color: #fff; + background-color: #428bca; + border-color: #357ebd; +} +.btn-primary:hover, +.btn-primary:focus, +.btn-primary.focus, +.btn-primary:active, +.btn-primary.active, +.open > .dropdown-toggle.btn-primary { + color: #fff; + background-color: #3071a9; + border-color: #285e8e; +} +.btn-primary:active, +.btn-primary.active, +.open > .dropdown-toggle.btn-primary { + background-image: none; +} +.btn-primary.disabled, +.btn-primary[disabled], +fieldset[disabled] .btn-primary, +.btn-primary.disabled:hover, +.btn-primary[disabled]:hover, +fieldset[disabled] .btn-primary:hover, +.btn-primary.disabled:focus, +.btn-primary[disabled]:focus, +fieldset[disabled] .btn-primary:focus, +.btn-primary.disabled.focus, +.btn-primary[disabled].focus, +fieldset[disabled] .btn-primary.focus, +.btn-primary.disabled:active, +.btn-primary[disabled]:active, +fieldset[disabled] .btn-primary:active, +.btn-primary.disabled.active, +.btn-primary[disabled].active, +fieldset[disabled] .btn-primary.active { + background-color: #428bca; + border-color: #357ebd; +} +.btn-primary .badge { + color: #428bca; + background-color: #fff; +} +.btn-success { + color: #fff; + background-color: #5cb85c; + border-color: #4cae4c; +} +.btn-success:hover, +.btn-success:focus, +.btn-success.focus, +.btn-success:active, +.btn-success.active, +.open > .dropdown-toggle.btn-success { + color: #fff; + background-color: #449d44; + border-color: #398439; +} +.btn-success:active, +.btn-success.active, +.open > .dropdown-toggle.btn-success { + background-image: none; +} +.btn-success.disabled, +.btn-success[disabled], +fieldset[disabled] .btn-success, +.btn-success.disabled:hover, +.btn-success[disabled]:hover, +fieldset[disabled] .btn-success:hover, +.btn-success.disabled:focus, +.btn-success[disabled]:focus, +fieldset[disabled] .btn-success:focus, +.btn-success.disabled.focus, +.btn-success[disabled].focus, +fieldset[disabled] .btn-success.focus, +.btn-success.disabled:active, +.btn-success[disabled]:active, +fieldset[disabled] .btn-success:active, +.btn-success.disabled.active, +.btn-success[disabled].active, +fieldset[disabled] .btn-success.active { + background-color: #5cb85c; + border-color: #4cae4c; +} +.btn-success .badge { + color: #5cb85c; + background-color: #fff; +} +.btn-info { + color: #fff; + background-color: #5bc0de; + border-color: #46b8da; +} +.btn-info:hover, +.btn-info:focus, +.btn-info.focus, +.btn-info:active, +.btn-info.active, +.open > .dropdown-toggle.btn-info { + color: #fff; + background-color: #31b0d5; + border-color: #269abc; +} +.btn-info:active, +.btn-info.active, +.open > .dropdown-toggle.btn-info { + background-image: none; +} +.btn-info.disabled, +.btn-info[disabled], +fieldset[disabled] .btn-info, +.btn-info.disabled:hover, +.btn-info[disabled]:hover, +fieldset[disabled] .btn-info:hover, +.btn-info.disabled:focus, +.btn-info[disabled]:focus, +fieldset[disabled] .btn-info:focus, +.btn-info.disabled.focus, +.btn-info[disabled].focus, +fieldset[disabled] .btn-info.focus, +.btn-info.disabled:active, +.btn-info[disabled]:active, +fieldset[disabled] .btn-info:active, +.btn-info.disabled.active, +.btn-info[disabled].active, +fieldset[disabled] .btn-info.active { + background-color: #5bc0de; + border-color: #46b8da; +} +.btn-info .badge { + color: #5bc0de; + background-color: #fff; +} +.btn-warning { + color: #fff; + background-color: #f0ad4e; + border-color: #eea236; +} +.btn-warning:hover, +.btn-warning:focus, +.btn-warning.focus, +.btn-warning:active, +.btn-warning.active, +.open > .dropdown-toggle.btn-warning { + color: #fff; + background-color: #ec971f; + border-color: #d58512; +} +.btn-warning:active, +.btn-warning.active, +.open > .dropdown-toggle.btn-warning { + background-image: none; +} +.btn-warning.disabled, +.btn-warning[disabled], +fieldset[disabled] .btn-warning, +.btn-warning.disabled:hover, +.btn-warning[disabled]:hover, +fieldset[disabled] .btn-warning:hover, +.btn-warning.disabled:focus, +.btn-warning[disabled]:focus, +fieldset[disabled] .btn-warning:focus, +.btn-warning.disabled.focus, +.btn-warning[disabled].focus, +fieldset[disabled] .btn-warning.focus, +.btn-warning.disabled:active, +.btn-warning[disabled]:active, +fieldset[disabled] .btn-warning:active, +.btn-warning.disabled.active, +.btn-warning[disabled].active, +fieldset[disabled] .btn-warning.active { + background-color: #f0ad4e; + border-color: #eea236; +} +.btn-warning .badge { + color: #f0ad4e; + background-color: #fff; +} +.btn-danger { + color: #fff; + background-color: #d9534f; + border-color: #d43f3a; +} +.btn-danger:hover, +.btn-danger:focus, +.btn-danger.focus, +.btn-danger:active, +.btn-danger.active, +.open > .dropdown-toggle.btn-danger { + color: #fff; + background-color: #c9302c; + border-color: #ac2925; +} +.btn-danger:active, +.btn-danger.active, +.open > .dropdown-toggle.btn-danger { + background-image: none; +} +.btn-danger.disabled, +.btn-danger[disabled], +fieldset[disabled] .btn-danger, +.btn-danger.disabled:hover, +.btn-danger[disabled]:hover, +fieldset[disabled] .btn-danger:hover, +.btn-danger.disabled:focus, +.btn-danger[disabled]:focus, +fieldset[disabled] .btn-danger:focus, +.btn-danger.disabled.focus, +.btn-danger[disabled].focus, +fieldset[disabled] .btn-danger.focus, +.btn-danger.disabled:active, +.btn-danger[disabled]:active, +fieldset[disabled] .btn-danger:active, +.btn-danger.disabled.active, +.btn-danger[disabled].active, +fieldset[disabled] .btn-danger.active { + background-color: #d9534f; + border-color: #d43f3a; +} +.btn-danger .badge { + color: #d9534f; + background-color: #fff; +} +.btn-link { + font-weight: normal; + color: #428bca; + border-radius: 0; +} +.btn-link, +.btn-link:active, +.btn-link.active, +.btn-link[disabled], +fieldset[disabled] .btn-link { + background-color: transparent; + -webkit-box-shadow: none; + box-shadow: none; +} +.btn-link, +.btn-link:hover, +.btn-link:focus, +.btn-link:active { + border-color: transparent; +} +.btn-link:hover, +.btn-link:focus { + color: #2a6496; + text-decoration: underline; + background-color: transparent; +} +.btn-link[disabled]:hover, +fieldset[disabled] .btn-link:hover, +.btn-link[disabled]:focus, +fieldset[disabled] .btn-link:focus { + color: #777; + text-decoration: none; +} +.btn-lg, +.btn-group-lg > .btn { + padding: 10px 16px; + font-size: 18px; + line-height: 1.33; + border-radius: 6px; +} +.btn-sm, +.btn-group-sm > .btn { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +.btn-xs, +.btn-group-xs > .btn { + padding: 1px 5px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +.btn-block { + display: block; + width: 100%; +} +.btn-block + .btn-block { + margin-top: 5px; +} +input[type="submit"].btn-block, +input[type="reset"].btn-block, +input[type="button"].btn-block { + width: 100%; +} +.fade { + opacity: 0; + -webkit-transition: opacity .15s linear; + -o-transition: opacity .15s linear; + transition: opacity .15s linear; +} +.fade.in { + opacity: 1; +} +.collapse { + display: none; + visibility: hidden; +} +.collapse.in { + display: block; + visibility: visible; +} +tr.collapse.in { + display: table-row; +} +tbody.collapse.in { + display: table-row-group; +} +.collapsing { + position: relative; + height: 0; + overflow: hidden; + -webkit-transition-timing-function: ease; + -o-transition-timing-function: ease; + transition-timing-function: ease; + -webkit-transition-duration: .35s; + -o-transition-duration: .35s; + transition-duration: .35s; + -webkit-transition-property: height, visibility; + -o-transition-property: height, visibility; + transition-property: height, visibility; +} +.caret { + display: inline-block; + width: 0; + height: 0; + margin-left: 2px; + vertical-align: middle; + border-top: 4px solid; + border-right: 4px solid transparent; + border-left: 4px solid transparent; +} +.dropdown { + position: relative; +} +.dropdown-toggle:focus { + outline: 0; +} +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + font-size: 14px; + text-align: left; + list-style: none; + background-color: #fff; + -webkit-background-clip: padding-box; + background-clip: padding-box; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, .15); + border-radius: 4px; + -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175); + box-shadow: 0 6px 12px rgba(0, 0, 0, .175); +} +.dropdown-menu.pull-right { + right: 0; + left: auto; +} +.dropdown-menu .divider { + height: 1px; + margin: 9px 0; + overflow: hidden; + background-color: #e5e5e5; +} +.dropdown-menu > li > a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 1.42857143; + color: #333; + white-space: nowrap; +} +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus { + color: #262626; + text-decoration: none; + background-color: #f5f5f5; +} +.dropdown-menu > .active > a, +.dropdown-menu > .active > a:hover, +.dropdown-menu > .active > a:focus { + color: #fff; + text-decoration: none; + background-color: #428bca; + outline: 0; +} +.dropdown-menu > .disabled > a, +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + color: #777; +} +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + text-decoration: none; + cursor: not-allowed; + background-color: transparent; + background-image: none; + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); +} +.open > .dropdown-menu { + display: block; +} +.open > a { + outline: 0; +} +.dropdown-menu-right { + right: 0; + left: auto; +} +.dropdown-menu-left { + right: auto; + left: 0; +} +.dropdown-header { + display: block; + padding: 3px 20px; + font-size: 12px; + line-height: 1.42857143; + color: #777; + white-space: nowrap; +} +.dropdown-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 990; +} +.pull-right > .dropdown-menu { + right: 0; + left: auto; +} +.dropup .caret, +.navbar-fixed-bottom .dropdown .caret { + content: ""; + border-top: 0; + border-bottom: 4px solid; +} +.dropup .dropdown-menu, +.navbar-fixed-bottom .dropdown .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 1px; +} +@media (min-width: 768px) { + .navbar-right .dropdown-menu { + right: 0; + left: auto; + } + .navbar-right .dropdown-menu-left { + right: auto; + left: 0; + } +} +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-block; + vertical-align: middle; +} +.btn-group > .btn, +.btn-group-vertical > .btn { + position: relative; + float: left; +} +.btn-group > .btn:hover, +.btn-group-vertical > .btn:hover, +.btn-group > .btn:focus, +.btn-group-vertical > .btn:focus, +.btn-group > .btn:active, +.btn-group-vertical > .btn:active, +.btn-group > .btn.active, +.btn-group-vertical > .btn.active { + z-index: 2; +} +.btn-group > .btn:focus, +.btn-group-vertical > .btn:focus { + outline: 0; +} +.btn-group .btn + .btn, +.btn-group .btn + .btn-group, +.btn-group .btn-group + .btn, +.btn-group .btn-group + .btn-group { + margin-left: -1px; +} +.btn-toolbar { + margin-left: -5px; +} +.btn-toolbar .btn-group, +.btn-toolbar .input-group { + float: left; +} +.btn-toolbar > .btn, +.btn-toolbar > .btn-group, +.btn-toolbar > .input-group { + margin-left: 5px; +} +.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { + border-radius: 0; +} +.btn-group > .btn:first-child { + margin-left: 0; +} +.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.btn-group > .btn:last-child:not(:first-child), +.btn-group > .dropdown-toggle:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group > .btn-group { + float: left; +} +.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group > .btn-group:first-child > .btn:last-child, +.btn-group > .btn-group:first-child > .dropdown-toggle { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.btn-group > .btn-group:last-child > .btn:first-child { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} +.btn-group > .btn + .dropdown-toggle { + padding-right: 8px; + padding-left: 8px; +} +.btn-group > .btn-lg + .dropdown-toggle { + padding-right: 12px; + padding-left: 12px; +} +.btn-group.open .dropdown-toggle { + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); +} +.btn-group.open .dropdown-toggle.btn-link { + -webkit-box-shadow: none; + box-shadow: none; +} +.btn .caret { + margin-left: 0; +} +.btn-lg .caret { + border-width: 5px 5px 0; + border-bottom-width: 0; +} +.dropup .btn-lg .caret { + border-width: 0 5px 5px; +} +.btn-group-vertical > .btn, +.btn-group-vertical > .btn-group, +.btn-group-vertical > .btn-group > .btn { + display: block; + float: none; + width: 100%; + max-width: 100%; +} +.btn-group-vertical > .btn-group > .btn { + float: none; +} +.btn-group-vertical > .btn + .btn, +.btn-group-vertical > .btn + .btn-group, +.btn-group-vertical > .btn-group + .btn, +.btn-group-vertical > .btn-group + .btn-group { + margin-top: -1px; + margin-left: 0; +} +.btn-group-vertical > .btn:not(:first-child):not(:last-child) { + border-radius: 0; +} +.btn-group-vertical > .btn:first-child:not(:last-child) { + border-top-right-radius: 4px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group-vertical > .btn:last-child:not(:first-child) { + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom-left-radius: 4px; +} +.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child, +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.btn-group-justified { + display: table; + width: 100%; + table-layout: fixed; + border-collapse: separate; +} +.btn-group-justified > .btn, +.btn-group-justified > .btn-group { + display: table-cell; + float: none; + width: 1%; +} +.btn-group-justified > .btn-group .btn { + width: 100%; +} +.btn-group-justified > .btn-group .dropdown-menu { + left: auto; +} +[data-toggle="buttons"] > .btn input[type="radio"], +[data-toggle="buttons"] > .btn-group > .btn input[type="radio"], +[data-toggle="buttons"] > .btn input[type="checkbox"], +[data-toggle="buttons"] > .btn-group > .btn input[type="checkbox"] { + position: absolute; + clip: rect(0, 0, 0, 0); + pointer-events: none; +} +.input-group { + position: relative; + display: table; + border-collapse: separate; +} +.input-group[class*="col-"] { + float: none; + padding-right: 0; + padding-left: 0; +} +.input-group .form-control { + position: relative; + z-index: 2; + float: left; + width: 100%; + margin-bottom: 0; +} +.input-group-lg > .form-control, +.input-group-lg > .input-group-addon, +.input-group-lg > .input-group-btn > .btn { + height: 46px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.33; + border-radius: 6px; +} +select.input-group-lg > .form-control, +select.input-group-lg > .input-group-addon, +select.input-group-lg > .input-group-btn > .btn { + height: 46px; + line-height: 46px; +} +textarea.input-group-lg > .form-control, +textarea.input-group-lg > .input-group-addon, +textarea.input-group-lg > .input-group-btn > .btn, +select[multiple].input-group-lg > .form-control, +select[multiple].input-group-lg > .input-group-addon, +select[multiple].input-group-lg > .input-group-btn > .btn { + height: auto; +} +.input-group-sm > .form-control, +.input-group-sm > .input-group-addon, +.input-group-sm > .input-group-btn > .btn { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +select.input-group-sm > .form-control, +select.input-group-sm > .input-group-addon, +select.input-group-sm > .input-group-btn > .btn { + height: 30px; + line-height: 30px; +} +textarea.input-group-sm > .form-control, +textarea.input-group-sm > .input-group-addon, +textarea.input-group-sm > .input-group-btn > .btn, +select[multiple].input-group-sm > .form-control, +select[multiple].input-group-sm > .input-group-addon, +select[multiple].input-group-sm > .input-group-btn > .btn { + height: auto; +} +.input-group-addon, +.input-group-btn, +.input-group .form-control { + display: table-cell; +} +.input-group-addon:not(:first-child):not(:last-child), +.input-group-btn:not(:first-child):not(:last-child), +.input-group .form-control:not(:first-child):not(:last-child) { + border-radius: 0; +} +.input-group-addon, +.input-group-btn { + width: 1%; + white-space: nowrap; + vertical-align: middle; +} +.input-group-addon { + padding: 6px 12px; + font-size: 14px; + font-weight: normal; + line-height: 1; + color: #555; + text-align: center; + background-color: #eee; + border: 1px solid #ccc; + border-radius: 4px; +} +.input-group-addon.input-sm { + padding: 5px 10px; + font-size: 12px; + border-radius: 3px; +} +.input-group-addon.input-lg { + padding: 10px 16px; + font-size: 18px; + border-radius: 6px; +} +.input-group-addon input[type="radio"], +.input-group-addon input[type="checkbox"] { + margin-top: 0; +} +.input-group .form-control:first-child, +.input-group-addon:first-child, +.input-group-btn:first-child > .btn, +.input-group-btn:first-child > .btn-group > .btn, +.input-group-btn:first-child > .dropdown-toggle, +.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), +.input-group-btn:last-child > .btn-group:not(:last-child) > .btn { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.input-group-addon:first-child { + border-right: 0; +} +.input-group .form-control:last-child, +.input-group-addon:last-child, +.input-group-btn:last-child > .btn, +.input-group-btn:last-child > .btn-group > .btn, +.input-group-btn:last-child > .dropdown-toggle, +.input-group-btn:first-child > .btn:not(:first-child), +.input-group-btn:first-child > .btn-group:not(:first-child) > .btn { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.input-group-addon:last-child { + border-left: 0; +} +.input-group-btn { + position: relative; + font-size: 0; + white-space: nowrap; +} +.input-group-btn > .btn { + position: relative; +} +.input-group-btn > .btn + .btn { + margin-left: -1px; +} +.input-group-btn > .btn:hover, +.input-group-btn > .btn:focus, +.input-group-btn > .btn:active { + z-index: 2; +} +.input-group-btn:first-child > .btn, +.input-group-btn:first-child > .btn-group { + margin-right: -1px; +} +.input-group-btn:last-child > .btn, +.input-group-btn:last-child > .btn-group { + margin-left: -1px; +} +.nav { + padding-left: 0; + margin-bottom: 0; + list-style: none; +} +.nav > li { + position: relative; + display: block; +} +.nav > li > a { + position: relative; + display: block; + padding: 10px 15px; +} +.nav > li > a:hover, +.nav > li > a:focus { + text-decoration: none; + background-color: #eee; +} +.nav > li.disabled > a { + color: #777; +} +.nav > li.disabled > a:hover, +.nav > li.disabled > a:focus { + color: #777; + text-decoration: none; + cursor: not-allowed; + background-color: transparent; +} +.nav .open > a, +.nav .open > a:hover, +.nav .open > a:focus { + background-color: #eee; + border-color: #428bca; +} +.nav .nav-divider { + height: 1px; + margin: 9px 0; + overflow: hidden; + background-color: #e5e5e5; +} +.nav > li > a > img { + max-width: none; +} +.nav-tabs { + border-bottom: 1px solid #ddd; +} +.nav-tabs > li { + float: left; + margin-bottom: -1px; +} +.nav-tabs > li > a { + margin-right: 2px; + line-height: 1.42857143; + border: 1px solid transparent; + border-radius: 4px 4px 0 0; +} +.nav-tabs > li > a:hover { + border-color: #eee #eee #ddd; +} +.nav-tabs > li.active > a, +.nav-tabs > li.active > a:hover, +.nav-tabs > li.active > a:focus { + color: #555; + cursor: default; + background-color: #fff; + border: 1px solid #ddd; + border-bottom-color: transparent; +} +.nav-tabs.nav-justified { + width: 100%; + border-bottom: 0; +} +.nav-tabs.nav-justified > li { + float: none; +} +.nav-tabs.nav-justified > li > a { + margin-bottom: 5px; + text-align: center; +} +.nav-tabs.nav-justified > .dropdown .dropdown-menu { + top: auto; + left: auto; +} +@media (min-width: 768px) { + .nav-tabs.nav-justified > li { + display: table-cell; + width: 1%; + } + .nav-tabs.nav-justified > li > a { + margin-bottom: 0; + } +} +.nav-tabs.nav-justified > li > a { + margin-right: 0; + border-radius: 4px; +} +.nav-tabs.nav-justified > .active > a, +.nav-tabs.nav-justified > .active > a:hover, +.nav-tabs.nav-justified > .active > a:focus { + border: 1px solid #ddd; +} +@media (min-width: 768px) { + .nav-tabs.nav-justified > li > a { + border-bottom: 1px solid #ddd; + border-radius: 4px 4px 0 0; + } + .nav-tabs.nav-justified > .active > a, + .nav-tabs.nav-justified > .active > a:hover, + .nav-tabs.nav-justified > .active > a:focus { + border-bottom-color: #fff; + } +} +.nav-pills > li { + float: left; +} +.nav-pills > li > a { + border-radius: 4px; +} +.nav-pills > li + li { + margin-left: 2px; +} +.nav-pills > li.active > a, +.nav-pills > li.active > a:hover, +.nav-pills > li.active > a:focus { + color: #fff; + background-color: #428bca; +} +.nav-stacked > li { + float: none; +} +.nav-stacked > li + li { + margin-top: 2px; + margin-left: 0; +} +.nav-justified { + width: 100%; +} +.nav-justified > li { + float: none; +} +.nav-justified > li > a { + margin-bottom: 5px; + text-align: center; +} +.nav-justified > .dropdown .dropdown-menu { + top: auto; + left: auto; +} +@media (min-width: 768px) { + .nav-justified > li { + display: table-cell; + width: 1%; + } + .nav-justified > li > a { + margin-bottom: 0; + } +} +.nav-tabs-justified { + border-bottom: 0; +} +.nav-tabs-justified > li > a { + margin-right: 0; + border-radius: 4px; +} +.nav-tabs-justified > .active > a, +.nav-tabs-justified > .active > a:hover, +.nav-tabs-justified > .active > a:focus { + border: 1px solid #ddd; +} +@media (min-width: 768px) { + .nav-tabs-justified > li > a { + border-bottom: 1px solid #ddd; + border-radius: 4px 4px 0 0; + } + .nav-tabs-justified > .active > a, + .nav-tabs-justified > .active > a:hover, + .nav-tabs-justified > .active > a:focus { + border-bottom-color: #fff; + } +} +.tab-content > .tab-pane { + display: none; + visibility: hidden; +} +.tab-content > .active { + display: block; + visibility: visible; +} +.nav-tabs .dropdown-menu { + margin-top: -1px; + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.navbar { + position: relative; + min-height: 50px; + margin-bottom: 20px; + border: 1px solid transparent; +} +@media (min-width: 768px) { + .navbar { + border-radius: 4px; + } +} +@media (min-width: 768px) { + .navbar-header { + float: left; + } +} +.navbar-collapse { + padding-right: 15px; + padding-left: 15px; + overflow-x: visible; + -webkit-overflow-scrolling: touch; + border-top: 1px solid transparent; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1); +} +.navbar-collapse.in { + overflow-y: auto; +} +@media (min-width: 768px) { + .navbar-collapse { + width: auto; + border-top: 0; + -webkit-box-shadow: none; + box-shadow: none; + } + .navbar-collapse.collapse { + display: block !important; + height: auto !important; + padding-bottom: 0; + overflow: visible !important; + visibility: visible !important; + } + .navbar-collapse.in { + overflow-y: visible; + } + .navbar-fixed-top .navbar-collapse, + .navbar-static-top .navbar-collapse, + .navbar-fixed-bottom .navbar-collapse { + padding-right: 0; + padding-left: 0; + } +} +.navbar-fixed-top .navbar-collapse, +.navbar-fixed-bottom .navbar-collapse { + max-height: 340px; +} +@media (max-device-width: 480px) and (orientation: landscape) { + .navbar-fixed-top .navbar-collapse, + .navbar-fixed-bottom .navbar-collapse { + max-height: 200px; + } +} +.container > .navbar-header, +.container-fluid > .navbar-header, +.container > .navbar-collapse, +.container-fluid > .navbar-collapse { + margin-right: -15px; + margin-left: -15px; +} +@media (min-width: 768px) { + .container > .navbar-header, + .container-fluid > .navbar-header, + .container > .navbar-collapse, + .container-fluid > .navbar-collapse { + margin-right: 0; + margin-left: 0; + } +} +.navbar-static-top { + z-index: 1000; + border-width: 0 0 1px; +} +@media (min-width: 768px) { + .navbar-static-top { + border-radius: 0; + } +} +.navbar-fixed-top, +.navbar-fixed-bottom { + position: fixed; + right: 0; + left: 0; + z-index: 1030; +} +@media (min-width: 768px) { + .navbar-fixed-top, + .navbar-fixed-bottom { + border-radius: 0; + } +} +.navbar-fixed-top { + top: 0; + border-width: 0 0 1px; +} +.navbar-fixed-bottom { + bottom: 0; + margin-bottom: 0; + border-width: 1px 0 0; +} +.navbar-brand { + float: left; + height: 50px; + padding: 15px 15px; + font-size: 18px; + line-height: 20px; +} +.navbar-brand:hover, +.navbar-brand:focus { + text-decoration: none; +} +.navbar-brand > img { + display: block; +} +@media (min-width: 768px) { + .navbar > .container .navbar-brand, + .navbar > .container-fluid .navbar-brand { + margin-left: -15px; + } +} +.navbar-toggle { + position: relative; + float: right; + padding: 9px 10px; + margin-top: 8px; + margin-right: 15px; + margin-bottom: 8px; + background-color: transparent; + background-image: none; + border: 1px solid transparent; + border-radius: 4px; +} +.navbar-toggle:focus { + outline: 0; +} +.navbar-toggle .icon-bar { + display: block; + width: 22px; + height: 2px; + border-radius: 1px; +} +.navbar-toggle .icon-bar + .icon-bar { + margin-top: 4px; +} +@media (min-width: 768px) { + .navbar-toggle { + display: none; + } +} +.navbar-nav { + margin: 7.5px -15px; +} +.navbar-nav > li > a { + padding-top: 10px; + padding-bottom: 10px; + line-height: 20px; +} +@media (max-width: 767px) { + .navbar-nav .open .dropdown-menu { + position: static; + float: none; + width: auto; + margin-top: 0; + background-color: transparent; + border: 0; + -webkit-box-shadow: none; + box-shadow: none; + } + .navbar-nav .open .dropdown-menu > li > a, + .navbar-nav .open .dropdown-menu .dropdown-header { + padding: 5px 15px 5px 25px; + } + .navbar-nav .open .dropdown-menu > li > a { + line-height: 20px; + } + .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-nav .open .dropdown-menu > li > a:focus { + background-image: none; + } +} +@media (min-width: 768px) { + .navbar-nav { + float: left; + margin: 0; + } + .navbar-nav > li { + float: left; + } + .navbar-nav > li > a { + padding-top: 15px; + padding-bottom: 15px; + } +} +.navbar-form { + padding: 10px 15px; + margin-top: 8px; + margin-right: -15px; + margin-bottom: 8px; + margin-left: -15px; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1); +} +@media (min-width: 768px) { + .navbar-form .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .form-control { + display: inline-block; + width: auto; + vertical-align: middle; + } + .navbar-form .form-control-static { + display: inline-block; + } + .navbar-form .input-group { + display: inline-table; + vertical-align: middle; + } + .navbar-form .input-group .input-group-addon, + .navbar-form .input-group .input-group-btn, + .navbar-form .input-group .form-control { + width: auto; + } + .navbar-form .input-group > .form-control { + width: 100%; + } + .navbar-form .control-label { + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .radio, + .navbar-form .checkbox { + display: inline-block; + margin-top: 0; + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .radio label, + .navbar-form .checkbox label { + padding-left: 0; + } + .navbar-form .radio input[type="radio"], + .navbar-form .checkbox input[type="checkbox"] { + position: relative; + margin-left: 0; + } + .navbar-form .has-feedback .form-control-feedback { + top: 0; + } +} +@media (max-width: 767px) { + .navbar-form .form-group { + margin-bottom: 5px; + } + .navbar-form .form-group:last-child { + margin-bottom: 0; + } +} +@media (min-width: 768px) { + .navbar-form { + width: auto; + padding-top: 0; + padding-bottom: 0; + margin-right: 0; + margin-left: 0; + border: 0; + -webkit-box-shadow: none; + box-shadow: none; + } +} +.navbar-nav > li > .dropdown-menu { + margin-top: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.navbar-btn { + margin-top: 8px; + margin-bottom: 8px; +} +.navbar-btn.btn-sm { + margin-top: 10px; + margin-bottom: 10px; +} +.navbar-btn.btn-xs { + margin-top: 14px; + margin-bottom: 14px; +} +.navbar-text { + margin-top: 15px; + margin-bottom: 15px; +} +@media (min-width: 768px) { + .navbar-text { + float: left; + margin-right: 15px; + margin-left: 15px; + } +} +@media (min-width: 768px) { + .navbar-left { + float: left !important; + } + .navbar-right { + float: right !important; + margin-right: -15px; + } + .navbar-right ~ .navbar-right { + margin-right: 0; + } +} +.navbar-default { + background-color: #f8f8f8; + border-color: #e7e7e7; +} +.navbar-default .navbar-brand { + color: #777; +} +.navbar-default .navbar-brand:hover, +.navbar-default .navbar-brand:focus { + color: #5e5e5e; + background-color: transparent; +} +.navbar-default .navbar-text { + color: #777; +} +.navbar-default .navbar-nav > li > a { + color: #777; +} +.navbar-default .navbar-nav > li > a:hover, +.navbar-default .navbar-nav > li > a:focus { + color: #333; + background-color: transparent; +} +.navbar-default .navbar-nav > .active > a, +.navbar-default .navbar-nav > .active > a:hover, +.navbar-default .navbar-nav > .active > a:focus { + color: #555; + background-color: #e7e7e7; +} +.navbar-default .navbar-nav > .disabled > a, +.navbar-default .navbar-nav > .disabled > a:hover, +.navbar-default .navbar-nav > .disabled > a:focus { + color: #ccc; + background-color: transparent; +} +.navbar-default .navbar-toggle { + border-color: #ddd; +} +.navbar-default .navbar-toggle:hover, +.navbar-default .navbar-toggle:focus { + background-color: #ddd; +} +.navbar-default .navbar-toggle .icon-bar { + background-color: #888; +} +.navbar-default .navbar-collapse, +.navbar-default .navbar-form { + border-color: #e7e7e7; +} +.navbar-default .navbar-nav > .open > a, +.navbar-default .navbar-nav > .open > a:hover, +.navbar-default .navbar-nav > .open > a:focus { + color: #555; + background-color: #e7e7e7; +} +@media (max-width: 767px) { + .navbar-default .navbar-nav .open .dropdown-menu > li > a { + color: #777; + } + .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus { + color: #333; + background-color: transparent; + } + .navbar-default .navbar-nav .open .dropdown-menu > .active > a, + .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #555; + background-color: #e7e7e7; + } + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a, + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus { + color: #ccc; + background-color: transparent; + } +} +.navbar-default .navbar-link { + color: #777; +} +.navbar-default .navbar-link:hover { + color: #333; +} +.navbar-default .btn-link { + color: #777; +} +.navbar-default .btn-link:hover, +.navbar-default .btn-link:focus { + color: #333; +} +.navbar-default .btn-link[disabled]:hover, +fieldset[disabled] .navbar-default .btn-link:hover, +.navbar-default .btn-link[disabled]:focus, +fieldset[disabled] .navbar-default .btn-link:focus { + color: #ccc; +} +.navbar-inverse { + background-color: #222; + border-color: #080808; +} +.navbar-inverse .navbar-brand { + color: #9d9d9d; +} +.navbar-inverse .navbar-brand:hover, +.navbar-inverse .navbar-brand:focus { + color: #fff; + background-color: transparent; +} +.navbar-inverse .navbar-text { + color: #9d9d9d; +} +.navbar-inverse .navbar-nav > li > a { + color: #9d9d9d; +} +.navbar-inverse .navbar-nav > li > a:hover, +.navbar-inverse .navbar-nav > li > a:focus { + color: #fff; + background-color: transparent; +} +.navbar-inverse .navbar-nav > .active > a, +.navbar-inverse .navbar-nav > .active > a:hover, +.navbar-inverse .navbar-nav > .active > a:focus { + color: #fff; + background-color: #080808; +} +.navbar-inverse .navbar-nav > .disabled > a, +.navbar-inverse .navbar-nav > .disabled > a:hover, +.navbar-inverse .navbar-nav > .disabled > a:focus { + color: #444; + background-color: transparent; +} +.navbar-inverse .navbar-toggle { + border-color: #333; +} +.navbar-inverse .navbar-toggle:hover, +.navbar-inverse .navbar-toggle:focus { + background-color: #333; +} +.navbar-inverse .navbar-toggle .icon-bar { + background-color: #fff; +} +.navbar-inverse .navbar-collapse, +.navbar-inverse .navbar-form { + border-color: #101010; +} +.navbar-inverse .navbar-nav > .open > a, +.navbar-inverse .navbar-nav > .open > a:hover, +.navbar-inverse .navbar-nav > .open > a:focus { + color: #fff; + background-color: #080808; +} +@media (max-width: 767px) { + .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header { + border-color: #080808; + } + .navbar-inverse .navbar-nav .open .dropdown-menu .divider { + background-color: #080808; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a { + color: #9d9d9d; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus { + color: #fff; + background-color: transparent; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a, + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #fff; + background-color: #080808; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a, + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus { + color: #444; + background-color: transparent; + } +} +.navbar-inverse .navbar-link { + color: #9d9d9d; +} +.navbar-inverse .navbar-link:hover { + color: #fff; +} +.navbar-inverse .btn-link { + color: #9d9d9d; +} +.navbar-inverse .btn-link:hover, +.navbar-inverse .btn-link:focus { + color: #fff; +} +.navbar-inverse .btn-link[disabled]:hover, +fieldset[disabled] .navbar-inverse .btn-link:hover, +.navbar-inverse .btn-link[disabled]:focus, +fieldset[disabled] .navbar-inverse .btn-link:focus { + color: #444; +} +.breadcrumb { + padding: 8px 15px; + margin-bottom: 20px; + list-style: none; + background-color: #f5f5f5; + border-radius: 4px; +} +.breadcrumb > li { + display: inline-block; +} +.breadcrumb > li + li:before { + padding: 0 5px; + color: #ccc; + content: "/\00a0"; +} +.breadcrumb > .active { + color: #777; +} +.pagination { + display: inline-block; + padding-left: 0; + margin: 20px 0; + border-radius: 4px; +} +.pagination > li { + display: inline; +} +.pagination > li > a, +.pagination > li > span { + position: relative; + float: left; + padding: 6px 12px; + margin-left: -1px; + line-height: 1.42857143; + color: #428bca; + text-decoration: none; + background-color: #fff; + border: 1px solid #ddd; +} +.pagination > li:first-child > a, +.pagination > li:first-child > span { + margin-left: 0; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; +} +.pagination > li:last-child > a, +.pagination > li:last-child > span { + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; +} +.pagination > li > a:hover, +.pagination > li > span:hover, +.pagination > li > a:focus, +.pagination > li > span:focus { + color: #2a6496; + background-color: #eee; + border-color: #ddd; +} +.pagination > .active > a, +.pagination > .active > span, +.pagination > .active > a:hover, +.pagination > .active > span:hover, +.pagination > .active > a:focus, +.pagination > .active > span:focus { + z-index: 2; + color: #fff; + cursor: default; + background-color: #428bca; + border-color: #428bca; +} +.pagination > .disabled > span, +.pagination > .disabled > span:hover, +.pagination > .disabled > span:focus, +.pagination > .disabled > a, +.pagination > .disabled > a:hover, +.pagination > .disabled > a:focus { + color: #777; + cursor: not-allowed; + background-color: #fff; + border-color: #ddd; +} +.pagination-lg > li > a, +.pagination-lg > li > span { + padding: 10px 16px; + font-size: 18px; +} +.pagination-lg > li:first-child > a, +.pagination-lg > li:first-child > span { + border-top-left-radius: 6px; + border-bottom-left-radius: 6px; +} +.pagination-lg > li:last-child > a, +.pagination-lg > li:last-child > span { + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; +} +.pagination-sm > li > a, +.pagination-sm > li > span { + padding: 5px 10px; + font-size: 12px; +} +.pagination-sm > li:first-child > a, +.pagination-sm > li:first-child > span { + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; +} +.pagination-sm > li:last-child > a, +.pagination-sm > li:last-child > span { + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; +} +.pager { + padding-left: 0; + margin: 20px 0; + text-align: center; + list-style: none; +} +.pager li { + display: inline; +} +.pager li > a, +.pager li > span { + display: inline-block; + padding: 5px 14px; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 15px; +} +.pager li > a:hover, +.pager li > a:focus { + text-decoration: none; + background-color: #eee; +} +.pager .next > a, +.pager .next > span { + float: right; +} +.pager .previous > a, +.pager .previous > span { + float: left; +} +.pager .disabled > a, +.pager .disabled > a:hover, +.pager .disabled > a:focus, +.pager .disabled > span { + color: #777; + cursor: not-allowed; + background-color: #fff; +} +.label { + display: inline; + padding: .2em .6em .3em; + font-size: 75%; + font-weight: bold; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25em; +} +a.label:hover, +a.label:focus { + color: #fff; + text-decoration: none; + cursor: pointer; +} +.label:empty { + display: none; +} +.btn .label { + position: relative; + top: -1px; +} +.label-default { + background-color: #777; +} +.label-default[href]:hover, +.label-default[href]:focus { + background-color: #5e5e5e; +} +.label-primary { + background-color: #428bca; +} +.label-primary[href]:hover, +.label-primary[href]:focus { + background-color: #3071a9; +} +.label-success { + background-color: #5cb85c; +} +.label-success[href]:hover, +.label-success[href]:focus { + background-color: #449d44; +} +.label-info { + background-color: #5bc0de; +} +.label-info[href]:hover, +.label-info[href]:focus { + background-color: #31b0d5; +} +.label-warning { + background-color: #f0ad4e; +} +.label-warning[href]:hover, +.label-warning[href]:focus { + background-color: #ec971f; +} +.label-danger { + background-color: #d9534f; +} +.label-danger[href]:hover, +.label-danger[href]:focus { + background-color: #c9302c; +} +.badge { + display: inline-block; + min-width: 10px; + padding: 3px 7px; + font-size: 12px; + font-weight: bold; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + background-color: #777; + border-radius: 10px; +} +.badge:empty { + display: none; +} +.btn .badge { + position: relative; + top: -1px; +} +.btn-xs .badge { + top: 0; + padding: 1px 5px; +} +a.badge:hover, +a.badge:focus { + color: #fff; + text-decoration: none; + cursor: pointer; +} +a.list-group-item.active > .badge, +.nav-pills > .active > a > .badge { + color: #428bca; + background-color: #fff; +} +.nav-pills > li > a > .badge { + margin-left: 3px; +} +.jumbotron { + padding: 30px 15px; + margin-bottom: 30px; + color: inherit; + background-color: #eee; +} +.jumbotron h1, +.jumbotron .h1 { + color: inherit; +} +.jumbotron p { + margin-bottom: 15px; + font-size: 21px; + font-weight: 200; +} +.jumbotron > hr { + border-top-color: #d5d5d5; +} +.container .jumbotron, +.container-fluid .jumbotron { + border-radius: 6px; +} +.jumbotron .container { + max-width: 100%; +} +@media screen and (min-width: 768px) { + .jumbotron { + padding: 48px 0; + } + .container .jumbotron { + padding-right: 60px; + padding-left: 60px; + } + .jumbotron h1, + .jumbotron .h1 { + font-size: 63px; + } +} +.thumbnail { + display: block; + padding: 4px; + margin-bottom: 20px; + line-height: 1.42857143; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 4px; + -webkit-transition: border .2s ease-in-out; + -o-transition: border .2s ease-in-out; + transition: border .2s ease-in-out; +} +.thumbnail > img, +.thumbnail a > img { + margin-right: auto; + margin-left: auto; +} +a.thumbnail:hover, +a.thumbnail:focus, +a.thumbnail.active { + border-color: #428bca; +} +.thumbnail .caption { + padding: 9px; + color: #333; +} +.alert { + padding: 15px; + margin-bottom: 20px; + border: 1px solid transparent; + border-radius: 4px; +} +.alert h4 { + margin-top: 0; + color: inherit; +} +.alert .alert-link { + font-weight: bold; +} +.alert > p, +.alert > ul { + margin-bottom: 0; +} +.alert > p + p { + margin-top: 5px; +} +.alert-dismissable, +.alert-dismissible { + padding-right: 35px; +} +.alert-dismissable .close, +.alert-dismissible .close { + position: relative; + top: -2px; + right: -21px; + color: inherit; +} +.alert-success { + color: #3c763d; + background-color: #dff0d8; + border-color: #d6e9c6; +} +.alert-success hr { + border-top-color: #c9e2b3; +} +.alert-success .alert-link { + color: #2b542c; +} +.alert-info { + color: #31708f; + background-color: #d9edf7; + border-color: #bce8f1; +} +.alert-info hr { + border-top-color: #a6e1ec; +} +.alert-info .alert-link { + color: #245269; +} +.alert-warning { + color: #8a6d3b; + background-color: #fcf8e3; + border-color: #faebcc; +} +.alert-warning hr { + border-top-color: #f7e1b5; +} +.alert-warning .alert-link { + color: #66512c; +} +.alert-danger { + color: #a94442; + background-color: #f2dede; + border-color: #ebccd1; +} +.alert-danger hr { + border-top-color: #e4b9c0; +} +.alert-danger .alert-link { + color: #843534; +} +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +@-o-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +@keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +.progress { + height: 20px; + margin-bottom: 20px; + overflow: hidden; + background-color: #f5f5f5; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1); +} +.progress-bar { + float: left; + width: 0; + height: 100%; + font-size: 12px; + line-height: 20px; + color: #fff; + text-align: center; + background-color: #428bca; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15); + -webkit-transition: width .6s ease; + -o-transition: width .6s ease; + transition: width .6s ease; +} +.progress-striped .progress-bar, +.progress-bar-striped { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + -webkit-background-size: 40px 40px; + background-size: 40px 40px; +} +.progress.active .progress-bar, +.progress-bar.active { + -webkit-animation: progress-bar-stripes 2s linear infinite; + -o-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; +} +.progress-bar-success { + background-color: #5cb85c; +} +.progress-striped .progress-bar-success { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +.progress-bar-info { + background-color: #5bc0de; +} +.progress-striped .progress-bar-info { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +.progress-bar-warning { + background-color: #f0ad4e; +} +.progress-striped .progress-bar-warning { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +.progress-bar-danger { + background-color: #d9534f; +} +.progress-striped .progress-bar-danger { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +.media { + margin-top: 15px; +} +.media:first-child { + margin-top: 0; +} +.media-right, +.media > .pull-right { + padding-left: 10px; +} +.media-left, +.media > .pull-left { + padding-right: 10px; +} +.media-left, +.media-right, +.media-body { + display: table-cell; + vertical-align: top; +} +.media-middle { + vertical-align: middle; +} +.media-bottom { + vertical-align: bottom; +} +.media-heading { + margin-top: 0; + margin-bottom: 5px; +} +.media-list { + padding-left: 0; + list-style: none; +} +.list-group { + padding-left: 0; + margin-bottom: 20px; +} +.list-group-item { + position: relative; + display: block; + padding: 10px 15px; + margin-bottom: -1px; + background-color: #fff; + border: 1px solid #ddd; +} +.list-group-item:first-child { + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} +.list-group-item:last-child { + margin-bottom: 0; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; +} +.list-group-item > .badge { + float: right; +} +.list-group-item > .badge + .badge { + margin-right: 5px; +} +a.list-group-item { + color: #555; +} +a.list-group-item .list-group-item-heading { + color: #333; +} +a.list-group-item:hover, +a.list-group-item:focus { + color: #555; + text-decoration: none; + background-color: #f5f5f5; +} +.list-group-item.disabled, +.list-group-item.disabled:hover, +.list-group-item.disabled:focus { + color: #777; + cursor: not-allowed; + background-color: #eee; +} +.list-group-item.disabled .list-group-item-heading, +.list-group-item.disabled:hover .list-group-item-heading, +.list-group-item.disabled:focus .list-group-item-heading { + color: inherit; +} +.list-group-item.disabled .list-group-item-text, +.list-group-item.disabled:hover .list-group-item-text, +.list-group-item.disabled:focus .list-group-item-text { + color: #777; +} +.list-group-item.active, +.list-group-item.active:hover, +.list-group-item.active:focus { + z-index: 2; + color: #fff; + background-color: #428bca; + border-color: #428bca; +} +.list-group-item.active .list-group-item-heading, +.list-group-item.active:hover .list-group-item-heading, +.list-group-item.active:focus .list-group-item-heading, +.list-group-item.active .list-group-item-heading > small, +.list-group-item.active:hover .list-group-item-heading > small, +.list-group-item.active:focus .list-group-item-heading > small, +.list-group-item.active .list-group-item-heading > .small, +.list-group-item.active:hover .list-group-item-heading > .small, +.list-group-item.active:focus .list-group-item-heading > .small { + color: inherit; +} +.list-group-item.active .list-group-item-text, +.list-group-item.active:hover .list-group-item-text, +.list-group-item.active:focus .list-group-item-text { + color: #e1edf7; +} +.list-group-item-success { + color: #3c763d; + background-color: #dff0d8; +} +a.list-group-item-success { + color: #3c763d; +} +a.list-group-item-success .list-group-item-heading { + color: inherit; +} +a.list-group-item-success:hover, +a.list-group-item-success:focus { + color: #3c763d; + background-color: #d0e9c6; +} +a.list-group-item-success.active, +a.list-group-item-success.active:hover, +a.list-group-item-success.active:focus { + color: #fff; + background-color: #3c763d; + border-color: #3c763d; +} +.list-group-item-info { + color: #31708f; + background-color: #d9edf7; +} +a.list-group-item-info { + color: #31708f; +} +a.list-group-item-info .list-group-item-heading { + color: inherit; +} +a.list-group-item-info:hover, +a.list-group-item-info:focus { + color: #31708f; + background-color: #c4e3f3; +} +a.list-group-item-info.active, +a.list-group-item-info.active:hover, +a.list-group-item-info.active:focus { + color: #fff; + background-color: #31708f; + border-color: #31708f; +} +.list-group-item-warning { + color: #8a6d3b; + background-color: #fcf8e3; +} +a.list-group-item-warning { + color: #8a6d3b; +} +a.list-group-item-warning .list-group-item-heading { + color: inherit; +} +a.list-group-item-warning:hover, +a.list-group-item-warning:focus { + color: #8a6d3b; + background-color: #faf2cc; +} +a.list-group-item-warning.active, +a.list-group-item-warning.active:hover, +a.list-group-item-warning.active:focus { + color: #fff; + background-color: #8a6d3b; + border-color: #8a6d3b; +} +.list-group-item-danger { + color: #a94442; + background-color: #f2dede; +} +a.list-group-item-danger { + color: #a94442; +} +a.list-group-item-danger .list-group-item-heading { + color: inherit; +} +a.list-group-item-danger:hover, +a.list-group-item-danger:focus { + color: #a94442; + background-color: #ebcccc; +} +a.list-group-item-danger.active, +a.list-group-item-danger.active:hover, +a.list-group-item-danger.active:focus { + color: #fff; + background-color: #a94442; + border-color: #a94442; +} +.list-group-item-heading { + margin-top: 0; + margin-bottom: 5px; +} +.list-group-item-text { + margin-bottom: 0; + line-height: 1.3; +} +.panel { + margin-bottom: 20px; + background-color: #fff; + border: 1px solid transparent; + border-radius: 4px; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .05); + box-shadow: 0 1px 1px rgba(0, 0, 0, .05); +} +.panel-body { + padding: 15px; +} +.panel-heading { + padding: 10px 15px; + border-bottom: 1px solid transparent; + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel-heading > .dropdown .dropdown-toggle { + color: inherit; +} +.panel-title { + margin-top: 0; + margin-bottom: 0; + font-size: 16px; + color: inherit; +} +.panel-title > a { + color: inherit; +} +.panel-footer { + padding: 10px 15px; + background-color: #f5f5f5; + border-top: 1px solid #ddd; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel > .list-group, +.panel > .panel-collapse > .list-group { + margin-bottom: 0; +} +.panel > .list-group .list-group-item, +.panel > .panel-collapse > .list-group .list-group-item { + border-width: 1px 0; + border-radius: 0; +} +.panel > .list-group:first-child .list-group-item:first-child, +.panel > .panel-collapse > .list-group:first-child .list-group-item:first-child { + border-top: 0; + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel > .list-group:last-child .list-group-item:last-child, +.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child { + border-bottom: 0; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel-heading + .list-group .list-group-item:first-child { + border-top-width: 0; +} +.list-group + .panel-footer { + border-top-width: 0; +} +.panel > .table, +.panel > .table-responsive > .table, +.panel > .panel-collapse > .table { + margin-bottom: 0; +} +.panel > .table caption, +.panel > .table-responsive > .table caption, +.panel > .panel-collapse > .table caption { + padding-right: 15px; + padding-left: 15px; +} +.panel > .table:first-child, +.panel > .table-responsive:first-child > .table:first-child { + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel > .table:first-child > thead:first-child > tr:first-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child { + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel > .table:first-child > thead:first-child > tr:first-child td:first-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child, +.panel > .table:first-child > thead:first-child > tr:first-child th:first-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child { + border-top-left-radius: 3px; +} +.panel > .table:first-child > thead:first-child > tr:first-child td:last-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child, +.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child, +.panel > .table:first-child > thead:first-child > tr:first-child th:last-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child, +.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child { + border-top-right-radius: 3px; +} +.panel > .table:last-child, +.panel > .table-responsive:last-child > .table:last-child { + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel > .table:last-child > tbody:last-child > tr:last-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child { + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child, +.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child { + border-bottom-left-radius: 3px; +} +.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child, +.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child { + border-bottom-right-radius: 3px; +} +.panel > .panel-body + .table, +.panel > .panel-body + .table-responsive, +.panel > .table + .panel-body, +.panel > .table-responsive + .panel-body { + border-top: 1px solid #ddd; +} +.panel > .table > tbody:first-child > tr:first-child th, +.panel > .table > tbody:first-child > tr:first-child td { + border-top: 0; +} +.panel > .table-bordered, +.panel > .table-responsive > .table-bordered { + border: 0; +} +.panel > .table-bordered > thead > tr > th:first-child, +.panel > .table-responsive > .table-bordered > thead > tr > th:first-child, +.panel > .table-bordered > tbody > tr > th:first-child, +.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child, +.panel > .table-bordered > tfoot > tr > th:first-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child, +.panel > .table-bordered > thead > tr > td:first-child, +.panel > .table-responsive > .table-bordered > thead > tr > td:first-child, +.panel > .table-bordered > tbody > tr > td:first-child, +.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child, +.panel > .table-bordered > tfoot > tr > td:first-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child { + border-left: 0; +} +.panel > .table-bordered > thead > tr > th:last-child, +.panel > .table-responsive > .table-bordered > thead > tr > th:last-child, +.panel > .table-bordered > tbody > tr > th:last-child, +.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child, +.panel > .table-bordered > tfoot > tr > th:last-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child, +.panel > .table-bordered > thead > tr > td:last-child, +.panel > .table-responsive > .table-bordered > thead > tr > td:last-child, +.panel > .table-bordered > tbody > tr > td:last-child, +.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child, +.panel > .table-bordered > tfoot > tr > td:last-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child { + border-right: 0; +} +.panel > .table-bordered > thead > tr:first-child > td, +.panel > .table-responsive > .table-bordered > thead > tr:first-child > td, +.panel > .table-bordered > tbody > tr:first-child > td, +.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td, +.panel > .table-bordered > thead > tr:first-child > th, +.panel > .table-responsive > .table-bordered > thead > tr:first-child > th, +.panel > .table-bordered > tbody > tr:first-child > th, +.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th { + border-bottom: 0; +} +.panel > .table-bordered > tbody > tr:last-child > td, +.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td, +.panel > .table-bordered > tfoot > tr:last-child > td, +.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td, +.panel > .table-bordered > tbody > tr:last-child > th, +.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th, +.panel > .table-bordered > tfoot > tr:last-child > th, +.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th { + border-bottom: 0; +} +.panel > .table-responsive { + margin-bottom: 0; + border: 0; +} +.panel-group { + margin-bottom: 20px; +} +.panel-group .panel { + margin-bottom: 0; + border-radius: 4px; +} +.panel-group .panel + .panel { + margin-top: 5px; +} +.panel-group .panel-heading { + border-bottom: 0; +} +.panel-group .panel-heading + .panel-collapse > .panel-body, +.panel-group .panel-heading + .panel-collapse > .list-group { + border-top: 1px solid #ddd; +} +.panel-group .panel-footer { + border-top: 0; +} +.panel-group .panel-footer + .panel-collapse .panel-body { + border-bottom: 1px solid #ddd; +} +.panel-default { + border-color: #ddd; +} +.panel-default > .panel-heading { + color: #333; + background-color: #f5f5f5; + border-color: #ddd; +} +.panel-default > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #ddd; +} +.panel-default > .panel-heading .badge { + color: #f5f5f5; + background-color: #333; +} +.panel-default > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #ddd; +} +.panel-primary { + border-color: #428bca; +} +.panel-primary > .panel-heading { + color: #fff; + background-color: #428bca; + border-color: #428bca; +} +.panel-primary > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #428bca; +} +.panel-primary > .panel-heading .badge { + color: #428bca; + background-color: #fff; +} +.panel-primary > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #428bca; +} +.panel-success { + border-color: #d6e9c6; +} +.panel-success > .panel-heading { + color: #3c763d; + background-color: #dff0d8; + border-color: #d6e9c6; +} +.panel-success > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #d6e9c6; +} +.panel-success > .panel-heading .badge { + color: #dff0d8; + background-color: #3c763d; +} +.panel-success > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #d6e9c6; +} +.panel-info { + border-color: #bce8f1; +} +.panel-info > .panel-heading { + color: #31708f; + background-color: #d9edf7; + border-color: #bce8f1; +} +.panel-info > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #bce8f1; +} +.panel-info > .panel-heading .badge { + color: #d9edf7; + background-color: #31708f; +} +.panel-info > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #bce8f1; +} +.panel-warning { + border-color: #faebcc; +} +.panel-warning > .panel-heading { + color: #8a6d3b; + background-color: #fcf8e3; + border-color: #faebcc; +} +.panel-warning > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #faebcc; +} +.panel-warning > .panel-heading .badge { + color: #fcf8e3; + background-color: #8a6d3b; +} +.panel-warning > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #faebcc; +} +.panel-danger { + border-color: #ebccd1; +} +.panel-danger > .panel-heading { + color: #a94442; + background-color: #f2dede; + border-color: #ebccd1; +} +.panel-danger > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #ebccd1; +} +.panel-danger > .panel-heading .badge { + color: #f2dede; + background-color: #a94442; +} +.panel-danger > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #ebccd1; +} +.embed-responsive { + position: relative; + display: block; + height: 0; + padding: 0; + overflow: hidden; +} +.embed-responsive .embed-responsive-item, +.embed-responsive iframe, +.embed-responsive embed, +.embed-responsive object, +.embed-responsive video { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 100%; + height: 100%; + border: 0; +} +.embed-responsive.embed-responsive-16by9 { + padding-bottom: 56.25%; +} +.embed-responsive.embed-responsive-4by3 { + padding-bottom: 75%; +} +.well { + min-height: 20px; + padding: 19px; + margin-bottom: 20px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); +} +.well blockquote { + border-color: #ddd; + border-color: rgba(0, 0, 0, .15); +} +.well-lg { + padding: 24px; + border-radius: 6px; +} +.well-sm { + padding: 9px; + border-radius: 3px; +} +.close { + float: right; + font-size: 21px; + font-weight: bold; + line-height: 1; + color: #000; + text-shadow: 0 1px 0 #fff; + filter: alpha(opacity=20); + opacity: .2; +} +.close:hover, +.close:focus { + color: #000; + text-decoration: none; + cursor: pointer; + filter: alpha(opacity=50); + opacity: .5; +} +button.close { + -webkit-appearance: none; + padding: 0; + cursor: pointer; + background: transparent; + border: 0; +} +.modal-open { + overflow: hidden; +} +.modal { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + display: none; + overflow: hidden; + -webkit-overflow-scrolling: touch; + outline: 0; +} +.modal.fade .modal-dialog { + -webkit-transition: -webkit-transform .3s ease-out; + -o-transition: -o-transform .3s ease-out; + transition: transform .3s ease-out; + -webkit-transform: translate(0, -25%); + -ms-transform: translate(0, -25%); + -o-transform: translate(0, -25%); + transform: translate(0, -25%); +} +.modal.in .modal-dialog { + -webkit-transform: translate(0, 0); + -ms-transform: translate(0, 0); + -o-transform: translate(0, 0); + transform: translate(0, 0); +} +.modal-open .modal { + overflow-x: hidden; + overflow-y: auto; +} +.modal-dialog { + position: relative; + width: auto; + margin: 10px; +} +.modal-content { + position: relative; + background-color: #fff; + -webkit-background-clip: padding-box; + background-clip: padding-box; + border: 1px solid #999; + border: 1px solid rgba(0, 0, 0, .2); + border-radius: 6px; + outline: 0; + -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, .5); + box-shadow: 0 3px 9px rgba(0, 0, 0, .5); +} +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: #000; +} +.modal-backdrop.fade { + filter: alpha(opacity=0); + opacity: 0; +} +.modal-backdrop.in { + filter: alpha(opacity=50); + opacity: .5; +} +.modal-header { + min-height: 16.42857143px; + padding: 15px; + border-bottom: 1px solid #e5e5e5; +} +.modal-header .close { + margin-top: -2px; +} +.modal-title { + margin: 0; + line-height: 1.42857143; +} +.modal-body { + position: relative; + padding: 15px; +} +.modal-footer { + padding: 15px; + text-align: right; + border-top: 1px solid #e5e5e5; +} +.modal-footer .btn + .btn { + margin-bottom: 0; + margin-left: 5px; +} +.modal-footer .btn-group .btn + .btn { + margin-left: -1px; +} +.modal-footer .btn-block + .btn-block { + margin-left: 0; +} +.modal-scrollbar-measure { + position: absolute; + top: -9999px; + width: 50px; + height: 50px; + overflow: scroll; +} +@media (min-width: 768px) { + .modal-dialog { + width: 600px; + margin: 30px auto; + } + .modal-content { + -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, .5); + box-shadow: 0 5px 15px rgba(0, 0, 0, .5); + } + .modal-sm { + width: 300px; + } +} +@media (min-width: 992px) { + .modal-lg { + width: 900px; + } +} +.tooltip { + position: absolute; + z-index: 1070; + display: block; + font-size: 12px; + line-height: 1.4; + visibility: visible; + filter: alpha(opacity=0); + opacity: 0; +} +.tooltip.in { + filter: alpha(opacity=90); + opacity: .9; +} +.tooltip.top { + padding: 5px 0; + margin-top: -3px; +} +.tooltip.right { + padding: 0 5px; + margin-left: 3px; +} +.tooltip.bottom { + padding: 5px 0; + margin-top: 3px; +} +.tooltip.left { + padding: 0 5px; + margin-left: -3px; +} +.tooltip-inner { + max-width: 200px; + padding: 3px 8px; + color: #fff; + text-align: center; + text-decoration: none; + background-color: #000; + border-radius: 4px; +} +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} +.tooltip.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-width: 5px 5px 0; + border-top-color: #000; +} +.tooltip.top-left .tooltip-arrow { + bottom: 0; + left: 5px; + border-width: 5px 5px 0; + border-top-color: #000; +} +.tooltip.top-right .tooltip-arrow { + right: 5px; + bottom: 0; + border-width: 5px 5px 0; + border-top-color: #000; +} +.tooltip.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-width: 5px 5px 5px 0; + border-right-color: #000; +} +.tooltip.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-width: 5px 0 5px 5px; + border-left-color: #000; +} +.tooltip.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} +.tooltip.bottom-left .tooltip-arrow { + top: 0; + left: 5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} +.tooltip.bottom-right .tooltip-arrow { + top: 0; + right: 5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1060; + display: none; + max-width: 276px; + padding: 1px; + font-size: 14px; + font-weight: normal; + line-height: 1.42857143; + text-align: left; + white-space: normal; + background-color: #fff; + -webkit-background-clip: padding-box; + background-clip: padding-box; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, .2); + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, .2); + box-shadow: 0 5px 10px rgba(0, 0, 0, .2); +} +.popover.top { + margin-top: -10px; +} +.popover.right { + margin-left: 10px; +} +.popover.bottom { + margin-top: 10px; +} +.popover.left { + margin-left: -10px; +} +.popover-title { + padding: 8px 14px; + margin: 0; + font-size: 14px; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + border-radius: 5px 5px 0 0; +} +.popover-content { + padding: 9px 14px; +} +.popover > .arrow, +.popover > .arrow:after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} +.popover > .arrow { + border-width: 11px; +} +.popover > .arrow:after { + content: ""; + border-width: 10px; +} +.popover.top > .arrow { + bottom: -11px; + left: 50%; + margin-left: -11px; + border-top-color: #999; + border-top-color: rgba(0, 0, 0, .25); + border-bottom-width: 0; +} +.popover.top > .arrow:after { + bottom: 1px; + margin-left: -10px; + content: " "; + border-top-color: #fff; + border-bottom-width: 0; +} +.popover.right > .arrow { + top: 50%; + left: -11px; + margin-top: -11px; + border-right-color: #999; + border-right-color: rgba(0, 0, 0, .25); + border-left-width: 0; +} +.popover.right > .arrow:after { + bottom: -10px; + left: 1px; + content: " "; + border-right-color: #fff; + border-left-width: 0; +} +.popover.bottom > .arrow { + top: -11px; + left: 50%; + margin-left: -11px; + border-top-width: 0; + border-bottom-color: #999; + border-bottom-color: rgba(0, 0, 0, .25); +} +.popover.bottom > .arrow:after { + top: 1px; + margin-left: -10px; + content: " "; + border-top-width: 0; + border-bottom-color: #fff; +} +.popover.left > .arrow { + top: 50%; + right: -11px; + margin-top: -11px; + border-right-width: 0; + border-left-color: #999; + border-left-color: rgba(0, 0, 0, .25); +} +.popover.left > .arrow:after { + right: 1px; + bottom: -10px; + content: " "; + border-right-width: 0; + border-left-color: #fff; +} +.carousel { + position: relative; +} +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; +} +.carousel-inner > .item { + position: relative; + display: none; + -webkit-transition: .6s ease-in-out left; + -o-transition: .6s ease-in-out left; + transition: .6s ease-in-out left; +} +.carousel-inner > .item > img, +.carousel-inner > .item > a > img { + line-height: 1; +} +@media all and (transform-3d), (-webkit-transform-3d) { + .carousel-inner > .item { + -webkit-transition: -webkit-transform .6s ease-in-out; + -o-transition: -o-transform .6s ease-in-out; + transition: transform .6s ease-in-out; + + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + -webkit-perspective: 1000; + perspective: 1000; + } + .carousel-inner > .item.next, + .carousel-inner > .item.active.right { + left: 0; + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } + .carousel-inner > .item.prev, + .carousel-inner > .item.active.left { + left: 0; + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } + .carousel-inner > .item.next.left, + .carousel-inner > .item.prev.right, + .carousel-inner > .item.active { + left: 0; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} +.carousel-inner > .active, +.carousel-inner > .next, +.carousel-inner > .prev { + display: block; +} +.carousel-inner > .active { + left: 0; +} +.carousel-inner > .next, +.carousel-inner > .prev { + position: absolute; + top: 0; + width: 100%; +} +.carousel-inner > .next { + left: 100%; +} +.carousel-inner > .prev { + left: -100%; +} +.carousel-inner > .next.left, +.carousel-inner > .prev.right { + left: 0; +} +.carousel-inner > .active.left { + left: -100%; +} +.carousel-inner > .active.right { + left: 100%; +} +.carousel-control { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 15%; + font-size: 20px; + color: #fff; + text-align: center; + text-shadow: 0 1px 2px rgba(0, 0, 0, .6); + filter: alpha(opacity=50); + opacity: .5; +} +.carousel-control.left { + background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); + background-image: -o-linear-gradient(left, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); + background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, .5)), to(rgba(0, 0, 0, .0001))); + background-image: linear-gradient(to right, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1); + background-repeat: repeat-x; +} +.carousel-control.right { + right: 0; + left: auto; + background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); + background-image: -o-linear-gradient(left, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); + background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, .0001)), to(rgba(0, 0, 0, .5))); + background-image: linear-gradient(to right, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1); + background-repeat: repeat-x; +} +.carousel-control:hover, +.carousel-control:focus { + color: #fff; + text-decoration: none; + filter: alpha(opacity=90); + outline: 0; + opacity: .9; +} +.carousel-control .icon-prev, +.carousel-control .icon-next { + position: absolute; + top: 50%; + z-index: 5; + display: inline-block; +} +.carousel-control .icon-prev { + left: 50%; + margin-left: -10px; +} +.carousel-control .icon-next { + right: 50%; + margin-right: -10px; +} +.carousel-control .icon-prev, +.carousel-control .icon-next { + width: 20px; + height: 20px; + margin-top: -10px; + font-family: serif; +} +.carousel-control .icon-prev:before { + content: '\2039'; +} +.carousel-control .icon-next:before { + content: '\203a'; +} +.carousel-indicators { + position: absolute; + bottom: 10px; + left: 50%; + z-index: 15; + width: 60%; + padding-left: 0; + margin-left: -30%; + text-align: center; + list-style: none; +} +.carousel-indicators li { + display: inline-block; + width: 10px; + height: 10px; + margin: 1px; + text-indent: -999px; + cursor: pointer; + background-color: #000 \9; + background-color: rgba(0, 0, 0, 0); + border: 1px solid #fff; + border-radius: 10px; +} +.carousel-indicators .active { + width: 12px; + height: 12px; + margin: 0; + background-color: #fff; +} +.carousel-caption { + position: absolute; + right: 15%; + bottom: 20px; + left: 15%; + z-index: 10; + padding-top: 20px; + padding-bottom: 20px; + color: #fff; + text-align: center; + text-shadow: 0 1px 2px rgba(0, 0, 0, .6); +} +.carousel-caption .btn { + text-shadow: none; +} +@media screen and (min-width: 768px) { + .carousel-control .icon-prev, + .carousel-control .icon-next { + width: 30px; + height: 30px; + margin-top: -15px; + font-size: 30px; + } + .carousel-control .icon-prev { + margin-left: -15px; + } + .carousel-control .icon-next { + margin-right: -15px; + } + .carousel-caption { + right: 20%; + left: 20%; + padding-bottom: 30px; + } + .carousel-indicators { + bottom: 20px; + } +} +.clearfix:before, +.clearfix:after, +.dl-horizontal dd:before, +.dl-horizontal dd:after, +.container:before, +.container:after, +.container-fluid:before, +.container-fluid:after, +.row:before, +.row:after, +.form-horizontal .form-group:before, +.form-horizontal .form-group:after, +.btn-toolbar:before, +.btn-toolbar:after, +.btn-group-vertical > .btn-group:before, +.btn-group-vertical > .btn-group:after, +.nav:before, +.nav:after, +.navbar:before, +.navbar:after, +.navbar-header:before, +.navbar-header:after, +.navbar-collapse:before, +.navbar-collapse:after, +.pager:before, +.pager:after, +.panel-body:before, +.panel-body:after, +.modal-footer:before, +.modal-footer:after { + display: table; + content: " "; +} +.clearfix:after, +.dl-horizontal dd:after, +.container:after, +.container-fluid:after, +.row:after, +.form-horizontal .form-group:after, +.btn-toolbar:after, +.btn-group-vertical > .btn-group:after, +.nav:after, +.navbar:after, +.navbar-header:after, +.navbar-collapse:after, +.pager:after, +.panel-body:after, +.modal-footer:after { + clear: both; +} +.center-block { + display: block; + margin-right: auto; + margin-left: auto; +} +.pull-right { + float: right !important; +} +.pull-left { + float: left !important; +} +.hide { + display: none !important; +} +.show { + display: block !important; +} +.invisible { + visibility: hidden; +} +.text-hide { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} +.hidden { + display: none !important; + visibility: hidden !important; +} +.affix { + position: fixed; +} +@-ms-viewport { + width: device-width; +} +.visible-xs, +.visible-sm, +.visible-md, +.visible-lg { + display: none !important; +} +.visible-xs-block, +.visible-xs-inline, +.visible-xs-inline-block, +.visible-sm-block, +.visible-sm-inline, +.visible-sm-inline-block, +.visible-md-block, +.visible-md-inline, +.visible-md-inline-block, +.visible-lg-block, +.visible-lg-inline, +.visible-lg-inline-block { + display: none !important; +} +@media (max-width: 767px) { + .visible-xs { + display: block !important; + } + table.visible-xs { + display: table; + } + tr.visible-xs { + display: table-row !important; + } + th.visible-xs, + td.visible-xs { + display: table-cell !important; + } +} +@media (max-width: 767px) { + .visible-xs-block { + display: block !important; + } +} +@media (max-width: 767px) { + .visible-xs-inline { + display: inline !important; + } +} +@media (max-width: 767px) { + .visible-xs-inline-block { + display: inline-block !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm { + display: block !important; + } + table.visible-sm { + display: table; + } + tr.visible-sm { + display: table-row !important; + } + th.visible-sm, + td.visible-sm { + display: table-cell !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm-block { + display: block !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm-inline { + display: inline !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm-inline-block { + display: inline-block !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md { + display: block !important; + } + table.visible-md { + display: table; + } + tr.visible-md { + display: table-row !important; + } + th.visible-md, + td.visible-md { + display: table-cell !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md-block { + display: block !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md-inline { + display: inline !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md-inline-block { + display: inline-block !important; + } +} +@media (min-width: 1200px) { + .visible-lg { + display: block !important; + } + table.visible-lg { + display: table; + } + tr.visible-lg { + display: table-row !important; + } + th.visible-lg, + td.visible-lg { + display: table-cell !important; + } +} +@media (min-width: 1200px) { + .visible-lg-block { + display: block !important; + } +} +@media (min-width: 1200px) { + .visible-lg-inline { + display: inline !important; + } +} +@media (min-width: 1200px) { + .visible-lg-inline-block { + display: inline-block !important; + } +} +@media (max-width: 767px) { + .hidden-xs { + display: none !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .hidden-sm { + display: none !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .hidden-md { + display: none !important; + } +} +@media (min-width: 1200px) { + .hidden-lg { + display: none !important; + } +} +.visible-print { + display: none !important; +} +@media print { + .visible-print { + display: block !important; + } + table.visible-print { + display: table; + } + tr.visible-print { + display: table-row !important; + } + th.visible-print, + td.visible-print { + display: table-cell !important; + } +} +.visible-print-block { + display: none !important; +} +@media print { + .visible-print-block { + display: block !important; + } +} +.visible-print-inline { + display: none !important; +} +@media print { + .visible-print-inline { + display: inline !important; + } +} +.visible-print-inline-block { + display: none !important; +} +@media print { + .visible-print-inline-block { + display: inline-block !important; + } +} +@media print { + .hidden-print { + display: none !important; + } +} diff --git a/stat_track/stat_track/static/css/lib/ss-standard.css b/stat_track/stat_track/static/css/lib/ss-standard.css new file mode 100644 index 0000000..3fcc946 --- /dev/null +++ b/stat_track/stat_track/static/css/lib/ss-standard.css @@ -0,0 +1,53 @@ +@charset "UTF-8"; + +/* +* Symbolset +* www.symbolset.com +* Copyright © 2012 Oak Studios LLC +* +* Upload this file to your web server +* and place this within your tags. +* +*/ + +@font-face { + font-family: "SSStandard"; + src: url('../../fonts/ss-standard/ss-standard.eot'); + src: url('../../fonts/ss-standard/ss-standard.eot?#iefix') format('embedded-opentype'), + url('../../fonts/ss-standard/ss-standard.woff') format('woff'), + url('../../fonts/ss-standard/ss-standard.ttf') format('truetype'), + url('../../fonts/ss-standard/ss-standard.svg#SSStandard') format('svg'); + font-weight: normal; + font-style: normal; +} + +/* This triggers a redraw in IE to Fix IE8's :before content rendering. */ +html:hover [class^="ss-"]{-ms-zoom: 1;} + +.ss-icon, .ss-icon.ss-standard, +[class^="ss-"]:before, [class*=" ss-"]:before, +[class^="ss-"].ss-standard:before, [class*=" ss-"].ss-standard:before, +[class^="ss-"].right:after, [class*=" ss-"].right:after, +[class^="ss-"].ss-standard.right:after, [class*=" ss-"].ss-standard.right:after { + font-family: "SSStandard"; + font-style: normal; + font-weight: normal; + text-decoration: none; + text-rendering: optimizeLegibility; + white-space: nowrap; + /*-webkit-font-feature-settings: "liga"; Currently broken in Chrome >= v22. Falls back to text-rendering. Safari is unaffected. */ + -moz-font-feature-settings: "liga=1"; + -moz-font-feature-settings: "liga"; + -ms-font-feature-settings: "liga" 1; + -o-font-feature-settings: "liga"; + font-feature-settings: "liga"; + -webkit-font-smoothing: antialiased; +} + +[class^="ss-"].right:before, +[class*=" ss-"].right:before{display:none;content:'';} + +.ss-cursor:before,.ss-cursor.right:after{content:''}.ss-crosshair:before,.ss-crosshair.right:after{content:'⌖'}.ss-search:before,.ss-search.right:after{content:'🔎'}.ss-zoomin:before,.ss-zoomin.right:after{content:''}.ss-zoomout:before,.ss-zoomout.right:after{content:''}.ss-view:before,.ss-view.right:after{content:'👀'}.ss-attach:before,.ss-attach.right:after{content:'📎'}.ss-link:before,.ss-link.right:after{content:'🔗'}.ss-move:before,.ss-move.right:after{content:''}.ss-write:before,.ss-write.right:after{content:'✎'}.ss-writingdisabled:before,.ss-writingdisabled.right:after{content:''}.ss-erase:before,.ss-erase.right:after{content:'✐'}.ss-compose:before,.ss-compose.right:after{content:'📝'}.ss-lock:before,.ss-lock.right:after{content:'🔒'}.ss-unlock:before,.ss-unlock.right:after{content:'🔓'}.ss-key:before,.ss-key.right:after{content:'🔑'}.ss-backspace:before,.ss-backspace.right:after{content:'⌫'}.ss-ban:before,.ss-ban.right:after{content:'🚫'}.ss-trash:before,.ss-trash.right:after{content:''}.ss-target:before,.ss-target.right:after{content:'◎'}.ss-tag:before,.ss-tag.right:after{content:''}.ss-bookmark:before,.ss-bookmark.right:after{content:'🔖'}.ss-flag:before,.ss-flag.right:after{content:'⚑'}.ss-like:before,.ss-like.right:after{content:'👍'}.ss-dislike:before,.ss-dislike.right:after{content:'👎'}.ss-heart:before,.ss-heart.right:after{content:'♥'}.ss-halfheart:before,.ss-halfheart.right:after{content:''}.ss-star:before,.ss-star.right:after{content:'⋆'}.ss-halfstar:before,.ss-halfstar.right:after{content:''}.ss-sample:before,.ss-sample.right:after{content:''}.ss-crop:before,.ss-crop.right:after{content:''}.ss-layers:before,.ss-layers.right:after{content:''}.ss-fill:before,.ss-fill.right:after{content:''}.ss-stroke:before,.ss-stroke.right:after{content:''}.ss-phone:before,.ss-phone.right:after{content:'📞'}.ss-phonedisabled:before,.ss-phonedisabled.right:after{content:''}.ss-rss:before,.ss-rss.right:after{content:''}.ss-facetime:before,.ss-facetime.right:after{content:''}.ss-reply:before,.ss-reply.right:after{content:'↩'}.ss-send:before,.ss-send.right:after{content:''}.ss-mail:before,.ss-mail.right:after{content:'✉'}.ss-inbox:before,.ss-inbox.right:after{content:'📥'}.ss-chat:before,.ss-chat.right:after{content:'💬'}.ss-ellipsischat:before,.ss-ellipsischat.right:after{content:''}.ss-ellipsis:before,.ss-ellipsis.right:after{content:'…'}.ss-user:before,.ss-user.right:after{content:'👤'}.ss-femaleuser:before,.ss-femaleuser.right:after{content:'👧'}.ss-users:before,.ss-users.right:after{content:'👥'}.ss-cart:before,.ss-cart.right:after{content:''}.ss-creditcard:before,.ss-creditcard.right:after{content:'💳'}.ss-dollarsign:before,.ss-dollarsign.right:after{content:'💲'}.ss-barchart:before,.ss-barchart.right:after{content:'📊'}.ss-piechart:before,.ss-piechart.right:after{content:''}.ss-box:before,.ss-box.right:after{content:'📦'}.ss-home:before,.ss-home.right:after{content:'⌂'}.ss-buildings:before,.ss-buildings.right:after{content:'🏢'}.ss-warehouse:before,.ss-warehouse.right:after{content:''}.ss-globe:before,.ss-globe.right:after{content:'🌎'}.ss-navigate:before,.ss-navigate.right:after{content:''}.ss-compass:before,.ss-compass.right:after{content:''}.ss-signpost:before,.ss-signpost.right:after{content:''}.ss-map:before,.ss-map.right:after{content:''}.ss-location:before,.ss-location.right:after{content:''}.ss-pin:before,.ss-pin.right:after{content:'📍'}.ss-database:before,.ss-database.right:after{content:''}.ss-hdd:before,.ss-hdd.right:after{content:''}.ss-music:before,.ss-music.right:after{content:'♫'}.ss-mic:before,.ss-mic.right:after{content:'🎤'}.ss-volume:before,.ss-volume.right:after{content:'🔈'}.ss-lowvolume:before,.ss-lowvolume.right:after{content:'🔉'}.ss-highvolume:before,.ss-highvolume.right:after{content:'🔊'}.ss-airplay:before,.ss-airplay.right:after{content:''}.ss-camera:before,.ss-camera.right:after{content:'📷'}.ss-picture:before,.ss-picture.right:after{content:'🌄'}.ss-video:before,.ss-video.right:after{content:'📹'}.ss-play:before,.ss-play.right:after{content:'▶'}.ss-pause:before,.ss-pause.right:after{content:''}.ss-stop:before,.ss-stop.right:after{content:'■'}.ss-record:before,.ss-record.right:after{content:'●'}.ss-rewind:before,.ss-rewind.right:after{content:'⏪'}.ss-fastforward:before,.ss-fastforward.right:after{content:'⏩'}.ss-skipback:before,.ss-skipback.right:after{content:'⏮'}.ss-skipforward:before,.ss-skipforward.right:after{content:'⏭'}.ss-eject:before,.ss-eject.right:after{content:'⏏'}.ss-repeat:before,.ss-repeat.right:after{content:'🔁'}.ss-replay:before,.ss-replay.right:after{content:'↺'}.ss-shuffle:before,.ss-shuffle.right:after{content:'🔀'}.ss-book:before,.ss-book.right:after{content:'📕'}.ss-openbook:before,.ss-openbook.right:after{content:'📖'}.ss-notebook:before,.ss-notebook.right:after{content:'📓'}.ss-newspaper:before,.ss-newspaper.right:after{content:'📰'}.ss-grid:before,.ss-grid.right:after{content:''}.ss-rows:before,.ss-rows.right:after{content:''}.ss-columns:before,.ss-columns.right:after{content:''}.ss-thumbnails:before,.ss-thumbnails.right:after{content:''}.ss-filter:before,.ss-filter.right:after{content:''}.ss-desktop:before,.ss-desktop.right:after{content:'💻'}.ss-laptop:before,.ss-laptop.right:after{content:''}.ss-tablet:before,.ss-tablet.right:after{content:''}.ss-cell:before,.ss-cell.right:after{content:'📱'}.ss-battery:before,.ss-battery.right:after{content:'🔋'}.ss-highbattery:before,.ss-highbattery.right:after{content:''}.ss-mediumbattery:before,.ss-mediumbattery.right:after{content:''}.ss-lowbattery:before,.ss-lowbattery.right:after{content:''}.ss-emptybattery:before,.ss-emptybattery.right:after{content:''}.ss-lightbulb:before,.ss-lightbulb.right:after{content:'💡'}.ss-downloadcloud:before,.ss-downloadcloud.right:after{content:''}.ss-download:before,.ss-download.right:after{content:''}.ss-uploadcloud:before,.ss-uploadcloud.right:after{content:''}.ss-upload:before,.ss-upload.right:after{content:''}.ss-fork:before,.ss-fork.right:after{content:''}.ss-merge:before,.ss-merge.right:after{content:''}.ss-transfer:before,.ss-transfer.right:after{content:'⇆'}.ss-refresh:before,.ss-refresh.right:after{content:'↻'}.ss-sync:before,.ss-sync.right:after{content:''}.ss-loading:before,.ss-loading.right:after{content:''}.ss-wifi:before,.ss-wifi.right:after{content:''}.ss-connection:before,.ss-connection.right:after{content:''}.ss-file:before,.ss-file.right:after{content:'📄'}.ss-folder:before,.ss-folder.right:after{content:'📁'}.ss-quote:before,.ss-quote.right:after{content:'“'}.ss-text:before,.ss-text.right:after{content:''}.ss-font:before,.ss-font.right:after{content:''}.ss-print:before,.ss-print.right:after{content:'⎙'}.ss-fax:before,.ss-fax.right:after{content:'📠'}.ss-list:before,.ss-list.right:after{content:''}.ss-layout:before,.ss-layout.right:after{content:''}.ss-action:before,.ss-action.right:after{content:''}.ss-redirect:before,.ss-redirect.right:after{content:'↪'}.ss-expand:before,.ss-expand.right:after{content:'⤢'}.ss-contract:before,.ss-contract.right:after{content:''}.ss-help:before,.ss-help.right:after{content:'❓'}.ss-info:before,.ss-info.right:after{content:'ℹ'}.ss-alert:before,.ss-alert.right:after{content:'⚠'}.ss-caution:before,.ss-caution.right:after{content:'⛔'}.ss-logout:before,.ss-logout.right:after{content:''}.ss-plus:before,.ss-plus.right:after{content:'+'}.ss-hyphen:before,.ss-hyphen.right:after{content:'-'}.ss-check:before,.ss-check.right:after{content:'✓'}.ss-delete:before,.ss-delete.right:after{content:'␡'}.ss-settings:before,.ss-settings.right:after{content:'⚙'}.ss-dashboard:before,.ss-dashboard.right:after{content:''}.ss-notifications:before,.ss-notifications.right:after{content:'🔔'}.ss-notificationsdisabled:before,.ss-notificationsdisabled.right:after{content:'🔕'}.ss-clock:before,.ss-clock.right:after{content:'⏲'}.ss-stopwatch:before,.ss-stopwatch.right:after{content:'⏱'}.ss-calendar:before,.ss-calendar.right:after{content:'📅'}.ss-addcalendar:before,.ss-addcalendar.right:after{content:''}.ss-removecalendar:before,.ss-removecalendar.right:after{content:''}.ss-checkcalendar:before,.ss-checkcalendar.right:after{content:''}.ss-deletecalendar:before,.ss-deletecalendar.right:after{content:''}.ss-plane:before,.ss-plane.right:after{content:'✈'}.ss-briefcase:before,.ss-briefcase.right:after{content:'💼'}.ss-cloud:before,.ss-cloud.right:after{content:'☁'}.ss-droplet:before,.ss-droplet.right:after{content:'💧'}.ss-flask:before,.ss-flask.right:after{content:''}.ss-up:before,.ss-up.right:after{content:'⬆'}.ss-upright:before,.ss-upright.right:after{content:'⬈'}.ss-right:before,.ss-right.right:after{content:'➡'}.ss-downright:before,.ss-downright.right:after{content:'⬊'}.ss-down:before,.ss-down.right:after{content:'⬇'}.ss-downleft:before,.ss-downleft.right:after{content:'⬋'}.ss-left:before,.ss-left.right:after{content:'⬅'}.ss-upleft:before,.ss-upleft.right:after{content:'⬉'}.ss-navigateup:before,.ss-navigateup.right:after{content:''}.ss-navigateright:before,.ss-navigateright.right:after{content:'▻'}.ss-navigatedown:before,.ss-navigatedown.right:after{content:''}.ss-navigateleft:before,.ss-navigateleft.right:after{content:'◅'}.ss-directup:before,.ss-directup.right:after{content:'▴'}.ss-directright:before,.ss-directright.right:after{content:'▹'}.ss-dropdown:before,.ss-dropdown.right:after{content:'▾'}.ss-directleft:before,.ss-directleft.right:after{content:'◃'}.ss-retweet:before,.ss-retweet.right:after{content:''} + +/* Legacy classes */ +.ss-volumelow:before,.ss-volumelow.right:after{content:'🔉'}.ss-volumehigh:before,.ss-volumehigh.right:after{content:'🔊'}.ss-batteryhigh:before,.ss-batteryhigh.right:after{content:''}.ss-batterymedium:before,.ss-batterymedium.right:after{content:''}.ss-batterylow:before,.ss-batterylow.right:after{content:''}.ss-batteryempty:before,.ss-batteryempty.right:after{content:''}.ss-clouddownload:before,.ss-clouddownload.right:after{content:''}.ss-cloudupload:before,.ss-cloudupload.right:after{content:''}.ss-calendaradd:before,.ss-calendaradd.right:after{content:''}.ss-calendarremove:before,.ss-calendarremove.right:after{content:''}.ss-calendarcheck:before,.ss-calendarcheck.right:after{content:''}.ss-calendardelete:before,.ss-calendardelete.right:after{content:''} diff --git a/stat_track/stat_track/static/css/scss/glitch.scss b/stat_track/stat_track/static/css/scss/glitch.scss new file mode 100644 index 0000000..7cd16bf --- /dev/null +++ b/stat_track/stat_track/static/css/scss/glitch.scss @@ -0,0 +1,79 @@ +// +// Glitchy text effect +// http://codepen.io/lbebber/pen/ypgql +// + +.glitch { + font-size: 80px; + position: relative; + + &:after { + animation: noise-anim 2s infinite linear alternate-reverse; + clip: rect(0, 900px, 0, 0); + content: attr(data-text); + left: 2px; + overflow: hidden; + position: absolute; + text-shadow: -1px 0 #fff; + top: 0; + } + + &:before { + animation: noise-anim-2 3s infinite linear alternate-reverse; + clip: rect(0, 900px, 0, 0); + content: attr(data-text); + left: -2px; + overflow: hidden; + position: absolute; + text-shadow: 1px 0 #ff0; + top: 0; + } +} + +@keyframes noise-anim { + 0% { clip: rect(29px, 9999px, 74px, 0); } + 5% { clip: rect(61px, 9999px, 11px, 0); } + 10% { clip: rect(99px, 9999px, 94px, 0); } + 15% { clip: rect(81px, 9999px, 70px, 0); } + 20% { clip: rect(29px, 9999px, 72px, 0); } + 25% { clip: rect(86px, 9999px, 11px, 0); } + 30% { clip: rect(32px, 9999px, 76px, 0); } + 35% { clip: rect(70px, 9999px, 70px, 0); } + 40% { clip: rect(96px, 9999px, 73px, 0); } + 45% { clip: rect(2px, 9999px, 2px, 0); } + 50% { clip: rect(35px, 9999px, 70px, 0); } + 55% { clip: rect(81px, 9999px, 18px, 0); } + 60% { clip: rect(54px, 9999px, 100px, 0); } + 65% { clip: rect(21px, 9999px, 57px, 0); } + 70% { clip: rect(35px, 9999px, 89px, 0); } + 75% { clip: rect(33px, 9999px, 23px, 0); } + 80% { clip: rect(45px, 9999px, 94px, 0); } + 85% { clip: rect(24px, 9999px, 58px, 0); } + 90% { clip: rect(45px, 9999px, 98px, 0); } + 95% { clip: rect(96px, 9999px, 100px, 0); } + 100% { clip: rect(16px, 9999px, 74px, 0); } +} + +@keyframes noise-anim-2 { + 0% { clip: rect(24px, 9999px, 95px, 0); } + 5% { clip: rect(84px, 9999px, 32px, 0); } + 10% { clip: rect(99px, 9999px, 16px, 0); } + 15% { clip: rect(55px, 9999px, 5px, 0); } + 20% { clip: rect(32px, 9999px, 52px, 0); } + 25% { clip: rect(57px, 9999px, 55px, 0); } + 30% { clip: rect(100px, 9999px, 81px, 0); } + 35% { clip: rect(100px, 9999px, 78px, 0); } + 40% { clip: rect(97px, 9999px, 39px, 0); } + 45% { clip: rect(62px, 9999px, 98px, 0); } + 50% { clip: rect(3px, 9999px, 9px, 0); } + 55% { clip: rect(86px, 9999px, 38px, 0); } + 60% { clip: rect(41px, 9999px, 39px, 0); } + 65% { clip: rect(94px, 9999px, 99px, 0); } + 70% { clip: rect(45px, 9999px, 20px, 0); } + 75% { clip: rect(57px, 9999px, 22px, 0); } + 80% { clip: rect(35px, 9999px, 70px, 0); } + 85% { clip: rect(80px, 9999px, 41px, 0); } + 90% { clip: rect(45px, 9999px, 15px, 0); } + 95% { clip: rect(45px, 9999px, 23px, 0); } + 100% { clip: rect(51px, 9999px, 50px, 0); } +} diff --git a/stat_track/stat_track/static/css/scss/style.scss b/stat_track/stat_track/static/css/scss/style.scss new file mode 100644 index 0000000..e69de29 diff --git a/stat_track/stat_track/static/fonts/ss-standard/documentation.html b/stat_track/stat_track/static/fonts/ss-standard/documentation.html new file mode 100644 index 0000000..83b9834 --- /dev/null +++ b/stat_track/stat_track/static/fonts/ss-standard/documentation.html @@ -0,0 +1,3207 @@ + + + + + SS Standard v1.005 + + + + + +
    + +
    +

    SS Standard

    +

    Regular

    +
    + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  + +
      +
    • .ss-cursor
    • +
    • &#xE001;
    • +
    +
    +
      +
    • cursor
    • +
    +
    + ⌖ + +
      +
    • .ss-crosshair
    • +
    • &#x2316;
    • +
    +
    +
      +
    • crosshair
    • +
    • screenshot
    • +
    +
    + 🔎 + +
      +
    • .ss-search
    • +
    • &#x1F50E;
    • +
    +
    +
      +
    • search
    • +
    +
    +  + +
      +
    • .ss-zoomin
    • +
    • &#xE002;
    • +
    +
    +
      +
    • zoom
    • +
    • zoomin
    • +
    • zoom in *
    • +
    +
    +  + +
      +
    • .ss-zoomout
    • +
    • &#xE003;
    • +
    +
    +
      +
    • zoomout
    • +
    • zoom out *
    • +
    +
    + 👀 + +
      +
    • .ss-view
    • +
    • &#x1F440;
    • +
    +
    +
      +
    • view
    • +
    • eye
    • +
    • look
    • +
    • visible
    • +
    • visibility
    • +
    +
    + 📎 + +
      +
    • .ss-attach
    • +
    • &#x1F4CE;
    • +
    +
    +
      +
    • attach
    • +
    • attachment
    • +
    • paperclip
    • +
    +
    + 🔗 + +
      +
    • .ss-link
    • +
    • &#x1F517;
    • +
    +
    +
      +
    • link
    • +
    +
    +  + +
      +
    • .ss-move
    • +
    • &#xE070;
    • +
    +
    +
      +
    • move
    • +
    +
    + ✎ + +
      +
    • .ss-write
    • +
    • &#x270E;
    • +
    +
    +
      +
    • edit
    • +
    • write
    • +
    • pencil
    • +
    +
    +  + +
      +
    • .ss-writingdisabled
    • +
    • &#xE071;
    • +
    +
    +
      +
    • writingdisabled
    • +
    • writing disabled *
    • +
    • pencildisabled
    • +
    • pencil disabled *
    • +
    +
    + ✐ + +
      +
    • .ss-erase
    • +
    • &#x2710;
    • +
    +
    +
      +
    • erase
    • +
    • eraser
    • +
    +
    + 📝 + +
      +
    • .ss-compose
    • +
    • &#x1F4DD;
    • +
    +
    +
      +
    • compose
    • +
    +
    + 🔒 + +
      +
    • .ss-lock
    • +
    • &#x1F512;
    • +
    +
    +
      +
    • lock
    • +
    • locked
    • +
    • private
    • +
    • secure
    • +
    +
    + 🔓 + +
      +
    • .ss-unlock
    • +
    • &#x1F513;
    • +
    +
    +
      +
    • unlock
    • +
    • unlocked
    • +
    • public
    • +
    • insecure
    • +
    +
    + 🔑 + +
      +
    • .ss-key
    • +
    • &#x1F511;
    • +
    +
    +
      +
    • key
    • +
    +
    + ⌫ + +
      +
    • .ss-backspace
    • +
    • &#x232B;
    • +
    +
    +
      +
    • backspace
    • +
    +
    + 🚫 + +
      +
    • .ss-ban
    • +
    • &#x1F6AB;
    • +
    +
    +
      +
    • ban
    • +
    +
    +  + +
      +
    • .ss-trash
    • +
    • &#xE0D0;
    • +
    +
    +
      +
    • trash
    • +
    • trashcan
    • +
    +
    + ◎ + +
      +
    • .ss-target
    • +
    • &#x25CE;
    • +
    +
    +
      +
    • target
    • +
    +
    +  + +
      +
    • .ss-tag
    • +
    • &#xE100;
    • +
    +
    +
      +
    • tag
    • +
    • tags
    • +
    • tagged
    • +
    • keyword
    • +
    • keywords
    • +
    +
    + 🔖 + +
      +
    • .ss-bookmark
    • +
    • &#x1F516;
    • +
    +
    +
      +
    • bookmark
    • +
    +
    + ⚑ + +
      +
    • .ss-flag
    • +
    • &#x2691;
    • +
    +
    +
      +
    • flag
    • +
    +
    + 👍 + +
      +
    • .ss-like
    • +
    • &#x1F44D;
    • +
    +
    +
      +
    • like
    • +
    • approve
    • +
    • thumbsup
    • +
    • thumbs up *
    • +
    +
    + 👎 + +
      +
    • .ss-dislike
    • +
    • &#x1F44E;
    • +
    +
    +
      +
    • dislike
    • +
    • disapprove
    • +
    • thumbsdown
    • +
    • thumbs down *
    • +
    +
    + ♥ + +
      +
    • .ss-heart
    • +
    • &#x2665;
    • +
    +
    +
      +
    • heart
    • +
    • love
    • +
    +
    +  + +
      +
    • .ss-halfheart
    • +
    • &#xE1A0;
    • +
    +
    +
      +
    • halfheart
    • +
    • half heart *
    • +
    • zelda
    • +
    +
    + ⋆ + +
      +
    • .ss-star
    • +
    • &#x22C6;
    • +
    +
    +
      +
    • star
    • +
    • favorite
    • +
    +
    +  + +
      +
    • .ss-halfstar
    • +
    • &#xE1A1;
    • +
    +
    +
      +
    • halfstar
    • +
    • half star *
    • +
    +
    +  + +
      +
    • .ss-sample
    • +
    • &#xE200;
    • +
    +
    +
      +
    • sample
    • +
    • eyedropper
    • +
    +
    +  + +
      +
    • .ss-crop
    • +
    • &#xE201;
    • +
    +
    +
      +
    • crop
    • +
    +
    +  + +
      +
    • .ss-layers
    • +
    • &#xE202;
    • +
    +
    +
      +
    • layers
    • +
    • windows
    • +
    +
    +  + +
      +
    • .ss-fill
    • +
    • &#xE240;
    • +
    +
    +
      +
    • fill
    • +
    +
    +  + +
      +
    • .ss-stroke
    • +
    • &#xE241;
    • +
    +
    +
      +
    • stroke
    • +
    +
    + 📞 + +
      +
    • .ss-phone
    • +
    • &#x1F4DE;
    • +
    +
    +
      +
    • phone
    • +
    • call
    • +
    • telephone
    • +
    +
    +  + +
      +
    • .ss-phonedisabled
    • +
    • &#xE300;
    • +
    +
    +
      +
    • phonedisabled
    • +
    • phone disabled *
    • +
    • calldisabled
    • +
    • call disabled *
    • +
    • endcall
    • +
    • end call *
    • +
    • telephonedisabled
    • +
    • telephone disabled *
    • +
    +
    +  + +
      +
    • .ss-rss
    • +
    • &#xE310;
    • +
    +
    +
      +
    • rss
    • +
    +
    +  + +
      +
    • .ss-facetime
    • +
    • &#xE320;
    • +
    +
    +
      +
    • facetime
    • +
    +
    + ↩ + +
      +
    • .ss-reply
    • +
    • &#x21A9;
    • +
    +
    +
      +
    • reply
    • +
    +
    +  + +
      +
    • .ss-send
    • +
    • &#xE350;
    • +
    +
    +
      +
    • send
    • +
    +
    + ✉ + +
      +
    • .ss-mail
    • +
    • &#x2709;
    • +
    +
    +
      +
    • mail
    • +
    • email
    • +
    • envelope
    • +
    +
    + 📥 + +
      +
    • .ss-inbox
    • +
    • &#x1F4E5;
    • +
    +
    +
      +
    • inbox
    • +
    +
    + 💬 + +
      +
    • .ss-chat
    • +
    • &#x1F4AC;
    • +
    +
    +
      +
    • chat
    • +
    • comment
    • +
    • talk
    • +
    +
    +  + +
      +
    • .ss-ellipsischat
    • +
    • &#xE399;
    • +
    +
    +
      +
    • ellipsischat
    • +
    • ellipsis chat *
    • +
    +
    + … + +
      +
    • .ss-ellipsis
    • +
    • &#x2026;
    • +
    +
    +
      +
    • ellipsis
    • +
    +
    + 👤 + +
      +
    • .ss-user
    • +
    • &#x1F464;
    • +
    +
    +
      +
    • user
    • +
    • avatar
    • +
    • maleavatar
    • +
    • male avatar *
    • +
    • maleuser
    • +
    • male user *
    • +
    +
    + 👧 + +
      +
    • .ss-femaleuser
    • +
    • &#x1F467;
    • +
    +
    +
      +
    • femaleuser
    • +
    • female user *
    • +
    • femaleavatar
    • +
    • female avatar *
    • +
    +
    + 👥 + +
      +
    • .ss-users
    • +
    • &#x1F465;
    • +
    +
    +
      +
    • users
    • +
    • avatars
    • +
    +
    +  + +
      +
    • .ss-cart
    • +
    • &#xE500;
    • +
    +
    +
      +
    • cart
    • +
    • shoppingcart
    • +
    • shopping cart *
    • +
    +
    + 💳 + +
      +
    • .ss-creditcard
    • +
    • &#x1F4B3;
    • +
    +
    +
      +
    • creditcard
    • +
    • credit card *
    • +
    +
    + 💲 + +
      +
    • .ss-dollarsign
    • +
    • &#x1F4B2;
    • +
    +
    +
      +
    • cost
    • +
    • dollarsign
    • +
    • dollar sign *
    • +
    • price
    • +
    +
    + 📊 + +
      +
    • .ss-barchart
    • +
    • &#x1F4CA;
    • +
    +
    +
      +
    • barchart
    • +
    • bar chart *
    • +
    +
    +  + +
      +
    • .ss-piechart
    • +
    • &#xE570;
    • +
    +
    +
      +
    • piechart
    • +
    • pie chart *
    • +
    +
    + 📦 + +
      +
    • .ss-box
    • +
    • &#x1F4E6;
    • +
    +
    +
      +
    • box
    • +
    • package
    • +
    +
    + ⌂ + +
      +
    • .ss-home
    • +
    • &#x2302;
    • +
    +
    +
      +
    • home
    • +
    • house
    • +
    +
    + 🏢 + +
      +
    • .ss-buildings
    • +
    • &#x1F3E2;
    • +
    +
    +
      +
    • buildings
    • +
    • city
    • +
    +
    +  + +
      +
    • .ss-warehouse
    • +
    • &#xE602;
    • +
    +
    +
      +
    • warehouse
    • +
    +
    + 🌎 + +
      +
    • .ss-globe
    • +
    • &#x1F30E;
    • +
    +
    +
      +
    • globe
    • +
    • earth
    • +
    • world
    • +
    +
    +  + +
      +
    • .ss-navigate
    • +
    • &#xE670;
    • +
    +
    +
      +
    • navigate
    • +
    • navigation
    • +
    • locate
    • +
    +
    +  + +
      +
    • .ss-compass
    • +
    • &#xE671;
    • +
    +
    +
      +
    • compass
    • +
    +
    +  + +
      +
    • .ss-signpost
    • +
    • &#xE672;
    • +
    +
    +
      +
    • signpost
    • +
    • directions
    • +
    +
    +  + +
      +
    • .ss-map
    • +
    • &#xE673;
    • +
    +
    +
      +
    • map
    • +
    +
    +  + +
      +
    • .ss-location
    • +
    • &#xE6D0;
    • +
    +
    +
      +
    • location
    • +
    +
    + 📍 + +
      +
    • .ss-pin
    • +
    • &#x1F4CD;
    • +
    +
    +
      +
    • pin
    • +
    +
    +  + +
      +
    • .ss-database
    • +
    • &#xE7A0;
    • +
    +
    +
      +
    • database
    • +
    +
    +  + +
      +
    • .ss-hdd
    • +
    • &#xE7B0;
    • +
    +
    +
      +
    • harddrive
    • +
    • hard drive *
    • +
    • hdd
    • +
    +
    + ♫ + +
      +
    • .ss-music
    • +
    • &#x266B;
    • +
    +
    +
      +
    • music
    • +
    • musicnote
    • +
    • audio
    • +
    +
    + 🎤 + +
      +
    • .ss-mic
    • +
    • &#x1F3A4;
    • +
    +
    +
      +
    • mic
    • +
    • microphone
    • +
    • dictate
    • +
    +
    + 🔈 + +
      +
    • .ss-volume
    • +
    • &#x1F508;
    • +
    +
    +
      +
    • volume
    • +
    • sound
    • +
    • speaker
    • +
    +
    + 🔉 + +
      +
    • .ss-lowvolume
    • +
    • &#x1F509;
    • +
    +
    +
      +
    • lowvolume
    • +
    • low volume *
    • +
    • volumelow
    • +
    • volume low *
    • +
    +
    + 🔊 + +
      +
    • .ss-highvolume
    • +
    • &#x1F50A;
    • +
    +
    +
      +
    • highvolume
    • +
    • high volume *
    • +
    • volumehigh
    • +
    • volume high *
    • +
    +
    +  + +
      +
    • .ss-airplay
    • +
    • &#xE800;
    • +
    +
    +
      +
    • airplay
    • +
    +
    + 📷 + +
      +
    • .ss-camera
    • +
    • &#x1F4F7;
    • +
    +
    +
      +
    • camera
    • +
    +
    + 🌄 + +
      +
    • .ss-picture
    • +
    • &#x1F304;
    • +
    +
    +
      +
    • picture
    • +
    • pictures
    • +
    • image
    • +
    • images
    • +
    • photo
    • +
    • photos
    • +
    • photograph
    • +
    • photographs
    • +
    +
    + 📹 + +
      +
    • .ss-video
    • +
    • &#x1F4F9;
    • +
    +
    +
      +
    • video
    • +
    • videos
    • +
    • videocamera
    • +
    +
    + ▶ + +
      +
    • .ss-play
    • +
    • &#x25B6;
    • +
    +
    +
      +
    • play
    • +
    +
    +  + +
      +
    • .ss-pause
    • +
    • &#xE8A0;
    • +
    +
    +
      +
    • pause
    • +
    +
    + ■ + +
      +
    • .ss-stop
    • +
    • &#x25A0;
    • +
    +
    +
      +
    • stop
    • +
    +
    + ● + +
      +
    • .ss-record
    • +
    • &#x25CF;
    • +
    +
    +
      +
    • record
    • +
    +
    + ⏪ + +
      +
    • .ss-rewind
    • +
    • &#x23EA;
    • +
    +
    +
      +
    • rewind
    • +
    +
    + ⏩ + +
      +
    • .ss-fastforward
    • +
    • &#x23E9;
    • +
    +
    +
      +
    • fastforward
    • +
    • fast forward *
    • +
    +
    + ⏮ + +
      +
    • .ss-skipback
    • +
    • &#x23EE;
    • +
    +
    +
      +
    • skipback
    • +
    • skip back *
    • +
    +
    + ⏭ + +
      +
    • .ss-skipforward
    • +
    • &#x23ED;
    • +
    +
    +
      +
    • skip
    • +
    • skipforward
    • +
    • skip forward *
    • +
    +
    + ⏏ + +
      +
    • .ss-eject
    • +
    • &#x23CF;
    • +
    +
    +
      +
    • eject
    • +
    +
    + 🔁 + +
      +
    • .ss-repeat
    • +
    • &#x1F501;
    • +
    +
    +
      +
    • repeat
    • +
    +
    + ↺ + +
      +
    • .ss-replay
    • +
    • &#x21BA;
    • +
    +
    +
      +
    • replay
    • +
    • rotateleft
    • +
    • rotate left *
    • +
    • undo
    • +
    +
    + 🔀 + +
      +
    • .ss-shuffle
    • +
    • &#x1F500;
    • +
    +
    +
      +
    • shuffle
    • +
    +
    + 📕 + +
      +
    • .ss-book
    • +
    • &#x1F4D5;
    • +
    +
    +
      +
    • book
    • +
    +
    + 📖 + +
      +
    • .ss-openbook
    • +
    • &#x1F4D6;
    • +
    +
    +
      +
    • openbook
    • +
    • open book *
    • +
    +
    + 📓 + +
      +
    • .ss-notebook
    • +
    • &#x1F4D3;
    • +
    +
    +
      +
    • notebook
    • +
    +
    + 📰 + +
      +
    • .ss-newspaper
    • +
    • &#x1F4F0;
    • +
    +
    +
      +
    • newspaper
    • +
    • news
    • +
    +
    +  + +
      +
    • .ss-grid
    • +
    • &#xE9A0;
    • +
    +
    +
      +
    • grid
    • +
    +
    +  + +
      +
    • .ss-rows
    • +
    • &#xE9A1;
    • +
    +
    +
      +
    • rows
    • +
    +
    +  + +
      +
    • .ss-columns
    • +
    • &#xE9A2;
    • +
    +
    +
      +
    • columns
    • +
    +
    +  + +
      +
    • .ss-thumbnails
    • +
    • &#xE9A3;
    • +
    +
    +
      +
    • thumbnails
    • +
    +
    +  + +
      +
    • .ss-filter
    • +
    • &#xE9B0;
    • +
    +
    +
      +
    • filter
    • +
    • funnel
    • +
    +
    + 💻 + +
      +
    • .ss-desktop
    • +
    • &#x1F4BB;
    • +
    +
    +
      +
    • desktop
    • +
    • computer
    • +
    • display
    • +
    • monitor
    • +
    +
    +  + +
      +
    • .ss-laptop
    • +
    • &#xEA00;
    • +
    +
    +
      +
    • laptop
    • +
    +
    +  + +
      +
    • .ss-tablet
    • +
    • &#xEA01;
    • +
    +
    +
      +
    • tablet
    • +
    • ipad
    • +
    +
    + 📱 + +
      +
    • .ss-cell
    • +
    • &#x1F4F1;
    • +
    +
    +
      +
    • cell
    • +
    • cellphone
    • +
    • cell phone *
    • +
    • iphone
    • +
    • mobile
    • +
    • mobilephone
    • +
    • mobile phone *
    • +
    • smartphone
    • +
    +
    + 🔋 + +
      +
    • .ss-battery
    • +
    • &#x1F50B;
    • +
    +
    +
      +
    • battery
    • +
    • fullbattery
    • +
    • full battery *
    • +
    • batteryfull
    • +
    • battery full *
    • +
    +
    +  + +
      +
    • .ss-highbattery
    • +
    • &#xEA10;
    • +
    +
    +
      +
    • highbattery
    • +
    • high battery *
    • +
    • batteryhigh
    • +
    • battery high *
    • +
    +
    +  + +
      +
    • .ss-mediumbattery
    • +
    • &#xEA11;
    • +
    +
    +
      +
    • mediumbattery
    • +
    • medium battery *
    • +
    • batterymedium
    • +
    • battery medium *
    • +
    +
    +  + +
      +
    • .ss-lowbattery
    • +
    • &#xEA12;
    • +
    +
    +
      +
    • lowbattery
    • +
    • low battery *
    • +
    • batterylow
    • +
    • battery low *
    • +
    +
    +  + +
      +
    • .ss-emptybattery
    • +
    • &#xEA13;
    • +
    +
    +
      +
    • emptybattery
    • +
    • empty battery *
    • +
    • batteryempty
    • +
    • battery empty *
    • +
    +
    + 💡 + +
      +
    • .ss-lightbulb
    • +
    • &#x1F4A1;
    • +
    +
    +
      +
    • lightbulb
    • +
    • idea
    • +
    +
    +  + +
      +
    • .ss-downloadcloud
    • +
    • &#xEB00;
    • +
    +
    +
      +
    • downloadcloud
    • +
    • download cloud *
    • +
    • clouddownload
    • +
    • cloud download *
    • +
    +
    +  + +
      +
    • .ss-download
    • +
    • &#xEB01;
    • +
    +
    +
      +
    • download
    • +
    +
    +  + +
      +
    • .ss-uploadcloud
    • +
    • &#xEB40;
    • +
    +
    +
      +
    • uploadcloud
    • +
    • upload cloud *
    • +
    • cloudupload
    • +
    • cloud upload *
    • +
    +
    +  + +
      +
    • .ss-upload
    • +
    • &#xEB41;
    • +
    +
    +
      +
    • upload
    • +
    +
    +  + +
      +
    • .ss-fork
    • +
    • &#xEB80;
    • +
    +
    +
      +
    • fork
    • +
    +
    +  + +
      +
    • .ss-merge
    • +
    • &#xEB81;
    • +
    +
    +
      +
    • merge
    • +
    +
    + ⇆ + +
      +
    • .ss-transfer
    • +
    • &#x21C6;
    • +
    +
    +
      +
    • transfer
    • +
    +
    + ↻ + +
      +
    • .ss-refresh
    • +
    • &#x21BB;
    • +
    +
    +
      +
    • refresh
    • +
    • redo
    • +
    • rotateright
    • +
    • rotate right *
    • +
    +
    +  + +
      +
    • .ss-sync
    • +
    • &#xEB82;
    • +
    +
    +
      +
    • sync
    • +
    • syncing
    • +
    +
    +  + +
      +
    • .ss-loading
    • +
    • &#xEB83;
    • +
    +
    +
      +
    • loading
    • +
    +
    +  + +
      +
    • .ss-wifi
    • +
    • &#xEB84;
    • +
    +
    +
      +
    • wifi
    • +
    +
    +  + +
      +
    • .ss-connection
    • +
    • &#xEB85;
    • +
    +
    +
      +
    • connection
    • +
    • nodes
    • +
    +
    + 📄 + +
      +
    • .ss-file
    • +
    • &#x1F4C4;
    • +
    +
    +
      +
    • file
    • +
    • document
    • +
    • page
    • +
    +
    + 📁 + +
      +
    • .ss-folder
    • +
    • &#x1F4C1;
    • +
    +
    +
      +
    • folder
    • +
    +
    + “ + +
      +
    • .ss-quote
    • +
    • &#x201C;
    • +
    +
    +
      +
    • quote
    • +
    • pullquote
    • +
    • pull quote *
    • +
    +
    +  + +
      +
    • .ss-text
    • +
    • &#xED00;
    • +
    +
    +
      +
    • text
    • +
    +
    +  + +
      +
    • .ss-font
    • +
    • &#xED01;
    • +
    +
    +
      +
    • font
    • +
    • typeface
    • +
    +
    + ⎙ + +
      +
    • .ss-print
    • +
    • &#x2399;
    • +
    +
    +
      +
    • print
    • +
    • printer
    • +
    +
    + 📠 + +
      +
    • .ss-fax
    • +
    • &#x1F4E0;
    • +
    +
    +
      +
    • fax
    • +
    +
    +  + +
      +
    • .ss-list
    • +
    • &#xED50;
    • +
    +
    +
      +
    • list
    • +
    +
    +  + +
      +
    • .ss-layout
    • +
    • &#xEDA0;
    • +
    +
    +
      +
    • layout
    • +
    +
    +  + +
      +
    • .ss-action
    • +
    • &#xEE00;
    • +
    +
    +
      +
    • action
    • +
    • out
    • +
    • share
    • +
    • visit
    • +
    +
    + ↪ + +
      +
    • .ss-redirect
    • +
    • &#x21AA;
    • +
    +
    +
      +
    • redirect
    • +
    +
    + ⤢ + +
      +
    • .ss-expand
    • +
    • &#x2922;
    • +
    +
    +
      +
    • expand
    • +
    +
    +  + +
      +
    • .ss-contract
    • +
    • &#xEE01;
    • +
    +
    +
      +
    • contract
    • +
    +
    + ❓ + +
      +
    • .ss-help
    • +
    • &#x2753;
    • +
    +
    +
      +
    • help
    • +
    • question
    • +
    +
    + ℹ + +
      +
    • .ss-info
    • +
    • &#x2139;
    • +
    +
    +
      +
    • info
    • +
    • information
    • +
    +
    + ⚠ + +
      +
    • .ss-alert
    • +
    • &#x26A0;
    • +
    +
    +
      +
    • alert
    • +
    • warning
    • +
    +
    + ⛔ + +
      +
    • .ss-caution
    • +
    • &#x26D4;
    • +
    +
    +
      +
    • caution
    • +
    +
    +  + +
      +
    • .ss-logout
    • +
    • &#xEE02;
    • +
    +
    +
      +
    • exit
    • +
    • logout
    • +
    • log out *
    • +
    • signout
    • +
    • sign out *
    • +
    +
    + + + +
      +
    • .ss-plus
    • +
    • &#x002B;
    • +
    +
    +
      +
    • plus
    • +
    • add
    • +
    +
    + - + +
      +
    • .ss-hyphen
    • +
    • &#x002D;
    • +
    +
    +
      +
    • hyphen
    • +
    • minus
    • +
    • remove
    • +
    • subtract
    • +
    +
    + ✓ + +
      +
    • .ss-check
    • +
    • &#x2713;
    • +
    +
    +
      +
    • check
    • +
    • checked
    • +
    • checkmark
    • +
    +
    + ␡ + +
      +
    • .ss-delete
    • +
    • &#x2421;
    • +
    +
    +
      +
    • delete
    • +
    • close
    • +
    +
    + ⚙ + +
      +
    • .ss-settings
    • +
    • &#x2699;
    • +
    +
    +
      +
    • settings
    • +
    • cog
    • +
    • gear
    • +
    +
    +  + +
      +
    • .ss-dashboard
    • +
    • &#xF000;
    • +
    +
    +
      +
    • dashboard
    • +
    +
    + 🔔 + +
      +
    • .ss-notifications
    • +
    • &#x1F514;
    • +
    +
    +
      +
    • notification
    • +
    • notifications
    • +
    • bell
    • +
    +
    + 🔕 + +
      +
    • .ss-notificationsdisabled
    • +
    • &#x1F515;
    • +
    +
    +
      +
    • notificationsdisabled
    • +
    • notifications disabled *
    • +
    • notificationdisabled
    • +
    • notification disabled *
    • +
    • belldisabled
    • +
    • bell disabled *
    • +
    +
    + ⏲ + +
      +
    • .ss-clock
    • +
    • &#x23F2;
    • +
    +
    +
      +
    • clock
    • +
    • time
    • +
    +
    + ⏱ + +
      +
    • .ss-stopwatch
    • +
    • &#x23F1;
    • +
    +
    +
      +
    • stopwatch
    • +
    • timer
    • +
    +
    + 📅 + +
      +
    • .ss-calendar
    • +
    • &#x1F4C5;
    • +
    +
    +
      +
    • calendar
    • +
    • date
    • +
    +
    +  + +
      +
    • .ss-addcalendar
    • +
    • &#xF070;
    • +
    +
    +
      +
    • addcalendar
    • +
    • add calendar *
    • +
    • adddate
    • +
    • add date *
    • +
    • calendaradd
    • +
    • calendar add *
    • +
    +
    +  + +
      +
    • .ss-removecalendar
    • +
    • &#xF071;
    • +
    +
    +
      +
    • removecalendar
    • +
    • remove calendar *
    • +
    • removedate
    • +
    • remove date *
    • +
    • calendarremove
    • +
    • calendar remove *
    • +
    +
    +  + +
      +
    • .ss-checkcalendar
    • +
    • &#xF072;
    • +
    +
    +
      +
    • checkcalendar
    • +
    • check calendar *
    • +
    • checkdate
    • +
    • check date *
    • +
    • calendarcheck
    • +
    • calendar check *
    • +
    +
    +  + +
      +
    • .ss-deletecalendar
    • +
    • &#xF073;
    • +
    +
    +
      +
    • deletecalendar
    • +
    • delete calendar *
    • +
    • deletedate
    • +
    • delete date *
    • +
    • calendardelete
    • +
    • calendar delete *
    • +
    +
    + ✈ + +
      +
    • .ss-plane
    • +
    • &#x2708;
    • +
    +
    +
      +
    • plane
    • +
    +
    + 💼 + +
      +
    • .ss-briefcase
    • +
    • &#x1F4BC;
    • +
    +
    +
      +
    • briefcase
    • +
    • work
    • +
    +
    + ☁ + +
      +
    • .ss-cloud
    • +
    • &#x2601;
    • +
    +
    +
      +
    • cloud
    • +
    +
    + 💧 + +
      +
    • .ss-droplet
    • +
    • &#x1F4A7;
    • +
    +
    +
      +
    • drop
    • +
    • droplet
    • +
    +
    +  + +
      +
    • .ss-flask
    • +
    • &#xF4C0;
    • +
    +
    +
      +
    • flask
    • +
    +
    + ⬆ + +
      +
    • .ss-up
    • +
    • &#x2B06;
    • +
    +
    +
      +
    • up
    • +
    +
    + ⬈ + +
      +
    • .ss-upright
    • +
    • &#x2B08;
    • +
    +
    +
      +
    • upright
    • +
    • up right *
    • +
    +
    + ➡ + +
      +
    • .ss-right
    • +
    • &#x27A1;
    • +
    +
    +
      +
    • right
    • +
    • forward
    • +
    +
    + ⬊ + +
      +
    • .ss-downright
    • +
    • &#x2B0A;
    • +
    +
    +
      +
    • downright
    • +
    • down right *
    • +
    +
    + ⬇ + +
      +
    • .ss-down
    • +
    • &#x2B07;
    • +
    +
    +
      +
    • down
    • +
    +
    + ⬋ + +
      +
    • .ss-downleft
    • +
    • &#x2B0B;
    • +
    +
    +
      +
    • downleft
    • +
    • down left *
    • +
    +
    + ⬅ + +
      +
    • .ss-left
    • +
    • &#x2B05;
    • +
    +
    +
      +
    • left
    • +
    • back
    • +
    +
    + ⬉ + +
      +
    • .ss-upleft
    • +
    • &#x2B09;
    • +
    +
    +
      +
    • upleft
    • +
    • up left *
    • +
    +
    +  + +
      +
    • .ss-navigateup
    • +
    • &#xF500;
    • +
    +
    +
      +
    • navigateup
    • +
    • navigate up *
    • +
    +
    + ▻ + +
      +
    • .ss-navigateright
    • +
    • &#x25BB;
    • +
    +
    +
      +
    • navigateright
    • +
    • navigate right *
    • +
    • next
    • +
    +
    +  + +
      +
    • .ss-navigatedown
    • +
    • &#xF501;
    • +
    +
    +
      +
    • navigatedown
    • +
    • navigate down *
    • +
    +
    + ◅ + +
      +
    • .ss-navigateleft
    • +
    • &#x25C5;
    • +
    +
    +
      +
    • navigateleft
    • +
    • navigate left *
    • +
    • previous
    • +
    +
    + ▴ + +
      +
    • .ss-directup
    • +
    • &#x25B4;
    • +
    +
    +
      +
    • ascend
    • +
    • ascending
    • +
    • directup
    • +
    • direct up *
    • +
    +
    + ▹ + +
      +
    • .ss-directright
    • +
    • &#x25B9;
    • +
    +
    +
      +
    • directright
    • +
    • direct right *
    • +
    +
    + ▾ + +
      +
    • .ss-dropdown
    • +
    • &#x25BE;
    • +
    +
    +
      +
    • descend
    • +
    • descending
    • +
    • directdown
    • +
    • direct down *
    • +
    • dropdown
    • +
    +
    + ◃ + +
      +
    • .ss-directleft
    • +
    • &#x25C3;
    • +
    +
    +
      +
    • directleft
    • +
    • direct left *
    • +
    +
    +  + +
      +
    • .ss-retweet
    • +
    • &#xF600;
    • +
    +
    +
      +
    • retweet
    • +
    +
    + +
    + +

    *Note: Keywords with spaces are not currently supported in Firefox or Chrome 22.

    + +
    + + + + + + + + diff --git a/stat_track/stat_track/static/fonts/ss-standard/license.txt b/stat_track/stat_track/static/fonts/ss-standard/license.txt new file mode 100644 index 0000000..7664001 --- /dev/null +++ b/stat_track/stat_track/static/fonts/ss-standard/license.txt @@ -0,0 +1,76 @@ + +--- +Symbolset +www.symbolset.com +Copyright © 2013 Oak Studios LLC +--- + + +SS Standard v1.005 + + +-------------------- +License +-------------------- + +This license agreement ("Agreement") is a legal contract between you ("Licensee") and Symbolset ("Licensor"). By purchasing, downloading, using, or copying any image or part thereof, from the internet or by any other method, of any icons, symbols, or pictograms ("Product") licensed by Symbolset, you automatically agree to the following terms and conditions: + +# Definitions +"Product" is defined as any part or whole of one or more icons, symbols, or pictographic images in vector, raster, and/or font formats. + +"Webfont" is defined as font software that has been created, optimized, translated or otherwise modified for use by means of transmitting via the Internet for use in the viewing of text on a website. By way of explanation, not limitation, such uses can be implemented by means of the CSS @font-face rule. Other technology or software implementations used to effect similar results may apply and such uses or methods shall be governed by this Agreement. + +# Grant of Agreement +Number of Users +Licensor grants the Licensee a non-exclusive license to use the Product. The maximum number of simultaneous users is limited to five (5). All users must belong to the same company or household purchasing the Product. + +Embedding +You may embed the licensed fonts into documents you send to third parties. Such documents may be viewed and printed (but not edited) by the recipients. + +You may embed the licensed fonts into software, including but not limited to websites and mobile applications. However, you may not redistribute or transfer the Product to end users or third parties through your software or by any means. An additional license is required if the software permits editing of text by end users or third parties. + +Back-up +You may make a reasonable number of back-up copies of the Product for archival purposes only. Copies must contain the same documentation, copyright, trademark, and other proprietary notices that appear on, in, or with the Product. + +Service Bureaus +You may take a digitized copy of the Product used in a particular document to a commercial print or service bureau for outputting this (a) particular document (this document must not be edited by the printer or service bureau). In the event of any modifications to the document or use of the Product for other purposes, the printer or service bureau must purchase its own Product license. + +Copying +Except for Embedding and Service Bureaus, you may not copy the Product or allow third parties to copy the Product. Any copy of the Product must contain the same copyright, trademark, and other proprietary information as the originals. + +Modifications +You may import characters from the font as graphical objects into a drawing program and modify such graphical objects. + +You may not modify, adapt, translate, reverse engineer, decompile, disassemble, or create derivative works based on the licensed font itself without Symbolset’s prior written consent. + +# Transfer of License +Except as expressly provided herein, you may not give, rent, or lease the Product or parts of it to third parties. You may transfer all your rights to use the Product and Documentation to another person or legal entity provided that: +-The Transferee accepts and agrees in writing (with copy to Licensee) to be bound by all the terms and conditions of this Agreement, and +-You destroy all copies of the Product and Documentation, including all copies stored in the memory of a hardware device. Without limiting the generality of the foregoing, you agree that you will not distribute or disseminate all or any part of the Product through any online service. + +# Refund Policy +No refund will be offered for products once sold. Because they are digital files, all symbols are not refundable or exchangeable. However files can be replaced if damaged or lost. + +# Copyright +The font and the accompanying materials are copyrighted and contain proprietary information and trade secrets belonging to the designer owning the font. Unauthorized copying of the Product (even if modified, merged, or included with other software), or of the written materials, is expressly forbidden. You may be held legally responsible for any infringement of the foundry’s intellectual property rights that is caused or encouraged by your failure to abide by the terms of this Agreement. + +# Disclaimer & Warranty +All licensed Products are provided ‘as-is’ without warranty of any kind, either expressed or implied. Licensor is not liable for any damages arising out of any defects in the materials. You agree to hold Licensor harmless for any result that may occur during the course of using, or inability to use these licensed Products. In no event shall Licensor be liable for any damages including, but not limited to, direct, indirect, special, incidental or consequential damages or other losses arising out of the use of or inability to use the Products. + +# Reserved Rights +We reserve rights to change the prices of the Product and to update the Product without prior notice and at our sole discretion. We reserve the rights to make changes to this Agreement without any prior notice at our sole discretion. Any such changes will be reflected on this page. + +# Governing Law & General Provisions +This Agreement will be governed by the laws in force in the United States of America and New York excluding the application of its conflicts of law rules. This agreement will not be governed by the United Nations Convention of Contracts for the International Sale of Goods, the application of which is expressly excluded. If any part of this Agreement is found void and unenforceable, it will not affect the validity of the balance of the Agreement, which shall remain valid and enforceable according to its terms. You agree that the Product(s) will not be shipped, transferred, or exported into any country or used in any manner prohibited by export administration legislation or any other export laws, restrictions, or regulations. + +# Termination +Symbolset has the right to terminate your license immediately if you fail to comply with any term of this Agreement. Upon termination, you must destroy the original and any copies of the Product and Documentation. + +# Contact +If you have any questions or comments about this Privacy Policy, you can contact us at help@symbolset.com. + +Symbolset +10 Jay St., #606 +Brooklyn, NY 11201 + +http://symbolset.com diff --git a/stat_track/stat_track/static/fonts/ss-standard/readme.txt b/stat_track/stat_track/static/fonts/ss-standard/readme.txt new file mode 100644 index 0000000..7f21809 --- /dev/null +++ b/stat_track/stat_track/static/fonts/ss-standard/readme.txt @@ -0,0 +1,78 @@ + +--- +Symbolset +www.symbolset.com +Copyright © 2013 Oak Studios LLC +--- + + +SS Standard v1.005 + + +-------------------- +Desktop +-------------------- + +Your download contains the set you purchased as OTF, TTF, EOT, WOFF, and SVG font files. OTF, or OpenType Format, is a cross-platform font file for use on the desktop. Don't be misled by the name, each of these file types contain OpenType features. The remaining four files are formatted for web browsers. + +Install the OTF font and use it in any desktop application that supports OpenType features. + +Your symbol set contains both symbols and alphabetic characters. Begin by typing a keyword from the documentation. Once the keyword is completed, and the font recognizes the keyword, the correlating symbol will be displayed in place of the letters. + + +-------------------- +Web +-------------------- + +Start by first uploading the "webfonts" folder to your webserver. Then place this link tag within the tags of your HTML: + + + +There are two common methods to use and reference symbols. + +The first is use a symbol alongside a complementary word. Using class names, with CSS :before pseudo elements and Unicode code points, we can append symbols to words. In this example we're adding a check symbol to a submit button: + + + +The second way is to use a symbol in place of a word. This method triggers symbols with semantic keywords. When using this method on the web apply the generic "ss-icon" class to the element wrapping the keyword. + +For example, when showing that a settings field is locked: + + Settings locked + +A lock symbol will be displayed alongside the word "Settings". + +When using the replacement technique, sighted users will see symbols and search engines and screen readers will see the semantic keyword. + +You can also use symbols with their direct Unicode value. See documentation.html for a full list of supported icons. This example will show a star icon: + + Favorites + + +-------------------- +Browser support +-------------------- + +Keywords (with optional Javascript support): +Firefox 3.5+, Chrome 1+, Safari 2+, IE 7+, Opera 10.5+, iOS 3.2+, Android 1+ + +Keywords (without Javascript): +Firefox 3.5+, Chrome 11+, Safari 5+, IE 10+, iOS 4.2+, Android 3+ + +Unicode: All browsers that support TTF, EOT, WOFF, or SVG webfonts. + +To add optional keyword support to legacy browsers through Javascript, insert this code at the end of your document before the tag: + + + +For more on browser support, see: http://symbolset.com/blog/browser-support/ + +Note: Keywords with spaces are not currently supported in Firefox. + + +-------------------- +Versions +-------------------- + +Periodically Symbolset will push updates to sets. We'll add keywords, build out symbols, and squash bugs. Be sure to check your account (http://symbolset.com/account) for updates. + diff --git a/stat_track/stat_track/static/fonts/ss-standard/ss-standard.eot b/stat_track/stat_track/static/fonts/ss-standard/ss-standard.eot new file mode 100644 index 0000000..69aaa0b Binary files /dev/null and b/stat_track/stat_track/static/fonts/ss-standard/ss-standard.eot differ diff --git a/stat_track/stat_track/static/fonts/ss-standard/ss-standard.otf b/stat_track/stat_track/static/fonts/ss-standard/ss-standard.otf new file mode 100644 index 0000000..1a4e5ec Binary files /dev/null and b/stat_track/stat_track/static/fonts/ss-standard/ss-standard.otf differ diff --git a/stat_track/stat_track/static/fonts/ss-standard/ss-standard.svg b/stat_track/stat_track/static/fonts/ss-standard/ss-standard.svg new file mode 100644 index 0000000..5520caa --- /dev/null +++ b/stat_track/stat_track/static/fonts/ss-standard/ss-standard.svg @@ -0,0 +1,404 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/stat_track/stat_track/static/fonts/ss-standard/ss-standard.ttf b/stat_track/stat_track/static/fonts/ss-standard/ss-standard.ttf new file mode 100644 index 0000000..46f86a8 Binary files /dev/null and b/stat_track/stat_track/static/fonts/ss-standard/ss-standard.ttf differ diff --git a/stat_track/stat_track/static/fonts/ss-standard/ss-standard.woff b/stat_track/stat_track/static/fonts/ss-standard/ss-standard.woff new file mode 100644 index 0000000..2cfc863 Binary files /dev/null and b/stat_track/stat_track/static/fonts/ss-standard/ss-standard.woff differ diff --git a/stat_track/stat_track/static/img/favicon.ico b/stat_track/stat_track/static/img/favicon.ico new file mode 100644 index 0000000..659ee2a Binary files /dev/null and b/stat_track/stat_track/static/img/favicon.ico differ diff --git a/stat_track/stat_track/static/img/loading.gif b/stat_track/stat_track/static/img/loading.gif new file mode 100644 index 0000000..4dedbdc Binary files /dev/null and b/stat_track/stat_track/static/img/loading.gif differ diff --git a/stat_track/stat_track/static/js/coffee/script.coffee b/stat_track/stat_track/static/js/coffee/script.coffee new file mode 100644 index 0000000..e69de29 diff --git a/stat_track/stat_track/static/js/lib/bootstrap.js b/stat_track/stat_track/static/js/lib/bootstrap.js new file mode 100644 index 0000000..8dff365 --- /dev/null +++ b/stat_track/stat_track/static/js/lib/bootstrap.js @@ -0,0 +1,2276 @@ +/*! + * Bootstrap v3.3.0 (http://getbootstrap.com) + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ + +if (typeof jQuery === 'undefined') { + throw new Error('Bootstrap\'s JavaScript requires jQuery') +} + ++function ($) { + var version = $.fn.jquery.split(' ')[0].split('.') + if ((version[0] < 2 && version[1] < 9) || (version[0] == 1 && version[1] == 9 && version[2] < 1)) { + throw new Error('Bootstrap\'s JavaScript requires jQuery version 1.9.1 or higher') + } +}(jQuery); + +/* ======================================================================== + * Bootstrap: transition.js v3.3.0 + * http://getbootstrap.com/javascript/#transitions + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/) + // ============================================================ + + function transitionEnd() { + var el = document.createElement('bootstrap') + + var transEndEventNames = { + WebkitTransition : 'webkitTransitionEnd', + MozTransition : 'transitionend', + OTransition : 'oTransitionEnd otransitionend', + transition : 'transitionend' + } + + for (var name in transEndEventNames) { + if (el.style[name] !== undefined) { + return { end: transEndEventNames[name] } + } + } + + return false // explicit for ie8 ( ._.) + } + + // http://blog.alexmaccaw.com/css-transitions + $.fn.emulateTransitionEnd = function (duration) { + var called = false + var $el = this + $(this).one('bsTransitionEnd', function () { called = true }) + var callback = function () { if (!called) $($el).trigger($.support.transition.end) } + setTimeout(callback, duration) + return this + } + + $(function () { + $.support.transition = transitionEnd() + + if (!$.support.transition) return + + $.event.special.bsTransitionEnd = { + bindType: $.support.transition.end, + delegateType: $.support.transition.end, + handle: function (e) { + if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments) + } + } + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: alert.js v3.3.0 + * http://getbootstrap.com/javascript/#alerts + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // ALERT CLASS DEFINITION + // ====================== + + var dismiss = '[data-dismiss="alert"]' + var Alert = function (el) { + $(el).on('click', dismiss, this.close) + } + + Alert.VERSION = '3.3.0' + + Alert.TRANSITION_DURATION = 150 + + Alert.prototype.close = function (e) { + var $this = $(this) + var selector = $this.attr('data-target') + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 + } + + var $parent = $(selector) + + if (e) e.preventDefault() + + if (!$parent.length) { + $parent = $this.closest('.alert') + } + + $parent.trigger(e = $.Event('close.bs.alert')) + + if (e.isDefaultPrevented()) return + + $parent.removeClass('in') + + function removeElement() { + // detach from parent, fire event then clean up data + $parent.detach().trigger('closed.bs.alert').remove() + } + + $.support.transition && $parent.hasClass('fade') ? + $parent + .one('bsTransitionEnd', removeElement) + .emulateTransitionEnd(Alert.TRANSITION_DURATION) : + removeElement() + } + + + // ALERT PLUGIN DEFINITION + // ======================= + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.alert') + + if (!data) $this.data('bs.alert', (data = new Alert(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + var old = $.fn.alert + + $.fn.alert = Plugin + $.fn.alert.Constructor = Alert + + + // ALERT NO CONFLICT + // ================= + + $.fn.alert.noConflict = function () { + $.fn.alert = old + return this + } + + + // ALERT DATA-API + // ============== + + $(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: button.js v3.3.0 + * http://getbootstrap.com/javascript/#buttons + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // BUTTON PUBLIC CLASS DEFINITION + // ============================== + + var Button = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, Button.DEFAULTS, options) + this.isLoading = false + } + + Button.VERSION = '3.3.0' + + Button.DEFAULTS = { + loadingText: 'loading...' + } + + Button.prototype.setState = function (state) { + var d = 'disabled' + var $el = this.$element + var val = $el.is('input') ? 'val' : 'html' + var data = $el.data() + + state = state + 'Text' + + if (data.resetText == null) $el.data('resetText', $el[val]()) + + // push to event loop to allow forms to submit + setTimeout($.proxy(function () { + $el[val](data[state] == null ? this.options[state] : data[state]) + + if (state == 'loadingText') { + this.isLoading = true + $el.addClass(d).attr(d, d) + } else if (this.isLoading) { + this.isLoading = false + $el.removeClass(d).removeAttr(d) + } + }, this), 0) + } + + Button.prototype.toggle = function () { + var changed = true + var $parent = this.$element.closest('[data-toggle="buttons"]') + + if ($parent.length) { + var $input = this.$element.find('input') + if ($input.prop('type') == 'radio') { + if ($input.prop('checked') && this.$element.hasClass('active')) changed = false + else $parent.find('.active').removeClass('active') + } + if (changed) $input.prop('checked', !this.$element.hasClass('active')).trigger('change') + } else { + this.$element.attr('aria-pressed', !this.$element.hasClass('active')) + } + + if (changed) this.$element.toggleClass('active') + } + + + // BUTTON PLUGIN DEFINITION + // ======================== + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.button') + var options = typeof option == 'object' && option + + if (!data) $this.data('bs.button', (data = new Button(this, options))) + + if (option == 'toggle') data.toggle() + else if (option) data.setState(option) + }) + } + + var old = $.fn.button + + $.fn.button = Plugin + $.fn.button.Constructor = Button + + + // BUTTON NO CONFLICT + // ================== + + $.fn.button.noConflict = function () { + $.fn.button = old + return this + } + + + // BUTTON DATA-API + // =============== + + $(document) + .on('click.bs.button.data-api', '[data-toggle^="button"]', function (e) { + var $btn = $(e.target) + if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') + Plugin.call($btn, 'toggle') + e.preventDefault() + }) + .on('focus.bs.button.data-api blur.bs.button.data-api', '[data-toggle^="button"]', function (e) { + $(e.target).closest('.btn').toggleClass('focus', e.type == 'focus') + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: carousel.js v3.3.0 + * http://getbootstrap.com/javascript/#carousel + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // CAROUSEL CLASS DEFINITION + // ========================= + + var Carousel = function (element, options) { + this.$element = $(element) + this.$indicators = this.$element.find('.carousel-indicators') + this.options = options + this.paused = + this.sliding = + this.interval = + this.$active = + this.$items = null + + this.options.keyboard && this.$element.on('keydown.bs.carousel', $.proxy(this.keydown, this)) + + this.options.pause == 'hover' && !('ontouchstart' in document.documentElement) && this.$element + .on('mouseenter.bs.carousel', $.proxy(this.pause, this)) + .on('mouseleave.bs.carousel', $.proxy(this.cycle, this)) + } + + Carousel.VERSION = '3.3.0' + + Carousel.TRANSITION_DURATION = 600 + + Carousel.DEFAULTS = { + interval: 5000, + pause: 'hover', + wrap: true, + keyboard: true + } + + Carousel.prototype.keydown = function (e) { + switch (e.which) { + case 37: this.prev(); break + case 39: this.next(); break + default: return + } + + e.preventDefault() + } + + Carousel.prototype.cycle = function (e) { + e || (this.paused = false) + + this.interval && clearInterval(this.interval) + + this.options.interval + && !this.paused + && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) + + return this + } + + Carousel.prototype.getItemIndex = function (item) { + this.$items = item.parent().children('.item') + return this.$items.index(item || this.$active) + } + + Carousel.prototype.getItemForDirection = function (direction, active) { + var delta = direction == 'prev' ? -1 : 1 + var activeIndex = this.getItemIndex(active) + var itemIndex = (activeIndex + delta) % this.$items.length + return this.$items.eq(itemIndex) + } + + Carousel.prototype.to = function (pos) { + var that = this + var activeIndex = this.getItemIndex(this.$active = this.$element.find('.item.active')) + + if (pos > (this.$items.length - 1) || pos < 0) return + + if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) // yes, "slid" + if (activeIndex == pos) return this.pause().cycle() + + return this.slide(pos > activeIndex ? 'next' : 'prev', this.$items.eq(pos)) + } + + Carousel.prototype.pause = function (e) { + e || (this.paused = true) + + if (this.$element.find('.next, .prev').length && $.support.transition) { + this.$element.trigger($.support.transition.end) + this.cycle(true) + } + + this.interval = clearInterval(this.interval) + + return this + } + + Carousel.prototype.next = function () { + if (this.sliding) return + return this.slide('next') + } + + Carousel.prototype.prev = function () { + if (this.sliding) return + return this.slide('prev') + } + + Carousel.prototype.slide = function (type, next) { + var $active = this.$element.find('.item.active') + var $next = next || this.getItemForDirection(type, $active) + var isCycling = this.interval + var direction = type == 'next' ? 'left' : 'right' + var fallback = type == 'next' ? 'first' : 'last' + var that = this + + if (!$next.length) { + if (!this.options.wrap) return + $next = this.$element.find('.item')[fallback]() + } + + if ($next.hasClass('active')) return (this.sliding = false) + + var relatedTarget = $next[0] + var slideEvent = $.Event('slide.bs.carousel', { + relatedTarget: relatedTarget, + direction: direction + }) + this.$element.trigger(slideEvent) + if (slideEvent.isDefaultPrevented()) return + + this.sliding = true + + isCycling && this.pause() + + if (this.$indicators.length) { + this.$indicators.find('.active').removeClass('active') + var $nextIndicator = $(this.$indicators.children()[this.getItemIndex($next)]) + $nextIndicator && $nextIndicator.addClass('active') + } + + var slidEvent = $.Event('slid.bs.carousel', { relatedTarget: relatedTarget, direction: direction }) // yes, "slid" + if ($.support.transition && this.$element.hasClass('slide')) { + $next.addClass(type) + $next[0].offsetWidth // force reflow + $active.addClass(direction) + $next.addClass(direction) + $active + .one('bsTransitionEnd', function () { + $next.removeClass([type, direction].join(' ')).addClass('active') + $active.removeClass(['active', direction].join(' ')) + that.sliding = false + setTimeout(function () { + that.$element.trigger(slidEvent) + }, 0) + }) + .emulateTransitionEnd(Carousel.TRANSITION_DURATION) + } else { + $active.removeClass('active') + $next.addClass('active') + this.sliding = false + this.$element.trigger(slidEvent) + } + + isCycling && this.cycle() + + return this + } + + + // CAROUSEL PLUGIN DEFINITION + // ========================== + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.carousel') + var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option) + var action = typeof option == 'string' ? option : options.slide + + if (!data) $this.data('bs.carousel', (data = new Carousel(this, options))) + if (typeof option == 'number') data.to(option) + else if (action) data[action]() + else if (options.interval) data.pause().cycle() + }) + } + + var old = $.fn.carousel + + $.fn.carousel = Plugin + $.fn.carousel.Constructor = Carousel + + + // CAROUSEL NO CONFLICT + // ==================== + + $.fn.carousel.noConflict = function () { + $.fn.carousel = old + return this + } + + + // CAROUSEL DATA-API + // ================= + + var clickHandler = function (e) { + var href + var $this = $(this) + var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7 + if (!$target.hasClass('carousel')) return + var options = $.extend({}, $target.data(), $this.data()) + var slideIndex = $this.attr('data-slide-to') + if (slideIndex) options.interval = false + + Plugin.call($target, options) + + if (slideIndex) { + $target.data('bs.carousel').to(slideIndex) + } + + e.preventDefault() + } + + $(document) + .on('click.bs.carousel.data-api', '[data-slide]', clickHandler) + .on('click.bs.carousel.data-api', '[data-slide-to]', clickHandler) + + $(window).on('load', function () { + $('[data-ride="carousel"]').each(function () { + var $carousel = $(this) + Plugin.call($carousel, $carousel.data()) + }) + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: collapse.js v3.3.0 + * http://getbootstrap.com/javascript/#collapse + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // COLLAPSE PUBLIC CLASS DEFINITION + // ================================ + + var Collapse = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, Collapse.DEFAULTS, options) + this.$trigger = $(this.options.trigger).filter('[href="#' + element.id + '"], [data-target="#' + element.id + '"]') + this.transitioning = null + + if (this.options.parent) { + this.$parent = this.getParent() + } else { + this.addAriaAndCollapsedClass(this.$element, this.$trigger) + } + + if (this.options.toggle) this.toggle() + } + + Collapse.VERSION = '3.3.0' + + Collapse.TRANSITION_DURATION = 350 + + Collapse.DEFAULTS = { + toggle: true, + trigger: '[data-toggle="collapse"]' + } + + Collapse.prototype.dimension = function () { + var hasWidth = this.$element.hasClass('width') + return hasWidth ? 'width' : 'height' + } + + Collapse.prototype.show = function () { + if (this.transitioning || this.$element.hasClass('in')) return + + var activesData + var actives = this.$parent && this.$parent.find('> .panel').children('.in, .collapsing') + + if (actives && actives.length) { + activesData = actives.data('bs.collapse') + if (activesData && activesData.transitioning) return + } + + var startEvent = $.Event('show.bs.collapse') + this.$element.trigger(startEvent) + if (startEvent.isDefaultPrevented()) return + + if (actives && actives.length) { + Plugin.call(actives, 'hide') + activesData || actives.data('bs.collapse', null) + } + + var dimension = this.dimension() + + this.$element + .removeClass('collapse') + .addClass('collapsing')[dimension](0) + .attr('aria-expanded', true) + + this.$trigger + .removeClass('collapsed') + .attr('aria-expanded', true) + + this.transitioning = 1 + + var complete = function () { + this.$element + .removeClass('collapsing') + .addClass('collapse in')[dimension]('') + this.transitioning = 0 + this.$element + .trigger('shown.bs.collapse') + } + + if (!$.support.transition) return complete.call(this) + + var scrollSize = $.camelCase(['scroll', dimension].join('-')) + + this.$element + .one('bsTransitionEnd', $.proxy(complete, this)) + .emulateTransitionEnd(Collapse.TRANSITION_DURATION)[dimension](this.$element[0][scrollSize]) + } + + Collapse.prototype.hide = function () { + if (this.transitioning || !this.$element.hasClass('in')) return + + var startEvent = $.Event('hide.bs.collapse') + this.$element.trigger(startEvent) + if (startEvent.isDefaultPrevented()) return + + var dimension = this.dimension() + + this.$element[dimension](this.$element[dimension]())[0].offsetHeight + + this.$element + .addClass('collapsing') + .removeClass('collapse in') + .attr('aria-expanded', false) + + this.$trigger + .addClass('collapsed') + .attr('aria-expanded', false) + + this.transitioning = 1 + + var complete = function () { + this.transitioning = 0 + this.$element + .removeClass('collapsing') + .addClass('collapse') + .trigger('hidden.bs.collapse') + } + + if (!$.support.transition) return complete.call(this) + + this.$element + [dimension](0) + .one('bsTransitionEnd', $.proxy(complete, this)) + .emulateTransitionEnd(Collapse.TRANSITION_DURATION) + } + + Collapse.prototype.toggle = function () { + this[this.$element.hasClass('in') ? 'hide' : 'show']() + } + + Collapse.prototype.getParent = function () { + return $(this.options.parent) + .find('[data-toggle="collapse"][data-parent="' + this.options.parent + '"]') + .each($.proxy(function (i, element) { + var $element = $(element) + this.addAriaAndCollapsedClass(getTargetFromTrigger($element), $element) + }, this)) + .end() + } + + Collapse.prototype.addAriaAndCollapsedClass = function ($element, $trigger) { + var isOpen = $element.hasClass('in') + + $element.attr('aria-expanded', isOpen) + $trigger + .toggleClass('collapsed', !isOpen) + .attr('aria-expanded', isOpen) + } + + function getTargetFromTrigger($trigger) { + var href + var target = $trigger.attr('data-target') + || (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7 + + return $(target) + } + + + // COLLAPSE PLUGIN DEFINITION + // ========================== + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.collapse') + var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option) + + if (!data && options.toggle && option == 'show') options.toggle = false + if (!data) $this.data('bs.collapse', (data = new Collapse(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + var old = $.fn.collapse + + $.fn.collapse = Plugin + $.fn.collapse.Constructor = Collapse + + + // COLLAPSE NO CONFLICT + // ==================== + + $.fn.collapse.noConflict = function () { + $.fn.collapse = old + return this + } + + + // COLLAPSE DATA-API + // ================= + + $(document).on('click.bs.collapse.data-api', '[data-toggle="collapse"]', function (e) { + var $this = $(this) + + if (!$this.attr('data-target')) e.preventDefault() + + var $target = getTargetFromTrigger($this) + var data = $target.data('bs.collapse') + var option = data ? 'toggle' : $.extend({}, $this.data(), { trigger: this }) + + Plugin.call($target, option) + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: dropdown.js v3.3.0 + * http://getbootstrap.com/javascript/#dropdowns + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // DROPDOWN CLASS DEFINITION + // ========================= + + var backdrop = '.dropdown-backdrop' + var toggle = '[data-toggle="dropdown"]' + var Dropdown = function (element) { + $(element).on('click.bs.dropdown', this.toggle) + } + + Dropdown.VERSION = '3.3.0' + + Dropdown.prototype.toggle = function (e) { + var $this = $(this) + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + clearMenus() + + if (!isActive) { + if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { + // if mobile we use a backdrop because click events don't delegate + $('