├── .gitignore ├── .gitmodules ├── .travis.yml ├── CHANGES.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── dev_requirements.txt ├── docs ├── Makefile ├── conf.py ├── index.rst └── make.bat ├── flask_split ├── __init__.py ├── core.py ├── models.py ├── static │ ├── css │ │ ├── bootstrap.css │ │ ├── bootstrap.min.css │ │ └── dashboard.css │ └── js │ │ ├── bootstrap.js │ │ ├── bootstrap.min.js │ │ └── dashboard.js ├── templates │ └── split │ │ ├── _experiment.html │ │ ├── base.html │ │ └── index.html ├── utils.py └── views.py ├── setup.py ├── tests ├── __init__.py ├── test_dashboard.py ├── test_extension.py └── test_models.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info/ 2 | *.pyc 3 | .tox/ 4 | dist/ 5 | docs/_build/ 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "docs/_themes"] 2 | path = docs/_themes 3 | url = https://github.com/mitsuhiko/flask-sphinx-themes.git 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | dist: xenial 3 | python: 4 | - 2.7 5 | - 3.7 6 | services: 7 | - redis 8 | env: 9 | - FLASK=1.0.2 10 | - FLASK=0.10.1 11 | install: 12 | - pip install -q Flask==$FLASK 13 | - pip install -q -r dev_requirements.txt 14 | - pip install -q -e . 15 | script: 16 | - python setup.py test 17 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | --------- 3 | 4 | Here you can see the full list of changes between each Flask-Split release. 5 | 6 | 0.4.0 (2018-10-14) 7 | ^^^^^^^^^^^^^^^^^^ 8 | 9 | Features 10 | ******** 11 | 12 | - Added support for Python 3.7. Thanks Angus Pearson. 13 | - Switch from HTTP for loading JQuery from Google to protocol-relative URL. Thanks Angus Pearson. 14 | 15 | Bug fixes 16 | ********* 17 | 18 | - Fixed #7: usage of deprecated ``flask.ext`` namespace. 19 | - Fixed usage of deprecated ``flask.Flask.save_session``. 20 | 21 | Breaking changes 22 | **************** 23 | 24 | - Dropped support for Python 2.6. 25 | - Bumped minimum Flask version to 0.10. 26 | - Bumped minimum Redis version to 2.6.0. 27 | 28 | 0.3.0 (2015-07-23) 29 | ^^^^^^^^^^^^^^^^^^ 30 | 31 | - Fixed #3: ``TypeError: set([]) is not JSON serializable`` when tracking a 32 | conversion in Flask 0.10. Thanks Kacper Wikieł and Nick Woodhams. 33 | - Dropped support for Python 2.5. 34 | 35 | 0.2.0 (2012-06-03) 36 | ^^^^^^^^^^^^^^^^^^ 37 | 38 | - Added ``REDIS_URL`` configuration variable for configuring the Redis 39 | connection. 40 | - Fixed properly ``finished`` incrementing alternative's completion count 41 | multiple times, when the test is not reset after it has been finished. The 42 | fix for this issue in the previous release did not work, when the version of 43 | the test was greater than 0. 44 | 45 | 0.1.3 (2012-05-30) 46 | ^^^^^^^^^^^^^^^^^^ 47 | 48 | - Fixed ``finished`` incrementing alternative's completion count multiple 49 | times, when the test is not reset after it has been finished. 50 | 51 | 0.1.2 (2012-03-15) 52 | ^^^^^^^^^^^^^^^^^^ 53 | 54 | - Fixed default value for ``SPLIT_DB_FAILOVER`` not being set. 55 | 56 | 0.1.1 (2012-03-12) 57 | ^^^^^^^^^^^^^^^^^^ 58 | 59 | - Fixed user's participation to an experiment not clearing out from their 60 | session, when experiment version was greater than 0. 61 | - Fixed ``ZeroDivisionError`` in altenative's z-score calculation. 62 | - Fixed conversion rate difference to control rendering. 63 | - More sensible rounding of percentage values in the dashboard. 64 | - Added 90% confidence level. 65 | - Removed a debug print from ``Experiment.find_or_create``. 66 | 67 | 0.1.0 (2012-03-11) 68 | ^^^^^^^^^^^^^^^^^^ 69 | 70 | - Initial public release 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2015 Janne Vanhala 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGES.rst LICENSE README.rst 2 | recursive-include docs * 3 | recursive-exclude docs *.pyc 4 | recursive-include tests * 5 | recursive-exclude tests *.pyc 6 | recursive-include flask_split/templates *.html 7 | recursive-include flask_split/static * 8 | prune docs/_build 9 | exclude docs/_themes/.git 10 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Flask-Split 2 | =========== 3 | 4 | |build status|_ 5 | 6 | .. |build status| image:: https://secure.travis-ci.org/jpvanhal/flask-split.png?branch=master 7 | :alt: Build Status 8 | .. _build status: http://travis-ci.org/jpvanhal/flask-split 9 | 10 | Flask-Split is a Flask extension for `A/B testing`_ your web application. It 11 | is a port of Andrew Nesbitt's excellent `Split`_ A/B testing framework to 12 | Python and Flask. 13 | 14 | .. _A/B testing: http://en.wikipedia.org/wiki/A/B_testing 15 | .. _Split: https://github.com/andrew/split 16 | 17 | 18 | Resources 19 | --------- 20 | 21 | - `Documentation `_ 22 | - `Issue Tracker `_ 23 | - `Code `_ 24 | - `Development Version 25 | `_ 26 | -------------------------------------------------------------------------------- /dev_requirements.txt: -------------------------------------------------------------------------------- 1 | pytest>=2.1,<2.2 2 | flexmock 3 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Flask-Split.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Flask-Split.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Flask-Split" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Flask-Split" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Flask-Split documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Mar 10 23:03:48 2012. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | from flask_split import __version__ 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | #sys.path.insert(0, os.path.abspath('.')) 22 | 23 | # -- General configuration ----------------------------------------------------- 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be extensions 29 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 30 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] 31 | 32 | # Add any paths that contain templates here, relative to this directory. 33 | templates_path = ['_templates'] 34 | 35 | # The suffix of source filenames. 36 | source_suffix = '.rst' 37 | 38 | # The encoding of source files. 39 | #source_encoding = 'utf-8-sig' 40 | 41 | # The master toctree document. 42 | master_doc = 'index' 43 | 44 | # General information about the project. 45 | project = u'Flask-Split' 46 | copyright = u'2012-2015, Janne Vanhala' 47 | 48 | # The version info for the project you're documenting, acts as replacement for 49 | # |version| and |release|, also used in various other places throughout the 50 | # built documents. 51 | # 52 | # The short X.Y version. 53 | version = __version__ 54 | # The full version, including alpha/beta/rc tags. 55 | release = version 56 | 57 | # The language for content autogenerated by Sphinx. Refer to documentation 58 | # for a list of supported languages. 59 | #language = None 60 | 61 | # There are two options for replacing |today|: either, you set today to some 62 | # non-false value, then it is used: 63 | #today = '' 64 | # Else, today_fmt is used as the format for a strftime call. 65 | #today_fmt = '%B %d, %Y' 66 | 67 | # List of patterns, relative to source directory, that match files and 68 | # directories to ignore when looking for source files. 69 | exclude_patterns = ['_build'] 70 | 71 | # The reST default role (used for this markup: `text`) to use for all documents. 72 | #default_role = None 73 | 74 | # If true, '()' will be appended to :func: etc. cross-reference text. 75 | #add_function_parentheses = True 76 | 77 | # If true, the current module name will be prepended to all description 78 | # unit titles (such as .. function::). 79 | #add_module_names = True 80 | 81 | # If true, sectionauthor and moduleauthor directives will be shown in the 82 | # output. They are ignored by default. 83 | #show_authors = False 84 | 85 | # The name of the Pygments (syntax highlighting) style to use. 86 | pygments_style = 'sphinx' 87 | 88 | # A list of ignored prefixes for module index sorting. 89 | #modindex_common_prefix = [] 90 | 91 | 92 | # -- Options for HTML output --------------------------------------------------- 93 | 94 | # The theme to use for HTML and HTML Help pages. See the documentation for 95 | # a list of builtin themes. 96 | html_theme = 'flask_small' 97 | 98 | # Theme options are theme-specific and customize the look and feel of a theme 99 | # further. For a list of options available for each theme, see the 100 | # documentation. 101 | html_theme_options = { 102 | 'index_logo': False, 103 | 'github_fork': 'jpvanhal/flask-split', 104 | } 105 | 106 | # Add any paths that contain custom themes here, relative to this directory. 107 | html_theme_path = ['_themes'] 108 | 109 | # The name for this set of Sphinx documents. If None, it defaults to 110 | # " v documentation". 111 | #html_title = None 112 | 113 | # A shorter title for the navigation bar. Default is the same as html_title. 114 | #html_short_title = None 115 | 116 | # The name of an image file (relative to this directory) to place at the top 117 | # of the sidebar. 118 | #html_logo = None 119 | 120 | # The name of an image file (within the static path) to use as favicon of the 121 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 122 | # pixels large. 123 | #html_favicon = None 124 | 125 | # Add any paths that contain custom static files (such as style sheets) here, 126 | # relative to this directory. They are copied after the builtin static files, 127 | # so a file named "default.css" will overwrite the builtin "default.css". 128 | html_static_path = ['_static'] 129 | 130 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 131 | # using the given strftime format. 132 | #html_last_updated_fmt = '%b %d, %Y' 133 | 134 | # If true, SmartyPants will be used to convert quotes and dashes to 135 | # typographically correct entities. 136 | #html_use_smartypants = True 137 | 138 | # Custom sidebar templates, maps document names to template names. 139 | #html_sidebars = {} 140 | 141 | # Additional templates that should be rendered to pages, maps page names to 142 | # template names. 143 | #html_additional_pages = {} 144 | 145 | # If false, no module index is generated. 146 | #html_domain_indices = True 147 | 148 | # If false, no index is generated. 149 | #html_use_index = True 150 | 151 | # If true, the index is split into individual pages for each letter. 152 | #html_split_index = False 153 | 154 | # If true, links to the reST sources are added to the pages. 155 | #html_show_sourcelink = True 156 | 157 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 158 | #html_show_sphinx = True 159 | 160 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 161 | #html_show_copyright = True 162 | 163 | # If true, an OpenSearch description file will be output, and all pages will 164 | # contain a tag referring to it. The value of this option must be the 165 | # base URL from which the finished HTML is served. 166 | #html_use_opensearch = '' 167 | 168 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 169 | #html_file_suffix = None 170 | 171 | # Output file base name for HTML help builder. 172 | htmlhelp_basename = 'Flask-Splitdoc' 173 | 174 | 175 | # -- Options for LaTeX output -------------------------------------------------- 176 | 177 | latex_elements = { 178 | # The paper size ('letterpaper' or 'a4paper'). 179 | #'papersize': 'letterpaper', 180 | 181 | # The font size ('10pt', '11pt' or '12pt'). 182 | #'pointsize': '10pt', 183 | 184 | # Additional stuff for the LaTeX preamble. 185 | #'preamble': '', 186 | } 187 | 188 | # Grouping the document tree into LaTeX files. List of tuples 189 | # (source start file, target name, title, author, documentclass [howto/manual]). 190 | latex_documents = [ 191 | ('index', 'Flask-Split.tex', u'Flask-Split Documentation', 192 | u'Janne Vanhala', 'manual'), 193 | ] 194 | 195 | # The name of an image file (relative to this directory) to place at the top of 196 | # the title page. 197 | #latex_logo = None 198 | 199 | # For "manual" documents, if this is true, then toplevel headings are parts, 200 | # not chapters. 201 | #latex_use_parts = False 202 | 203 | # If true, show page references after internal links. 204 | #latex_show_pagerefs = False 205 | 206 | # If true, show URL addresses after external links. 207 | #latex_show_urls = False 208 | 209 | # Documents to append as an appendix to all manuals. 210 | #latex_appendices = [] 211 | 212 | # If false, no module index is generated. 213 | #latex_domain_indices = True 214 | 215 | 216 | # -- Options for manual page output -------------------------------------------- 217 | 218 | # One entry per manual page. List of tuples 219 | # (source start file, name, description, authors, manual section). 220 | man_pages = [ 221 | ('index', 'flask-split', u'Flask-Split Documentation', 222 | [u'Janne Vanhala'], 1) 223 | ] 224 | 225 | # If true, show URL addresses after external links. 226 | #man_show_urls = False 227 | 228 | 229 | # -- Options for Texinfo output ------------------------------------------------ 230 | 231 | # Grouping the document tree into Texinfo files. List of tuples 232 | # (source start file, target name, title, author, 233 | # dir menu entry, description, category) 234 | texinfo_documents = [ 235 | ('index', 'Flask-Split', u'Flask-Split Documentation', 236 | u'Janne Vanhala', 'Flask-Split', 'One line description of project.', 237 | 'Miscellaneous'), 238 | ] 239 | 240 | # Documents to append as an appendix to all manuals. 241 | #texinfo_appendices = [] 242 | 243 | # If false, no module index is generated. 244 | #texinfo_domain_indices = True 245 | 246 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 247 | #texinfo_show_urls = 'footnote' 248 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Flask-Split 2 | =========== 3 | 4 | Flask-Split is a Flask extension for `A/B testing`_ your web application. It 5 | is a port of Andrew Nesbitt's excellent `Split`_ A/B testing framework to 6 | Python and Flask. 7 | 8 | .. _A/B testing: http://en.wikipedia.org/wiki/A/B_testing 9 | .. _Split: https://github.com/andrew/split 10 | 11 | 12 | Installation 13 | ------------ 14 | 15 | The easiest way to install Flask-Split is with pip:: 16 | 17 | pip install Flask-Split 18 | 19 | You will also need Redis as Flask-Split uses it as a datastore. Flask-Split 20 | only supports Redis 2.0 or greater. 21 | 22 | In case you are on OS X, the easiest way to install Redis is with Homebrew:: 23 | 24 | brew install redis 25 | 26 | If you are on Ubuntu or other Debian-based Linux, you can install Redis with 27 | APT:: 28 | 29 | sudo apt-get install redis-server 30 | 31 | 32 | Quickstart 33 | ---------- 34 | 35 | In order to start using Flask-Split, you need to first register the Flask-Split 36 | blueprint to your Flask application:: 37 | 38 | from flask import Flask 39 | from flask_split import split 40 | 41 | app = Flask(__name__) 42 | app.register_blueprint(split) 43 | 44 | After that you can start A/B testing your application. 45 | 46 | Defining an A/B test 47 | ^^^^^^^^^^^^^^^^^^^^ 48 | 49 | You can define experiments with the :func:`ab_test` function in a view or a 50 | template. For example, in a template you can define an experiment like so: 51 | 52 | .. sourcecode:: html+jinja 53 | 54 | 57 | 58 | This will set up a new experiment called `signup_btn_text` with two 59 | alternatives: `Register` and `Sign up`. The first alternative is the control. 60 | It should be the original text that was already on the page and the text you 61 | test new alternative against. You should not add only new alternatives as then 62 | you won't be able to tell if you have improved over the original or not. 63 | 64 | Tracking conversions 65 | ^^^^^^^^^^^^^^^^^^^^ 66 | 67 | To measure how the alternative has impacted the conversion rate of your 68 | experiment you need to mark a visitor reaching the conversion point. You can 69 | do this with the :func:`finished` function:: 70 | 71 | finished('signup_btn_text') 72 | 73 | You should place this in a view, for example after a user has completed the 74 | sign up process. 75 | 76 | Configuration 77 | ------------- 78 | 79 | The following configuration values exist for Flask-Split. Flask-Split loads 80 | these values from your main Flask config which can be populated in various 81 | ways. 82 | 83 | A list of configuration keys currently understood by the extension: 84 | 85 | ``REDIS_URL`` 86 | The database URL that should be used for the Redis connection. Defaults to 87 | ``'redis://localhost:6379'``. 88 | 89 | ``SPLIT_ALLOW_MULTIPLE_EXPERIMENTS`` 90 | If set to `True` Flask-Split will allow users to participate in multiple 91 | experiments. 92 | 93 | If set to `False` Flask-Split will avoid users participating in multiple 94 | experiments at once. This means you are less likely to skew results by 95 | adding in more variation to your tests. 96 | 97 | Defaults to `False`. 98 | 99 | ``SPLIT_IGNORE_IP_ADDRESSES`` 100 | Specifies a list of IP addresses to ignore visits from. You may wish to 101 | use this to prevent yourself or people from your office from skewing the 102 | results. 103 | 104 | Defaults to ``[]``, i.e. no IP addresses are ignored by default. 105 | 106 | ``SPLIT_ROBOT_REGEX`` 107 | Flask-Split ignores visitors that appear to be robots or spider in order to 108 | avoid them from skeweing any results. Flask-Split detects robots and 109 | spiders by comparing the user agent of each request with the regular 110 | expression in this setting. 111 | 112 | Defaults to:: 113 | 114 | r""" 115 | (?:i)\b( 116 | Baidu| 117 | Gigabot| 118 | Googlebot| 119 | libwww-perl| 120 | lwp-trivial| 121 | msnbot| 122 | SiteUptime| 123 | Slurp| 124 | WordPress| 125 | ZIBB| 126 | ZyBorg 127 | )\b 128 | """ 129 | 130 | ``SPLIT_DB_FAILOVER`` 131 | If set to `True` Flask-Split will not let :meth:`ab_test` or 132 | :meth:`finished` to crash in case of a Redis connection error. In that 133 | case :meth:`ab_test` always delivers the first alternative i.e. the 134 | control. 135 | 136 | Defaults to `True`. 137 | 138 | 139 | Web Interface 140 | ------------- 141 | 142 | Flask-Split comes with a web frontend to get an overview of how your 143 | experiments are doing. You can find the web interface from the address 144 | ``/split/``. 145 | 146 | If you would like to restrict the access to the web interface, you can take 147 | advantage of blueprint's hooks:: 148 | 149 | from flask import abort 150 | from flask_split import split 151 | 152 | @split.before_request 153 | def require_login(): 154 | if not user_logged_in(): 155 | abort(401) 156 | 157 | 158 | API reference 159 | ------------- 160 | 161 | .. module:: flask_split 162 | 163 | This part of the documentation covers all the public classes and functions 164 | in Flask-Split. 165 | 166 | .. autofunction:: ab_test 167 | .. autofunction:: finished 168 | 169 | 170 | .. include:: ../CHANGES.rst 171 | 172 | 173 | License 174 | ------- 175 | 176 | .. include:: ../LICENSE 177 | 178 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Flask-Split.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Flask-Split.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /flask_split/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | flask_split 4 | ~~~~~~~~~~~ 5 | 6 | A/B testing for your Flask application. 7 | 8 | :copyright: (c) 2012-2015 by Janne Vanhala. 9 | :license: MIT, see LICENSE for more details. 10 | """ 11 | 12 | from .core import ab_test, finished 13 | from .views import split 14 | 15 | 16 | __all__ = (ab_test, finished, split) 17 | 18 | 19 | try: 20 | __version__ = __import__('pkg_resources')\ 21 | .get_distribution('flask_split').version 22 | except Exception: 23 | __version__ = 'unknown' 24 | -------------------------------------------------------------------------------- /flask_split/core.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | flask_split.core 4 | ~~~~~~~~~~~~~~~~ 5 | 6 | Implements the core functionality for doing A/B tests. 7 | 8 | :copyright: (c) 2012-2015 by Janne Vanhala. 9 | :license: MIT, see LICENSE for more details. 10 | """ 11 | 12 | import re 13 | 14 | from flask import current_app, request, session 15 | from redis import ConnectionError 16 | 17 | from .models import Alternative, Experiment 18 | from .utils import _get_redis_connection 19 | from .views import split 20 | 21 | 22 | @split.record 23 | def init_app(state): 24 | """ 25 | Prepare the Flask application for Flask-Split. 26 | 27 | :param state: :class:`BlueprintSetupState` instance 28 | """ 29 | app = state.app 30 | 31 | app.config.setdefault('SPLIT_ALLOW_MULTIPLE_EXPERIMENTS', False) 32 | app.config.setdefault('SPLIT_DB_FAILOVER', False) 33 | app.config.setdefault('SPLIT_IGNORE_IP_ADDRESSES', []) 34 | app.config.setdefault('SPLIT_ROBOT_REGEX', r""" 35 | (?i)\b( 36 | Baidu| 37 | Gigabot| 38 | Googlebot| 39 | libwww-perl| 40 | lwp-trivial| 41 | msnbot| 42 | SiteUptime| 43 | Slurp| 44 | WordPress| 45 | ZIBB| 46 | ZyBorg 47 | )\b 48 | """) 49 | 50 | app.jinja_env.globals.update({ 51 | 'ab_test': ab_test, 52 | 'finished': finished 53 | }) 54 | 55 | @app.template_filter() 56 | def percentage(number): 57 | number *= 100 58 | if abs(number) < 10: 59 | return "%.1f%%" % round(number, 1) 60 | else: 61 | return "%d%%" % round(number) 62 | 63 | 64 | def ab_test(experiment_name, *alternatives): 65 | """ 66 | Start a new A/B test. 67 | 68 | Returns one of the alternatives. If the user has already seen the test, 69 | they will get the same alternative as before. 70 | 71 | :param experiment_name: Name of the experiment. You should never use the 72 | same experiment name to refer to a second experiment. 73 | :param alternatives: A list of alternatives. Each item can be either a 74 | string or a two-tuple of the form (alternative name, weight). By 75 | default each alternative has the weight of 1. The first alternative 76 | is the control. Every experiment must have at least two alternatives. 77 | """ 78 | redis = _get_redis_connection() 79 | try: 80 | experiment = Experiment.find_or_create( 81 | redis, experiment_name, *alternatives) 82 | if experiment.winner: 83 | return experiment.winner.name 84 | else: 85 | forced_alternative = _override( 86 | experiment.name, experiment.alternative_names) 87 | if forced_alternative: 88 | return forced_alternative 89 | _clean_old_versions(experiment) 90 | if (_exclude_visitor() or 91 | _not_allowed_to_test(experiment.key)): 92 | _begin_experiment(experiment) 93 | 94 | alternative_name = _get_session().get(experiment.key) 95 | if alternative_name: 96 | return alternative_name 97 | alternative = experiment.next_alternative() 98 | alternative.increment_participation() 99 | _begin_experiment(experiment, alternative.name) 100 | return alternative.name 101 | except ConnectionError: 102 | if not current_app.config['SPLIT_DB_FAILOVER']: 103 | raise 104 | control = alternatives[0] 105 | return control[0] if isinstance(control, tuple) else control 106 | 107 | 108 | def finished(experiment_name, reset=True): 109 | """ 110 | Track a conversion. 111 | 112 | :param experiment_name: Name of the experiment. 113 | :param reset: If set to `True` current user's session is reset so that they 114 | may start the test again in the future. If set to `False` the user 115 | will always see the alternative they started with. Defaults to `True`. 116 | """ 117 | if _exclude_visitor(): 118 | return 119 | redis = _get_redis_connection() 120 | try: 121 | experiment = Experiment.find(redis, experiment_name) 122 | if not experiment: 123 | return 124 | alternative_name = _get_session().get(experiment.key) 125 | if alternative_name: 126 | split_finished = set(session.get('split_finished', [])) 127 | if experiment.key not in split_finished: 128 | alternative = Alternative( 129 | redis, alternative_name, experiment_name) 130 | alternative.increment_completion() 131 | if reset: 132 | _get_session().pop(experiment.key, None) 133 | try: 134 | split_finished.remove(experiment.key) 135 | except KeyError: 136 | pass 137 | else: 138 | split_finished.add(experiment.key) 139 | session['split_finished'] = list(split_finished) 140 | except ConnectionError: 141 | if not current_app.config['SPLIT_DB_FAILOVER']: 142 | raise 143 | 144 | 145 | def _override(experiment_name, alternatives): 146 | if request.args.get(experiment_name) in alternatives: 147 | return request.args.get(experiment_name) 148 | 149 | 150 | def _begin_experiment(experiment, alternative_name=None): 151 | if not alternative_name: 152 | alternative_name = experiment.control.name 153 | _get_session()[experiment.key] = alternative_name 154 | session.modified = True 155 | 156 | 157 | def _get_session(): 158 | if 'split' not in session: 159 | session['split'] = {} 160 | return session['split'] 161 | 162 | 163 | def _exclude_visitor(): 164 | """ 165 | Return `True` if the current visitor should be excluded from participating 166 | to the A/B test, or `False` otherwise. 167 | """ 168 | return _is_robot() or _is_ignored_ip_address() 169 | 170 | 171 | def _not_allowed_to_test(experiment_key): 172 | return ( 173 | not current_app.config['SPLIT_ALLOW_MULTIPLE_EXPERIMENTS'] and 174 | _doing_other_tests(experiment_key) 175 | ) 176 | 177 | 178 | def _doing_other_tests(experiment_key): 179 | """ 180 | Return `True` if the current user is doing other experiments than the 181 | experiment with the key ``experiment_key`` at the moment, or `False` 182 | otherwise. 183 | """ 184 | for key in _get_session(): 185 | if key != experiment_key: 186 | return True 187 | return False 188 | 189 | 190 | def _clean_old_versions(experiment): 191 | for old_key in _old_versions(experiment): 192 | del _get_session()[old_key] 193 | session.modified = True 194 | 195 | 196 | def _old_versions(experiment): 197 | if experiment.version > 0: 198 | return [ 199 | key for key in _get_session() 200 | if key.startswith(experiment.name) and key != experiment.key 201 | ] 202 | else: 203 | return [] 204 | 205 | 206 | def _is_robot(): 207 | """ 208 | Return `True` if the current visitor is a robot or spider, or 209 | `False` otherwise. 210 | 211 | This function works by comparing the request's user agent with a regular 212 | expression. The regular expression can be configured with the 213 | ``SPLIT_ROBOT_REGEX`` setting. 214 | """ 215 | robot_regex = current_app.config['SPLIT_ROBOT_REGEX'] 216 | user_agent = request.headers.get('User-Agent', '') 217 | return re.search(robot_regex, user_agent, flags=re.VERBOSE) 218 | 219 | 220 | def _is_ignored_ip_address(): 221 | """ 222 | Return `True` if the IP address of the current visitor should be 223 | ignored, or `False` otherwise. 224 | 225 | The list of ignored IP addresses can be configured with the 226 | ``SPLIT_IGNORE_IP_ADDRESSES`` setting. 227 | """ 228 | ignore_ip_addresses = current_app.config['SPLIT_IGNORE_IP_ADDRESSES'] 229 | return request.remote_addr in ignore_ip_addresses 230 | -------------------------------------------------------------------------------- /flask_split/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | flask_split.models 4 | ~~~~~~~~~~~~~~~~~~ 5 | 6 | This module provides the models for experiments and alternatives. 7 | 8 | :copyright: (c) 2012-2015 by Janne Vanhala. 9 | :license: MIT, see LICENSE for more details. 10 | """ 11 | 12 | from datetime import datetime 13 | from math import sqrt 14 | from random import random 15 | 16 | 17 | class Alternative(object): 18 | def __init__(self, redis, name, experiment_name): 19 | self.redis = redis 20 | self.experiment_name = experiment_name 21 | if isinstance(name, tuple): 22 | self.name, self.weight = name 23 | else: 24 | self.name = name 25 | self.weight = 1 26 | 27 | def _get_participant_count(self): 28 | return int(self.redis.hget(self.key, 'participant_count') or 0) 29 | 30 | def _set_participant_count(self, count): 31 | self.redis.hset(self.key, 'participant_count', int(count)) 32 | 33 | participant_count = property( 34 | _get_participant_count, 35 | _set_participant_count 36 | ) 37 | 38 | def _get_completed_count(self): 39 | return int(self.redis.hget(self.key, 'completed_count') or 0) 40 | 41 | def _set_completed_count(self, count): 42 | self.redis.hset(self.key, 'completed_count', int(count)) 43 | 44 | completed_count = property( 45 | _get_completed_count, 46 | _set_completed_count 47 | ) 48 | 49 | def increment_participation(self): 50 | self.redis.hincrby(self.key, 'participant_count', 1) 51 | 52 | def increment_completion(self): 53 | self.redis.hincrby(self.key, 'completed_count', 1) 54 | 55 | @property 56 | def is_control(self): 57 | return self.experiment.control.name == self.name 58 | 59 | @property 60 | def conversion_rate(self): 61 | if self.participant_count == 0: 62 | return 0 63 | return float(self.completed_count) / float(self.participant_count) 64 | 65 | @property 66 | def experiment(self): 67 | return Experiment.find(self.redis, self.experiment_name) 68 | 69 | def save(self): 70 | self.redis.hsetnx(self.key, 'participant_count', 0) 71 | self.redis.hsetnx(self.key, 'completed_count', 0) 72 | 73 | def reset(self): 74 | self.redis.hmset(self.key, { 75 | 'participant_count': 0, 76 | 'completed_count': 0 77 | }) 78 | 79 | def delete(self): 80 | self.redis.delete(self.key) 81 | 82 | @property 83 | def key(self): 84 | return '%s:%s' % (self.experiment_name, self.name) 85 | 86 | @property 87 | def z_score(self): 88 | control = self.experiment.control 89 | alternative = self 90 | 91 | if control.name == alternative.name: 92 | return None 93 | 94 | cr = alternative.conversion_rate 95 | crc = control.conversion_rate 96 | 97 | n = alternative.participant_count 98 | nc = control.participant_count 99 | 100 | if n == 0 or nc == 0: 101 | return None 102 | 103 | mean = cr - crc 104 | var_cr = cr * (1 - cr) / float(n) 105 | var_crc = crc * (1 - crc) / float(nc) 106 | 107 | if var_cr + var_crc == 0: 108 | return None 109 | 110 | return mean / sqrt(var_cr + var_crc) 111 | 112 | @property 113 | def confidence_level(self): 114 | z = self.z_score 115 | if z is None: 116 | return 'N/A' 117 | z = abs(round(z, 3)) 118 | if z == 0: 119 | return 'no change' 120 | elif z < 1.64: 121 | return 'no confidence' 122 | elif z < 1.96: 123 | return '90% confidence' 124 | elif z < 2.57: 125 | return '95% confidence' 126 | elif z < 3.29: 127 | return '99% confidence' 128 | else: 129 | return '99.9% confidence' 130 | 131 | 132 | class Experiment(object): 133 | def __init__(self, redis, name, *alternative_names): 134 | self.redis = redis 135 | self.name = name 136 | self.alternatives = [ 137 | Alternative(redis, alternative, name) 138 | for alternative in alternative_names 139 | ] 140 | 141 | @property 142 | def control(self): 143 | return self.alternatives[0] 144 | 145 | def _get_winner(self): 146 | winner = self.redis.hget('experiment_winner', self.name) 147 | if winner: 148 | return Alternative(self.redis, winner, self.name) 149 | 150 | def _set_winner(self, winner_name): 151 | self.redis.hset('experiment_winner', self.name, winner_name) 152 | 153 | winner = property( 154 | _get_winner, 155 | _set_winner 156 | ) 157 | 158 | def reset_winner(self): 159 | """Reset the winner of this experiment.""" 160 | self.redis.hdel('experiment_winner', self.name) 161 | 162 | @property 163 | def start_time(self): 164 | """The start time of this experiment.""" 165 | t = self.redis.hget('experiment_start_times', self.name) 166 | if t: 167 | return datetime.strptime(t, '%Y-%m-%dT%H:%M:%S') 168 | 169 | @property 170 | def total_participants(self): 171 | """The total number of participants in this experiment.""" 172 | return sum(a.participant_count for a in self.alternatives) 173 | 174 | @property 175 | def total_completed(self): 176 | """The total number of users who completed this experiment.""" 177 | return sum(a.completed_count for a in self.alternatives) 178 | 179 | @property 180 | def alternative_names(self): 181 | """A list of alternative names. in this experiment.""" 182 | return [alternative.name for alternative in self.alternatives] 183 | 184 | def next_alternative(self): 185 | """Return the winner of the experiment if set, or a random 186 | alternative.""" 187 | return self.winner or self.random_alternative() 188 | 189 | def random_alternative(self): 190 | total = sum(alternative.weight for alternative in self.alternatives) 191 | point = random() * total 192 | for alternative in self.alternatives: 193 | if alternative.weight >= point: 194 | return alternative 195 | point -= alternative.weight 196 | 197 | @property 198 | def version(self): 199 | return int(self.redis.get('%s:version' % self.name) or 0) 200 | 201 | def increment_version(self): 202 | self.redis.incr('%s:version' % self.name) 203 | 204 | @property 205 | def key(self): 206 | if self.version > 0: 207 | return "%s:%s" % (self.name, self.version) 208 | else: 209 | return self.name 210 | 211 | def reset(self): 212 | """Delete all data for this experiment.""" 213 | for alternative in self.alternatives: 214 | alternative.reset() 215 | self.reset_winner() 216 | self.increment_version() 217 | 218 | def delete(self): 219 | """Delete this experiment and all its data.""" 220 | for alternative in self.alternatives: 221 | alternative.delete() 222 | self.reset_winner() 223 | self.redis.srem('experiments', self.name) 224 | self.redis.delete(self.name) 225 | self.increment_version() 226 | 227 | @property 228 | def is_new_record(self): 229 | return self.name not in self.redis 230 | 231 | def save(self): 232 | if self.is_new_record: 233 | start_time = self._get_time().isoformat()[:19] 234 | self.redis.sadd('experiments', self.name) 235 | self.redis.hset('experiment_start_times', self.name, start_time) 236 | for alternative in reversed(self.alternatives): 237 | self.redis.lpush(self.name, alternative.name) 238 | 239 | @classmethod 240 | def load_alternatives_for(cls, redis, name): 241 | return redis.lrange(name, 0, -1) 242 | 243 | @classmethod 244 | def all(cls, redis): 245 | return [cls.find(redis, e) for e in redis.smembers('experiments')] 246 | 247 | @classmethod 248 | def find(cls, redis, name): 249 | if name in redis: 250 | return cls(redis, name, *cls.load_alternatives_for(redis, name)) 251 | 252 | @classmethod 253 | def find_or_create(cls, redis, key, *alternatives): 254 | name = key.split(':')[0] 255 | 256 | if len(alternatives) < 2: 257 | raise TypeError('You must declare at least 2 alternatives.') 258 | 259 | experiment = cls.find(redis, name) 260 | if experiment: 261 | alts = [a[0] if isinstance(a, tuple) else a for a in alternatives] 262 | if [a.name for a in experiment.alternatives] != alts: 263 | experiment.reset() 264 | for alternative in experiment.alternatives: 265 | alternative.delete() 266 | experiment = cls(redis, name, *alternatives) 267 | experiment.save() 268 | else: 269 | experiment = cls(redis, name, *alternatives) 270 | experiment.save() 271 | return experiment 272 | 273 | def _get_time(self): 274 | return datetime.now() 275 | -------------------------------------------------------------------------------- /flask_split/static/css/bootstrap.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v2.0.1 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */ 10 | .clearfix { 11 | *zoom: 1; 12 | } 13 | .clearfix:before, .clearfix:after { 14 | display: table; 15 | content: ""; 16 | } 17 | .clearfix:after { 18 | clear: both; 19 | } 20 | article, 21 | aside, 22 | details, 23 | figcaption, 24 | figure, 25 | footer, 26 | header, 27 | hgroup, 28 | nav, 29 | section { 30 | display: block; 31 | } 32 | audio, canvas, video { 33 | display: inline-block; 34 | *display: inline; 35 | *zoom: 1; 36 | } 37 | audio:not([controls]) { 38 | display: none; 39 | } 40 | html { 41 | font-size: 100%; 42 | -webkit-text-size-adjust: 100%; 43 | -ms-text-size-adjust: 100%; 44 | } 45 | a:focus { 46 | outline: thin dotted #333; 47 | outline: 5px auto -webkit-focus-ring-color; 48 | outline-offset: -2px; 49 | } 50 | a:hover, a:active { 51 | outline: 0; 52 | } 53 | sub, sup { 54 | position: relative; 55 | font-size: 75%; 56 | line-height: 0; 57 | vertical-align: baseline; 58 | } 59 | sup { 60 | top: -0.5em; 61 | } 62 | sub { 63 | bottom: -0.25em; 64 | } 65 | img { 66 | max-width: 100%; 67 | height: auto; 68 | border: 0; 69 | -ms-interpolation-mode: bicubic; 70 | } 71 | button, 72 | input, 73 | select, 74 | textarea { 75 | margin: 0; 76 | font-size: 100%; 77 | vertical-align: middle; 78 | } 79 | button, input { 80 | *overflow: visible; 81 | line-height: normal; 82 | } 83 | button::-moz-focus-inner, input::-moz-focus-inner { 84 | padding: 0; 85 | border: 0; 86 | } 87 | button, 88 | input[type="button"], 89 | input[type="reset"], 90 | input[type="submit"] { 91 | cursor: pointer; 92 | -webkit-appearance: button; 93 | } 94 | input[type="search"] { 95 | -webkit-appearance: textfield; 96 | -webkit-box-sizing: content-box; 97 | -moz-box-sizing: content-box; 98 | box-sizing: content-box; 99 | } 100 | input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button { 101 | -webkit-appearance: none; 102 | } 103 | textarea { 104 | overflow: auto; 105 | vertical-align: top; 106 | } 107 | body { 108 | margin: 0; 109 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 110 | font-size: 13px; 111 | line-height: 18px; 112 | color: #333333; 113 | background-color: #ffffff; 114 | } 115 | a { 116 | color: #0088cc; 117 | text-decoration: none; 118 | } 119 | a:hover { 120 | color: #005580; 121 | text-decoration: underline; 122 | } 123 | .container { 124 | width: 940px; 125 | margin-left: auto; 126 | margin-right: auto; 127 | *zoom: 1; 128 | } 129 | .container:before, .container:after { 130 | display: table; 131 | content: ""; 132 | } 133 | .container:after { 134 | clear: both; 135 | } 136 | .container-fluid { 137 | padding-left: 20px; 138 | padding-right: 20px; 139 | *zoom: 1; 140 | } 141 | .container-fluid:before, .container-fluid:after { 142 | display: table; 143 | content: ""; 144 | } 145 | .container-fluid:after { 146 | clear: both; 147 | } 148 | p { 149 | margin: 0 0 9px; 150 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 151 | font-size: 13px; 152 | line-height: 18px; 153 | } 154 | p small { 155 | font-size: 11px; 156 | color: #999999; 157 | } 158 | .lead { 159 | margin-bottom: 18px; 160 | font-size: 20px; 161 | font-weight: 200; 162 | line-height: 27px; 163 | } 164 | h1, 165 | h2, 166 | h3, 167 | h4, 168 | h5, 169 | h6 { 170 | margin: 0; 171 | font-weight: bold; 172 | color: #333333; 173 | text-rendering: optimizelegibility; 174 | } 175 | h1 small, 176 | h2 small, 177 | h3 small, 178 | h4 small, 179 | h5 small, 180 | h6 small { 181 | font-weight: normal; 182 | color: #999999; 183 | } 184 | h1 { 185 | font-size: 30px; 186 | line-height: 36px; 187 | } 188 | h1 small { 189 | font-size: 18px; 190 | } 191 | h2 { 192 | font-size: 24px; 193 | line-height: 36px; 194 | } 195 | h2 small { 196 | font-size: 18px; 197 | } 198 | h3 { 199 | line-height: 27px; 200 | font-size: 18px; 201 | } 202 | h3 small { 203 | font-size: 14px; 204 | } 205 | h4, h5, h6 { 206 | line-height: 18px; 207 | } 208 | h4 { 209 | font-size: 14px; 210 | } 211 | h4 small { 212 | font-size: 12px; 213 | } 214 | h5 { 215 | font-size: 12px; 216 | } 217 | h6 { 218 | font-size: 11px; 219 | color: #999999; 220 | text-transform: uppercase; 221 | } 222 | .page-header { 223 | padding-bottom: 17px; 224 | margin: 18px 0; 225 | border-bottom: 1px solid #eeeeee; 226 | } 227 | .page-header h1 { 228 | line-height: 1; 229 | } 230 | ul, ol { 231 | padding: 0; 232 | margin: 0 0 9px 25px; 233 | } 234 | ul ul, 235 | ul ol, 236 | ol ol, 237 | ol ul { 238 | margin-bottom: 0; 239 | } 240 | ul { 241 | list-style: disc; 242 | } 243 | ol { 244 | list-style: decimal; 245 | } 246 | li { 247 | line-height: 18px; 248 | } 249 | ul.unstyled, ol.unstyled { 250 | margin-left: 0; 251 | list-style: none; 252 | } 253 | dl { 254 | margin-bottom: 18px; 255 | } 256 | dt, dd { 257 | line-height: 18px; 258 | } 259 | dt { 260 | font-weight: bold; 261 | } 262 | dd { 263 | margin-left: 9px; 264 | } 265 | hr { 266 | margin: 18px 0; 267 | border: 0; 268 | border-top: 1px solid #eeeeee; 269 | border-bottom: 1px solid #ffffff; 270 | } 271 | strong { 272 | font-weight: bold; 273 | } 274 | em { 275 | font-style: italic; 276 | } 277 | .muted { 278 | color: #999999; 279 | } 280 | abbr { 281 | font-size: 90%; 282 | text-transform: uppercase; 283 | border-bottom: 1px dotted #ddd; 284 | cursor: help; 285 | } 286 | blockquote { 287 | padding: 0 0 0 15px; 288 | margin: 0 0 18px; 289 | border-left: 5px solid #eeeeee; 290 | } 291 | blockquote p { 292 | margin-bottom: 0; 293 | font-size: 16px; 294 | font-weight: 300; 295 | line-height: 22.5px; 296 | } 297 | blockquote small { 298 | display: block; 299 | line-height: 18px; 300 | color: #999999; 301 | } 302 | blockquote small:before { 303 | content: '\2014 \00A0'; 304 | } 305 | blockquote.pull-right { 306 | float: right; 307 | padding-left: 0; 308 | padding-right: 15px; 309 | border-left: 0; 310 | border-right: 5px solid #eeeeee; 311 | } 312 | blockquote.pull-right p, blockquote.pull-right small { 313 | text-align: right; 314 | } 315 | q:before, 316 | q:after, 317 | blockquote:before, 318 | blockquote:after { 319 | content: ""; 320 | } 321 | address { 322 | display: block; 323 | margin-bottom: 18px; 324 | line-height: 18px; 325 | font-style: normal; 326 | } 327 | small { 328 | font-size: 100%; 329 | } 330 | cite { 331 | font-style: normal; 332 | } 333 | .label { 334 | padding: 2px 4px 3px; 335 | font-size: 11.049999999999999px; 336 | font-weight: bold; 337 | color: #ffffff; 338 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 339 | background-color: #999999; 340 | -webkit-border-radius: 3px; 341 | -moz-border-radius: 3px; 342 | border-radius: 3px; 343 | } 344 | .label:hover { 345 | color: #ffffff; 346 | text-decoration: none; 347 | } 348 | .label-important { 349 | background-color: #b94a48; 350 | } 351 | .label-important:hover { 352 | background-color: #953b39; 353 | } 354 | .label-warning { 355 | background-color: #f89406; 356 | } 357 | .label-warning:hover { 358 | background-color: #c67605; 359 | } 360 | .label-success { 361 | background-color: #468847; 362 | } 363 | .label-success:hover { 364 | background-color: #356635; 365 | } 366 | .label-info { 367 | background-color: #3a87ad; 368 | } 369 | .label-info:hover { 370 | background-color: #2d6987; 371 | } 372 | table { 373 | max-width: 100%; 374 | border-collapse: collapse; 375 | border-spacing: 0; 376 | } 377 | .table { 378 | width: 100%; 379 | margin-bottom: 18px; 380 | } 381 | .table th, .table td { 382 | padding: 8px; 383 | line-height: 18px; 384 | text-align: left; 385 | vertical-align: top; 386 | border-top: 1px solid #ddd; 387 | } 388 | .table th { 389 | font-weight: bold; 390 | } 391 | .table thead th { 392 | vertical-align: bottom; 393 | } 394 | .table thead:first-child tr th, .table thead:first-child tr td { 395 | border-top: 0; 396 | } 397 | .table tbody + tbody { 398 | border-top: 2px solid #ddd; 399 | } 400 | .table-condensed th, .table-condensed td { 401 | padding: 4px 5px; 402 | } 403 | .table-bordered { 404 | border: 1px solid #ddd; 405 | border-collapse: separate; 406 | *border-collapse: collapsed; 407 | -webkit-border-radius: 4px; 408 | -moz-border-radius: 4px; 409 | border-radius: 4px; 410 | } 411 | .table-bordered th + th, 412 | .table-bordered td + td, 413 | .table-bordered th + td, 414 | .table-bordered td + th { 415 | border-left: 1px solid #ddd; 416 | } 417 | .table-bordered thead:first-child tr:first-child th, .table-bordered tbody:first-child tr:first-child th, .table-bordered tbody:first-child tr:first-child td { 418 | border-top: 0; 419 | } 420 | .table-bordered thead:first-child tr:first-child th:first-child, .table-bordered tbody:first-child tr:first-child td:first-child { 421 | -webkit-border-radius: 4px 0 0 0; 422 | -moz-border-radius: 4px 0 0 0; 423 | border-radius: 4px 0 0 0; 424 | } 425 | .table-bordered thead:first-child tr:first-child th:last-child, .table-bordered tbody:first-child tr:first-child td:last-child { 426 | -webkit-border-radius: 0 4px 0 0; 427 | -moz-border-radius: 0 4px 0 0; 428 | border-radius: 0 4px 0 0; 429 | } 430 | .table-bordered thead:last-child tr:last-child th:first-child, .table-bordered tbody:last-child tr:last-child td:first-child { 431 | -webkit-border-radius: 0 0 0 4px; 432 | -moz-border-radius: 0 0 0 4px; 433 | border-radius: 0 0 0 4px; 434 | } 435 | .table-bordered thead:last-child tr:last-child th:last-child, .table-bordered tbody:last-child tr:last-child td:last-child { 436 | -webkit-border-radius: 0 0 4px 0; 437 | -moz-border-radius: 0 0 4px 0; 438 | border-radius: 0 0 4px 0; 439 | } 440 | .table-striped tbody tr:nth-child(odd) td, .table-striped tbody tr:nth-child(odd) th { 441 | background-color: #f9f9f9; 442 | } 443 | .table tbody tr:hover td, .table tbody tr:hover th { 444 | background-color: #f5f5f5; 445 | } 446 | table .span1 { 447 | float: none; 448 | width: 44px; 449 | margin-left: 0; 450 | } 451 | table .span2 { 452 | float: none; 453 | width: 124px; 454 | margin-left: 0; 455 | } 456 | table .span3 { 457 | float: none; 458 | width: 204px; 459 | margin-left: 0; 460 | } 461 | table .span4 { 462 | float: none; 463 | width: 284px; 464 | margin-left: 0; 465 | } 466 | table .span5 { 467 | float: none; 468 | width: 364px; 469 | margin-left: 0; 470 | } 471 | table .span6 { 472 | float: none; 473 | width: 444px; 474 | margin-left: 0; 475 | } 476 | table .span7 { 477 | float: none; 478 | width: 524px; 479 | margin-left: 0; 480 | } 481 | table .span8 { 482 | float: none; 483 | width: 604px; 484 | margin-left: 0; 485 | } 486 | table .span9 { 487 | float: none; 488 | width: 684px; 489 | margin-left: 0; 490 | } 491 | table .span10 { 492 | float: none; 493 | width: 764px; 494 | margin-left: 0; 495 | } 496 | table .span11 { 497 | float: none; 498 | width: 844px; 499 | margin-left: 0; 500 | } 501 | table .span12 { 502 | float: none; 503 | width: 924px; 504 | margin-left: 0; 505 | } 506 | .btn { 507 | display: inline-block; 508 | padding: 4px 10px 4px; 509 | margin-bottom: 0; 510 | font-size: 13px; 511 | line-height: 18px; 512 | color: #333333; 513 | text-align: center; 514 | text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); 515 | vertical-align: middle; 516 | background-color: #f5f5f5; 517 | background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); 518 | background-image: -ms-linear-gradient(top, #ffffff, #e6e6e6); 519 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); 520 | background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); 521 | background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); 522 | background-image: linear-gradient(top, #ffffff, #e6e6e6); 523 | background-repeat: repeat-x; 524 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0); 525 | border-color: #e6e6e6 #e6e6e6 #bfbfbf; 526 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 527 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 528 | border: 1px solid #ccc; 529 | border-bottom-color: #bbb; 530 | -webkit-border-radius: 4px; 531 | -moz-border-radius: 4px; 532 | border-radius: 4px; 533 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); 534 | -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); 535 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); 536 | cursor: pointer; 537 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 538 | *margin-left: .3em; 539 | } 540 | .btn:hover, 541 | .btn:active, 542 | .btn.active, 543 | .btn.disabled, 544 | .btn[disabled] { 545 | background-color: #e6e6e6; 546 | } 547 | .btn:active, .btn.active { 548 | background-color: #cccccc \9; 549 | } 550 | .btn:first-child { 551 | *margin-left: 0; 552 | } 553 | .btn:hover { 554 | color: #333333; 555 | text-decoration: none; 556 | background-color: #e6e6e6; 557 | background-position: 0 -15px; 558 | -webkit-transition: background-position 0.1s linear; 559 | -moz-transition: background-position 0.1s linear; 560 | -ms-transition: background-position 0.1s linear; 561 | -o-transition: background-position 0.1s linear; 562 | transition: background-position 0.1s linear; 563 | } 564 | .btn:focus { 565 | outline: thin dotted #333; 566 | outline: 5px auto -webkit-focus-ring-color; 567 | outline-offset: -2px; 568 | } 569 | .btn.active, .btn:active { 570 | background-image: none; 571 | -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); 572 | -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); 573 | box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); 574 | background-color: #e6e6e6; 575 | background-color: #d9d9d9 \9; 576 | outline: 0; 577 | } 578 | .btn.disabled, .btn[disabled] { 579 | cursor: default; 580 | background-image: none; 581 | background-color: #e6e6e6; 582 | opacity: 0.65; 583 | filter: alpha(opacity=65); 584 | -webkit-box-shadow: none; 585 | -moz-box-shadow: none; 586 | box-shadow: none; 587 | } 588 | .btn-large { 589 | padding: 9px 14px; 590 | font-size: 15px; 591 | line-height: normal; 592 | -webkit-border-radius: 5px; 593 | -moz-border-radius: 5px; 594 | border-radius: 5px; 595 | } 596 | .btn-large [class^="icon-"] { 597 | margin-top: 1px; 598 | } 599 | .btn-small { 600 | padding: 5px 9px; 601 | font-size: 11px; 602 | line-height: 16px; 603 | } 604 | .btn-small [class^="icon-"] { 605 | margin-top: -1px; 606 | } 607 | .btn-mini { 608 | padding: 2px 6px; 609 | font-size: 11px; 610 | line-height: 14px; 611 | } 612 | .btn-primary, 613 | .btn-primary:hover, 614 | .btn-warning, 615 | .btn-warning:hover, 616 | .btn-danger, 617 | .btn-danger:hover, 618 | .btn-success, 619 | .btn-success:hover, 620 | .btn-info, 621 | .btn-info:hover, 622 | .btn-inverse, 623 | .btn-inverse:hover { 624 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 625 | color: #ffffff; 626 | } 627 | .btn-primary.active, 628 | .btn-warning.active, 629 | .btn-danger.active, 630 | .btn-success.active, 631 | .btn-info.active, 632 | .btn-dark.active { 633 | color: rgba(255, 255, 255, 0.75); 634 | } 635 | .btn-primary { 636 | background-color: #006dcc; 637 | background-image: -moz-linear-gradient(top, #0088cc, #0044cc); 638 | background-image: -ms-linear-gradient(top, #0088cc, #0044cc); 639 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc)); 640 | background-image: -webkit-linear-gradient(top, #0088cc, #0044cc); 641 | background-image: -o-linear-gradient(top, #0088cc, #0044cc); 642 | background-image: linear-gradient(top, #0088cc, #0044cc); 643 | background-repeat: repeat-x; 644 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0); 645 | border-color: #0044cc #0044cc #002a80; 646 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 647 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 648 | } 649 | .btn-primary:hover, 650 | .btn-primary:active, 651 | .btn-primary.active, 652 | .btn-primary.disabled, 653 | .btn-primary[disabled] { 654 | background-color: #0044cc; 655 | } 656 | .btn-primary:active, .btn-primary.active { 657 | background-color: #003399 \9; 658 | } 659 | .btn-warning { 660 | background-color: #faa732; 661 | background-image: -moz-linear-gradient(top, #fbb450, #f89406); 662 | background-image: -ms-linear-gradient(top, #fbb450, #f89406); 663 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); 664 | background-image: -webkit-linear-gradient(top, #fbb450, #f89406); 665 | background-image: -o-linear-gradient(top, #fbb450, #f89406); 666 | background-image: linear-gradient(top, #fbb450, #f89406); 667 | background-repeat: repeat-x; 668 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0); 669 | border-color: #f89406 #f89406 #ad6704; 670 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 671 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 672 | } 673 | .btn-warning:hover, 674 | .btn-warning:active, 675 | .btn-warning.active, 676 | .btn-warning.disabled, 677 | .btn-warning[disabled] { 678 | background-color: #f89406; 679 | } 680 | .btn-warning:active, .btn-warning.active { 681 | background-color: #c67605 \9; 682 | } 683 | .btn-danger { 684 | background-color: #da4f49; 685 | background-image: -moz-linear-gradient(top, #ee5f5b, #bd362f); 686 | background-image: -ms-linear-gradient(top, #ee5f5b, #bd362f); 687 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f)); 688 | background-image: -webkit-linear-gradient(top, #ee5f5b, #bd362f); 689 | background-image: -o-linear-gradient(top, #ee5f5b, #bd362f); 690 | background-image: linear-gradient(top, #ee5f5b, #bd362f); 691 | background-repeat: repeat-x; 692 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#bd362f', GradientType=0); 693 | border-color: #bd362f #bd362f #802420; 694 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 695 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 696 | } 697 | .btn-danger:hover, 698 | .btn-danger:active, 699 | .btn-danger.active, 700 | .btn-danger.disabled, 701 | .btn-danger[disabled] { 702 | background-color: #bd362f; 703 | } 704 | .btn-danger:active, .btn-danger.active { 705 | background-color: #942a25 \9; 706 | } 707 | .btn-success { 708 | background-color: #5bb75b; 709 | background-image: -moz-linear-gradient(top, #62c462, #51a351); 710 | background-image: -ms-linear-gradient(top, #62c462, #51a351); 711 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351)); 712 | background-image: -webkit-linear-gradient(top, #62c462, #51a351); 713 | background-image: -o-linear-gradient(top, #62c462, #51a351); 714 | background-image: linear-gradient(top, #62c462, #51a351); 715 | background-repeat: repeat-x; 716 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#51a351', GradientType=0); 717 | border-color: #51a351 #51a351 #387038; 718 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 719 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 720 | } 721 | .btn-success:hover, 722 | .btn-success:active, 723 | .btn-success.active, 724 | .btn-success.disabled, 725 | .btn-success[disabled] { 726 | background-color: #51a351; 727 | } 728 | .btn-success:active, .btn-success.active { 729 | background-color: #408140 \9; 730 | } 731 | .btn-info { 732 | background-color: #49afcd; 733 | background-image: -moz-linear-gradient(top, #5bc0de, #2f96b4); 734 | background-image: -ms-linear-gradient(top, #5bc0de, #2f96b4); 735 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4)); 736 | background-image: -webkit-linear-gradient(top, #5bc0de, #2f96b4); 737 | background-image: -o-linear-gradient(top, #5bc0de, #2f96b4); 738 | background-image: linear-gradient(top, #5bc0de, #2f96b4); 739 | background-repeat: repeat-x; 740 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#2f96b4', GradientType=0); 741 | border-color: #2f96b4 #2f96b4 #1f6377; 742 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 743 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 744 | } 745 | .btn-info:hover, 746 | .btn-info:active, 747 | .btn-info.active, 748 | .btn-info.disabled, 749 | .btn-info[disabled] { 750 | background-color: #2f96b4; 751 | } 752 | .btn-info:active, .btn-info.active { 753 | background-color: #24748c \9; 754 | } 755 | .btn-inverse { 756 | background-color: #393939; 757 | background-image: -moz-linear-gradient(top, #454545, #262626); 758 | background-image: -ms-linear-gradient(top, #454545, #262626); 759 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#454545), to(#262626)); 760 | background-image: -webkit-linear-gradient(top, #454545, #262626); 761 | background-image: -o-linear-gradient(top, #454545, #262626); 762 | background-image: linear-gradient(top, #454545, #262626); 763 | background-repeat: repeat-x; 764 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#454545', endColorstr='#262626', GradientType=0); 765 | border-color: #262626 #262626 #000000; 766 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 767 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 768 | } 769 | .btn-inverse:hover, 770 | .btn-inverse:active, 771 | .btn-inverse.active, 772 | .btn-inverse.disabled, 773 | .btn-inverse[disabled] { 774 | background-color: #262626; 775 | } 776 | .btn-inverse:active, .btn-inverse.active { 777 | background-color: #0c0c0c \9; 778 | } 779 | button.btn, input[type="submit"].btn { 780 | *padding-top: 2px; 781 | *padding-bottom: 2px; 782 | } 783 | button.btn::-moz-focus-inner, input[type="submit"].btn::-moz-focus-inner { 784 | padding: 0; 785 | border: 0; 786 | } 787 | button.btn.large, input[type="submit"].btn.large { 788 | *padding-top: 7px; 789 | *padding-bottom: 7px; 790 | } 791 | button.btn.small, input[type="submit"].btn.small { 792 | *padding-top: 3px; 793 | *padding-bottom: 3px; 794 | } 795 | .navbar { 796 | overflow: visible; 797 | margin-bottom: 18px; 798 | } 799 | .navbar-inner { 800 | padding-left: 20px; 801 | padding-right: 20px; 802 | background-color: #2c2c2c; 803 | background-image: -moz-linear-gradient(top, #333333, #222222); 804 | background-image: -ms-linear-gradient(top, #333333, #222222); 805 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222)); 806 | background-image: -webkit-linear-gradient(top, #333333, #222222); 807 | background-image: -o-linear-gradient(top, #333333, #222222); 808 | background-image: linear-gradient(top, #333333, #222222); 809 | background-repeat: repeat-x; 810 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0); 811 | -webkit-border-radius: 4px; 812 | -moz-border-radius: 4px; 813 | border-radius: 4px; 814 | -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); 815 | -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); 816 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); 817 | } 818 | .btn-navbar { 819 | display: none; 820 | float: right; 821 | padding: 7px 10px; 822 | margin-left: 5px; 823 | margin-right: 5px; 824 | background-color: #2c2c2c; 825 | background-image: -moz-linear-gradient(top, #333333, #222222); 826 | background-image: -ms-linear-gradient(top, #333333, #222222); 827 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222)); 828 | background-image: -webkit-linear-gradient(top, #333333, #222222); 829 | background-image: -o-linear-gradient(top, #333333, #222222); 830 | background-image: linear-gradient(top, #333333, #222222); 831 | background-repeat: repeat-x; 832 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0); 833 | border-color: #222222 #222222 #000000; 834 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 835 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 836 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); 837 | -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); 838 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); 839 | } 840 | .btn-navbar:hover, 841 | .btn-navbar:active, 842 | .btn-navbar.active, 843 | .btn-navbar.disabled, 844 | .btn-navbar[disabled] { 845 | background-color: #222222; 846 | } 847 | .btn-navbar:active, .btn-navbar.active { 848 | background-color: #080808 \9; 849 | } 850 | .btn-navbar .icon-bar { 851 | display: block; 852 | width: 18px; 853 | height: 2px; 854 | background-color: #f5f5f5; 855 | -webkit-border-radius: 1px; 856 | -moz-border-radius: 1px; 857 | border-radius: 1px; 858 | -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); 859 | -moz-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); 860 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); 861 | } 862 | .btn-navbar .icon-bar + .icon-bar { 863 | margin-top: 3px; 864 | } 865 | .nav-collapse.collapse { 866 | height: auto; 867 | } 868 | .navbar .brand:hover { 869 | text-decoration: none; 870 | } 871 | .navbar .brand { 872 | float: left; 873 | display: block; 874 | padding: 8px 20px 12px; 875 | margin-left: -20px; 876 | font-size: 20px; 877 | font-weight: 200; 878 | line-height: 1; 879 | color: #ffffff; 880 | } 881 | .navbar .navbar-text { 882 | margin-bottom: 0; 883 | line-height: 40px; 884 | color: #999999; 885 | } 886 | .navbar .navbar-text a:hover { 887 | color: #ffffff; 888 | background-color: transparent; 889 | } 890 | .navbar .btn, .navbar .btn-group { 891 | margin-top: 5px; 892 | } 893 | .navbar .btn-group .btn { 894 | margin-top: 0; 895 | } 896 | .navbar-form { 897 | margin-bottom: 0; 898 | *zoom: 1; 899 | } 900 | .navbar-form:before, .navbar-form:after { 901 | display: table; 902 | content: ""; 903 | } 904 | .navbar-form:after { 905 | clear: both; 906 | } 907 | .navbar-form input, .navbar-form select { 908 | display: inline-block; 909 | margin-top: 5px; 910 | margin-bottom: 0; 911 | } 912 | .navbar-form .radio, .navbar-form .checkbox { 913 | margin-top: 5px; 914 | } 915 | .navbar-form input[type="image"], .navbar-form input[type="checkbox"], .navbar-form input[type="radio"] { 916 | margin-top: 3px; 917 | } 918 | .navbar-form .input-append, .navbar-form .input-prepend { 919 | margin-top: 6px; 920 | white-space: nowrap; 921 | } 922 | .navbar-form .input-append input, .navbar-form .input-prepend input { 923 | margin-top: 0; 924 | } 925 | .navbar-search { 926 | position: relative; 927 | float: left; 928 | margin-top: 6px; 929 | margin-bottom: 0; 930 | } 931 | .navbar-search .search-query { 932 | padding: 4px 9px; 933 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 934 | font-size: 13px; 935 | font-weight: normal; 936 | line-height: 1; 937 | color: #ffffff; 938 | color: rgba(255, 255, 255, 0.75); 939 | background: #666; 940 | background: rgba(255, 255, 255, 0.3); 941 | border: 1px solid #111; 942 | -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0px rgba(255, 255, 255, 0.15); 943 | -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0px rgba(255, 255, 255, 0.15); 944 | box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0px rgba(255, 255, 255, 0.15); 945 | -webkit-transition: none; 946 | -moz-transition: none; 947 | -ms-transition: none; 948 | -o-transition: none; 949 | transition: none; 950 | } 951 | .navbar-search .search-query :-moz-placeholder { 952 | color: #eeeeee; 953 | } 954 | .navbar-search .search-query::-webkit-input-placeholder { 955 | color: #eeeeee; 956 | } 957 | .navbar-search .search-query:hover { 958 | color: #ffffff; 959 | background-color: #999999; 960 | background-color: rgba(255, 255, 255, 0.5); 961 | } 962 | .navbar-search .search-query:focus, .navbar-search .search-query.focused { 963 | padding: 5px 10px; 964 | color: #333333; 965 | text-shadow: 0 1px 0 #ffffff; 966 | background-color: #ffffff; 967 | border: 0; 968 | -webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); 969 | -moz-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); 970 | box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); 971 | outline: 0; 972 | } 973 | .navbar-fixed-top { 974 | position: fixed; 975 | top: 0; 976 | right: 0; 977 | left: 0; 978 | z-index: 1030; 979 | } 980 | .navbar-fixed-top .navbar-inner { 981 | padding-left: 0; 982 | padding-right: 0; 983 | -webkit-border-radius: 0; 984 | -moz-border-radius: 0; 985 | border-radius: 0; 986 | } 987 | .navbar .nav { 988 | position: relative; 989 | left: 0; 990 | display: block; 991 | float: left; 992 | margin: 0 10px 0 0; 993 | } 994 | .navbar .nav.pull-right { 995 | float: right; 996 | } 997 | .navbar .nav > li { 998 | display: block; 999 | float: left; 1000 | } 1001 | .navbar .nav > li > a { 1002 | float: none; 1003 | padding: 10px 10px 11px; 1004 | line-height: 19px; 1005 | color: #999999; 1006 | text-decoration: none; 1007 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 1008 | } 1009 | .navbar .nav > li > a:hover { 1010 | background-color: transparent; 1011 | color: #ffffff; 1012 | text-decoration: none; 1013 | } 1014 | .navbar .nav .active > a, .navbar .nav .active > a:hover { 1015 | color: #ffffff; 1016 | text-decoration: none; 1017 | background-color: #222222; 1018 | } 1019 | .navbar .divider-vertical { 1020 | height: 40px; 1021 | width: 1px; 1022 | margin: 0 9px; 1023 | overflow: hidden; 1024 | background-color: #222222; 1025 | border-right: 1px solid #333333; 1026 | } 1027 | .navbar .nav.pull-right { 1028 | margin-left: 10px; 1029 | margin-right: 0; 1030 | } 1031 | .navbar .dropdown-menu { 1032 | margin-top: 1px; 1033 | -webkit-border-radius: 4px; 1034 | -moz-border-radius: 4px; 1035 | border-radius: 4px; 1036 | } 1037 | .navbar .dropdown-menu:before { 1038 | content: ''; 1039 | display: inline-block; 1040 | border-left: 7px solid transparent; 1041 | border-right: 7px solid transparent; 1042 | border-bottom: 7px solid #ccc; 1043 | border-bottom-color: rgba(0, 0, 0, 0.2); 1044 | position: absolute; 1045 | top: -7px; 1046 | left: 9px; 1047 | } 1048 | .navbar .dropdown-menu:after { 1049 | content: ''; 1050 | display: inline-block; 1051 | border-left: 6px solid transparent; 1052 | border-right: 6px solid transparent; 1053 | border-bottom: 6px solid #ffffff; 1054 | position: absolute; 1055 | top: -6px; 1056 | left: 10px; 1057 | } 1058 | .navbar .nav .dropdown-toggle .caret, .navbar .nav .open.dropdown .caret { 1059 | border-top-color: #ffffff; 1060 | } 1061 | .navbar .nav .active .caret { 1062 | opacity: 1; 1063 | filter: alpha(opacity=100); 1064 | } 1065 | .navbar .nav .open > .dropdown-toggle, .navbar .nav .active > .dropdown-toggle, .navbar .nav .open.active > .dropdown-toggle { 1066 | background-color: transparent; 1067 | } 1068 | .navbar .nav .active > .dropdown-toggle:hover { 1069 | color: #ffffff; 1070 | } 1071 | .navbar .nav.pull-right .dropdown-menu { 1072 | left: auto; 1073 | right: 0; 1074 | } 1075 | .navbar .nav.pull-right .dropdown-menu:before { 1076 | left: auto; 1077 | right: 12px; 1078 | } 1079 | .navbar .nav.pull-right .dropdown-menu:after { 1080 | left: auto; 1081 | right: 13px; 1082 | } 1083 | .alert { 1084 | padding: 8px 35px 8px 14px; 1085 | margin-bottom: 18px; 1086 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 1087 | background-color: #fcf8e3; 1088 | border: 1px solid #fbeed5; 1089 | -webkit-border-radius: 4px; 1090 | -moz-border-radius: 4px; 1091 | border-radius: 4px; 1092 | } 1093 | .alert, .alert-heading { 1094 | color: #c09853; 1095 | } 1096 | .alert .close { 1097 | position: relative; 1098 | top: -2px; 1099 | right: -21px; 1100 | line-height: 18px; 1101 | } 1102 | .alert-success { 1103 | background-color: #dff0d8; 1104 | border-color: #d6e9c6; 1105 | } 1106 | .alert-success, .alert-success .alert-heading { 1107 | color: #468847; 1108 | } 1109 | .alert-danger, .alert-error { 1110 | background-color: #f2dede; 1111 | border-color: #eed3d7; 1112 | } 1113 | .alert-danger, 1114 | .alert-error, 1115 | .alert-danger .alert-heading, 1116 | .alert-error .alert-heading { 1117 | color: #b94a48; 1118 | } 1119 | .alert-info { 1120 | background-color: #d9edf7; 1121 | border-color: #bce8f1; 1122 | } 1123 | .alert-info, .alert-info .alert-heading { 1124 | color: #3a87ad; 1125 | } 1126 | .alert-block { 1127 | padding-top: 14px; 1128 | padding-bottom: 14px; 1129 | } 1130 | .alert-block > p, .alert-block > ul { 1131 | margin-bottom: 0; 1132 | } 1133 | .alert-block p + p { 1134 | margin-top: 5px; 1135 | } 1136 | .tooltip { 1137 | position: absolute; 1138 | z-index: 1020; 1139 | display: block; 1140 | visibility: visible; 1141 | padding: 5px; 1142 | font-size: 11px; 1143 | opacity: 0; 1144 | filter: alpha(opacity=0); 1145 | } 1146 | .tooltip.in { 1147 | opacity: 0.8; 1148 | filter: alpha(opacity=80); 1149 | } 1150 | .tooltip.top { 1151 | margin-top: -2px; 1152 | } 1153 | .tooltip.right { 1154 | margin-left: 2px; 1155 | } 1156 | .tooltip.bottom { 1157 | margin-top: 2px; 1158 | } 1159 | .tooltip.left { 1160 | margin-left: -2px; 1161 | } 1162 | .tooltip.top .tooltip-arrow { 1163 | bottom: 0; 1164 | left: 50%; 1165 | margin-left: -5px; 1166 | border-left: 5px solid transparent; 1167 | border-right: 5px solid transparent; 1168 | border-top: 5px solid #000000; 1169 | } 1170 | .tooltip.left .tooltip-arrow { 1171 | top: 50%; 1172 | right: 0; 1173 | margin-top: -5px; 1174 | border-top: 5px solid transparent; 1175 | border-bottom: 5px solid transparent; 1176 | border-left: 5px solid #000000; 1177 | } 1178 | .tooltip.bottom .tooltip-arrow { 1179 | top: 0; 1180 | left: 50%; 1181 | margin-left: -5px; 1182 | border-left: 5px solid transparent; 1183 | border-right: 5px solid transparent; 1184 | border-bottom: 5px solid #000000; 1185 | } 1186 | .tooltip.right .tooltip-arrow { 1187 | top: 50%; 1188 | left: 0; 1189 | margin-top: -5px; 1190 | border-top: 5px solid transparent; 1191 | border-bottom: 5px solid transparent; 1192 | border-right: 5px solid #000000; 1193 | } 1194 | .tooltip-inner { 1195 | max-width: 200px; 1196 | padding: 3px 8px; 1197 | color: #ffffff; 1198 | text-align: center; 1199 | text-decoration: none; 1200 | background-color: #000000; 1201 | -webkit-border-radius: 4px; 1202 | -moz-border-radius: 4px; 1203 | border-radius: 4px; 1204 | } 1205 | .tooltip-arrow { 1206 | position: absolute; 1207 | width: 0; 1208 | height: 0; 1209 | } 1210 | .modal-open .dropdown-menu { 1211 | z-index: 2050; 1212 | } 1213 | .modal-open .dropdown.open { 1214 | *z-index: 2050; 1215 | } 1216 | .modal-open .popover { 1217 | z-index: 2060; 1218 | } 1219 | .modal-open .tooltip { 1220 | z-index: 2070; 1221 | } 1222 | .modal-backdrop { 1223 | position: fixed; 1224 | top: 0; 1225 | right: 0; 1226 | bottom: 0; 1227 | left: 0; 1228 | z-index: 1040; 1229 | background-color: #000000; 1230 | } 1231 | .modal-backdrop.fade { 1232 | opacity: 0; 1233 | } 1234 | .modal-backdrop, .modal-backdrop.fade.in { 1235 | opacity: 0.8; 1236 | filter: alpha(opacity=80); 1237 | } 1238 | .modal { 1239 | position: fixed; 1240 | top: 50%; 1241 | left: 50%; 1242 | z-index: 1050; 1243 | max-height: 500px; 1244 | overflow: auto; 1245 | width: 560px; 1246 | margin: -250px 0 0 -280px; 1247 | background-color: #ffffff; 1248 | border: 1px solid #999; 1249 | border: 1px solid rgba(0, 0, 0, 0.3); 1250 | *border: 1px solid #999; 1251 | /* IE6-7 */ 1252 | 1253 | -webkit-border-radius: 6px; 1254 | -moz-border-radius: 6px; 1255 | border-radius: 6px; 1256 | -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); 1257 | -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); 1258 | box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); 1259 | -webkit-background-clip: padding-box; 1260 | -moz-background-clip: padding-box; 1261 | background-clip: padding-box; 1262 | } 1263 | .modal.fade { 1264 | -webkit-transition: opacity .3s linear, top .3s ease-out; 1265 | -moz-transition: opacity .3s linear, top .3s ease-out; 1266 | -ms-transition: opacity .3s linear, top .3s ease-out; 1267 | -o-transition: opacity .3s linear, top .3s ease-out; 1268 | transition: opacity .3s linear, top .3s ease-out; 1269 | top: -25%; 1270 | } 1271 | .modal.fade.in { 1272 | top: 50%; 1273 | } 1274 | .modal-header { 1275 | padding: 9px 15px; 1276 | border-bottom: 1px solid #eee; 1277 | } 1278 | .modal-header .close { 1279 | margin-top: 2px; 1280 | } 1281 | .modal-body { 1282 | padding: 15px; 1283 | } 1284 | .modal-body .modal-form { 1285 | margin-bottom: 0; 1286 | } 1287 | .modal-footer { 1288 | padding: 14px 15px 15px; 1289 | margin-bottom: 0; 1290 | background-color: #f5f5f5; 1291 | border-top: 1px solid #ddd; 1292 | -webkit-border-radius: 0 0 6px 6px; 1293 | -moz-border-radius: 0 0 6px 6px; 1294 | border-radius: 0 0 6px 6px; 1295 | -webkit-box-shadow: inset 0 1px 0 #ffffff; 1296 | -moz-box-shadow: inset 0 1px 0 #ffffff; 1297 | box-shadow: inset 0 1px 0 #ffffff; 1298 | *zoom: 1; 1299 | } 1300 | .modal-footer:before, .modal-footer:after { 1301 | display: table; 1302 | content: ""; 1303 | } 1304 | .modal-footer:after { 1305 | clear: both; 1306 | } 1307 | .modal-footer .btn { 1308 | float: right; 1309 | margin-left: 5px; 1310 | margin-bottom: 0; 1311 | } 1312 | .close { 1313 | float: right; 1314 | font-size: 20px; 1315 | font-weight: bold; 1316 | line-height: 18px; 1317 | color: #000000; 1318 | text-shadow: 0 1px 0 #ffffff; 1319 | opacity: 0.2; 1320 | filter: alpha(opacity=20); 1321 | } 1322 | .close:hover { 1323 | color: #000000; 1324 | text-decoration: none; 1325 | opacity: 0.4; 1326 | filter: alpha(opacity=40); 1327 | cursor: pointer; 1328 | } 1329 | .pull-right { 1330 | float: right; 1331 | } 1332 | .pull-left { 1333 | float: left; 1334 | } 1335 | .hide { 1336 | display: none; 1337 | } 1338 | .show { 1339 | display: block; 1340 | } 1341 | .invisible { 1342 | visibility: hidden; 1343 | } 1344 | .fade { 1345 | -webkit-transition: opacity 0.15s linear; 1346 | -moz-transition: opacity 0.15s linear; 1347 | -ms-transition: opacity 0.15s linear; 1348 | -o-transition: opacity 0.15s linear; 1349 | transition: opacity 0.15s linear; 1350 | opacity: 0; 1351 | } 1352 | .fade.in { 1353 | opacity: 1; 1354 | } 1355 | .collapse { 1356 | -webkit-transition: height 0.35s ease; 1357 | -moz-transition: height 0.35s ease; 1358 | -ms-transition: height 0.35s ease; 1359 | -o-transition: height 0.35s ease; 1360 | transition: height 0.35s ease; 1361 | position: relative; 1362 | overflow: hidden; 1363 | height: 0; 1364 | } 1365 | .collapse.in { 1366 | height: auto; 1367 | } 1368 | -------------------------------------------------------------------------------- /flask_split/static/css/bootstrap.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v2.0.1 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */ 10 | .clearfix{*zoom:1;}.clearfix:before,.clearfix:after{display:table;content:"";} 11 | .clearfix:after{clear:both;} 12 | article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block;} 13 | audio,canvas,video{display:inline-block;*display:inline;*zoom:1;} 14 | audio:not([controls]){display:none;} 15 | html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;} 16 | a:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;} 17 | a:hover,a:active{outline:0;} 18 | sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline;} 19 | sup{top:-0.5em;} 20 | sub{bottom:-0.25em;} 21 | img{max-width:100%;height:auto;border:0;-ms-interpolation-mode:bicubic;} 22 | button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle;} 23 | button,input{*overflow:visible;line-height:normal;} 24 | button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0;} 25 | button,input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button;} 26 | input[type="search"]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;} 27 | input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none;} 28 | textarea{overflow:auto;vertical-align:top;} 29 | body{margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;line-height:18px;color:#333333;background-color:#ffffff;} 30 | a{color:#0088cc;text-decoration:none;} 31 | a:hover{color:#005580;text-decoration:underline;} 32 | .container{width:940px;margin-left:auto;margin-right:auto;*zoom:1;}.container:before,.container:after{display:table;content:"";} 33 | .container:after{clear:both;} 34 | .container-fluid{padding-left:20px;padding-right:20px;*zoom:1;}.container-fluid:before,.container-fluid:after{display:table;content:"";} 35 | .container-fluid:after{clear:both;} 36 | p{margin:0 0 9px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;line-height:18px;}p small{font-size:11px;color:#999999;} 37 | .lead{margin-bottom:18px;font-size:20px;font-weight:200;line-height:27px;} 38 | h1,h2,h3,h4,h5,h6{margin:0;font-weight:bold;color:#333333;text-rendering:optimizelegibility;}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-weight:normal;color:#999999;} 39 | h1{font-size:30px;line-height:36px;}h1 small{font-size:18px;} 40 | h2{font-size:24px;line-height:36px;}h2 small{font-size:18px;} 41 | h3{line-height:27px;font-size:18px;}h3 small{font-size:14px;} 42 | h4,h5,h6{line-height:18px;} 43 | h4{font-size:14px;}h4 small{font-size:12px;} 44 | h5{font-size:12px;} 45 | h6{font-size:11px;color:#999999;text-transform:uppercase;} 46 | .page-header{padding-bottom:17px;margin:18px 0;border-bottom:1px solid #eeeeee;} 47 | .page-header h1{line-height:1;} 48 | ul,ol{padding:0;margin:0 0 9px 25px;} 49 | ul ul,ul ol,ol ol,ol ul{margin-bottom:0;} 50 | ul{list-style:disc;} 51 | ol{list-style:decimal;} 52 | li{line-height:18px;} 53 | ul.unstyled,ol.unstyled{margin-left:0;list-style:none;} 54 | dl{margin-bottom:18px;} 55 | dt,dd{line-height:18px;} 56 | dt{font-weight:bold;} 57 | dd{margin-left:9px;} 58 | hr{margin:18px 0;border:0;border-top:1px solid #eeeeee;border-bottom:1px solid #ffffff;} 59 | strong{font-weight:bold;} 60 | em{font-style:italic;} 61 | .muted{color:#999999;} 62 | abbr{font-size:90%;text-transform:uppercase;border-bottom:1px dotted #ddd;cursor:help;} 63 | blockquote{padding:0 0 0 15px;margin:0 0 18px;border-left:5px solid #eeeeee;}blockquote p{margin-bottom:0;font-size:16px;font-weight:300;line-height:22.5px;} 64 | blockquote small{display:block;line-height:18px;color:#999999;}blockquote small:before{content:'\2014 \00A0';} 65 | blockquote.pull-right{float:right;padding-left:0;padding-right:15px;border-left:0;border-right:5px solid #eeeeee;}blockquote.pull-right p,blockquote.pull-right small{text-align:right;} 66 | q:before,q:after,blockquote:before,blockquote:after{content:"";} 67 | address{display:block;margin-bottom:18px;line-height:18px;font-style:normal;} 68 | small{font-size:100%;} 69 | cite{font-style:normal;} 70 | .label{padding:2px 4px 3px;font-size:11.049999999999999px;font-weight:bold;color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#999999;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} 71 | .label:hover{color:#ffffff;text-decoration:none;} 72 | .label-important{background-color:#b94a48;} 73 | .label-important:hover{background-color:#953b39;} 74 | .label-warning{background-color:#f89406;} 75 | .label-warning:hover{background-color:#c67605;} 76 | .label-success{background-color:#468847;} 77 | .label-success:hover{background-color:#356635;} 78 | .label-info{background-color:#3a87ad;} 79 | .label-info:hover{background-color:#2d6987;} 80 | table{max-width:100%;border-collapse:collapse;border-spacing:0;} 81 | .table{width:100%;margin-bottom:18px;}.table th,.table td{padding:8px;line-height:18px;text-align:left;vertical-align:top;border-top:1px solid #ddd;} 82 | .table th{font-weight:bold;} 83 | .table thead th{vertical-align:bottom;} 84 | .table thead:first-child tr th,.table thead:first-child tr td{border-top:0;} 85 | .table tbody+tbody{border-top:2px solid #ddd;} 86 | .table-condensed th,.table-condensed td{padding:4px 5px;} 87 | .table-bordered{border:1px solid #ddd;border-collapse:separate;*border-collapse:collapsed;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}.table-bordered th+th,.table-bordered td+td,.table-bordered th+td,.table-bordered td+th{border-left:1px solid #ddd;} 88 | .table-bordered thead:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child td{border-top:0;} 89 | .table-bordered thead:first-child tr:first-child th:first-child,.table-bordered tbody:first-child tr:first-child td:first-child{-webkit-border-radius:4px 0 0 0;-moz-border-radius:4px 0 0 0;border-radius:4px 0 0 0;} 90 | .table-bordered thead:first-child tr:first-child th:last-child,.table-bordered tbody:first-child tr:first-child td:last-child{-webkit-border-radius:0 4px 0 0;-moz-border-radius:0 4px 0 0;border-radius:0 4px 0 0;} 91 | .table-bordered thead:last-child tr:last-child th:first-child,.table-bordered tbody:last-child tr:last-child td:first-child{-webkit-border-radius:0 0 0 4px;-moz-border-radius:0 0 0 4px;border-radius:0 0 0 4px;} 92 | .table-bordered thead:last-child tr:last-child th:last-child,.table-bordered tbody:last-child tr:last-child td:last-child{-webkit-border-radius:0 0 4px 0;-moz-border-radius:0 0 4px 0;border-radius:0 0 4px 0;} 93 | .table-striped tbody tr:nth-child(odd) td,.table-striped tbody tr:nth-child(odd) th{background-color:#f9f9f9;} 94 | .table tbody tr:hover td,.table tbody tr:hover th{background-color:#f5f5f5;} 95 | table .span1{float:none;width:44px;margin-left:0;} 96 | table .span2{float:none;width:124px;margin-left:0;} 97 | table .span3{float:none;width:204px;margin-left:0;} 98 | table .span4{float:none;width:284px;margin-left:0;} 99 | table .span5{float:none;width:364px;margin-left:0;} 100 | table .span6{float:none;width:444px;margin-left:0;} 101 | table .span7{float:none;width:524px;margin-left:0;} 102 | table .span8{float:none;width:604px;margin-left:0;} 103 | table .span9{float:none;width:684px;margin-left:0;} 104 | table .span10{float:none;width:764px;margin-left:0;} 105 | table .span11{float:none;width:844px;margin-left:0;} 106 | table .span12{float:none;width:924px;margin-left:0;} 107 | .btn{display:inline-block;padding:4px 10px 4px;margin-bottom:0;font-size:13px;line-height:18px;color:#333333;text-align:center;text-shadow:0 1px 1px rgba(255, 255, 255, 0.75);vertical-align:middle;background-color:#f5f5f5;background-image:-moz-linear-gradient(top, #ffffff, #e6e6e6);background-image:-ms-linear-gradient(top, #ffffff, #e6e6e6);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6));background-image:-webkit-linear-gradient(top, #ffffff, #e6e6e6);background-image:-o-linear-gradient(top, #ffffff, #e6e6e6);background-image:linear-gradient(top, #ffffff, #e6e6e6);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0);border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);border:1px solid #ccc;border-bottom-color:#bbb;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);cursor:pointer;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);*margin-left:.3em;}.btn:hover,.btn:active,.btn.active,.btn.disabled,.btn[disabled]{background-color:#e6e6e6;} 108 | .btn:active,.btn.active{background-color:#cccccc \9;} 109 | .btn:first-child{*margin-left:0;} 110 | .btn:hover{color:#333333;text-decoration:none;background-color:#e6e6e6;background-position:0 -15px;-webkit-transition:background-position 0.1s linear;-moz-transition:background-position 0.1s linear;-ms-transition:background-position 0.1s linear;-o-transition:background-position 0.1s linear;transition:background-position 0.1s linear;} 111 | .btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;} 112 | .btn.active,.btn:active{background-image:none;-webkit-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);background-color:#e6e6e6;background-color:#d9d9d9 \9;outline:0;} 113 | .btn.disabled,.btn[disabled]{cursor:default;background-image:none;background-color:#e6e6e6;opacity:0.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;} 114 | .btn-large{padding:9px 14px;font-size:15px;line-height:normal;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;} 115 | .btn-large [class^="icon-"]{margin-top:1px;} 116 | .btn-small{padding:5px 9px;font-size:11px;line-height:16px;} 117 | .btn-small [class^="icon-"]{margin-top:-1px;} 118 | .btn-mini{padding:2px 6px;font-size:11px;line-height:14px;} 119 | .btn-primary,.btn-primary:hover,.btn-warning,.btn-warning:hover,.btn-danger,.btn-danger:hover,.btn-success,.btn-success:hover,.btn-info,.btn-info:hover,.btn-inverse,.btn-inverse:hover{text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);color:#ffffff;} 120 | .btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active,.btn-dark.active{color:rgba(255, 255, 255, 0.75);} 121 | .btn-primary{background-color:#006dcc;background-image:-moz-linear-gradient(top, #0088cc, #0044cc);background-image:-ms-linear-gradient(top, #0088cc, #0044cc);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));background-image:-webkit-linear-gradient(top, #0088cc, #0044cc);background-image:-o-linear-gradient(top, #0088cc, #0044cc);background-image:linear-gradient(top, #0088cc, #0044cc);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0);border-color:#0044cc #0044cc #002a80;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-primary:hover,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{background-color:#0044cc;} 122 | .btn-primary:active,.btn-primary.active{background-color:#003399 \9;} 123 | .btn-warning{background-color:#faa732;background-image:-moz-linear-gradient(top, #fbb450, #f89406);background-image:-ms-linear-gradient(top, #fbb450, #f89406);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406));background-image:-webkit-linear-gradient(top, #fbb450, #f89406);background-image:-o-linear-gradient(top, #fbb450, #f89406);background-image:linear-gradient(top, #fbb450, #f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0);border-color:#f89406 #f89406 #ad6704;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-warning:hover,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{background-color:#f89406;} 124 | .btn-warning:active,.btn-warning.active{background-color:#c67605 \9;} 125 | .btn-danger{background-color:#da4f49;background-image:-moz-linear-gradient(top, #ee5f5b, #bd362f);background-image:-ms-linear-gradient(top, #ee5f5b, #bd362f);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f));background-image:-webkit-linear-gradient(top, #ee5f5b, #bd362f);background-image:-o-linear-gradient(top, #ee5f5b, #bd362f);background-image:linear-gradient(top, #ee5f5b, #bd362f);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#bd362f', GradientType=0);border-color:#bd362f #bd362f #802420;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-danger:hover,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{background-color:#bd362f;} 126 | .btn-danger:active,.btn-danger.active{background-color:#942a25 \9;} 127 | .btn-success{background-color:#5bb75b;background-image:-moz-linear-gradient(top, #62c462, #51a351);background-image:-ms-linear-gradient(top, #62c462, #51a351);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351));background-image:-webkit-linear-gradient(top, #62c462, #51a351);background-image:-o-linear-gradient(top, #62c462, #51a351);background-image:linear-gradient(top, #62c462, #51a351);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#51a351', GradientType=0);border-color:#51a351 #51a351 #387038;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-success:hover,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{background-color:#51a351;} 128 | .btn-success:active,.btn-success.active{background-color:#408140 \9;} 129 | .btn-info{background-color:#49afcd;background-image:-moz-linear-gradient(top, #5bc0de, #2f96b4);background-image:-ms-linear-gradient(top, #5bc0de, #2f96b4);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4));background-image:-webkit-linear-gradient(top, #5bc0de, #2f96b4);background-image:-o-linear-gradient(top, #5bc0de, #2f96b4);background-image:linear-gradient(top, #5bc0de, #2f96b4);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#2f96b4', GradientType=0);border-color:#2f96b4 #2f96b4 #1f6377;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-info:hover,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{background-color:#2f96b4;} 130 | .btn-info:active,.btn-info.active{background-color:#24748c \9;} 131 | .btn-inverse{background-color:#393939;background-image:-moz-linear-gradient(top, #454545, #262626);background-image:-ms-linear-gradient(top, #454545, #262626);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#454545), to(#262626));background-image:-webkit-linear-gradient(top, #454545, #262626);background-image:-o-linear-gradient(top, #454545, #262626);background-image:linear-gradient(top, #454545, #262626);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#454545', endColorstr='#262626', GradientType=0);border-color:#262626 #262626 #000000;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-inverse:hover,.btn-inverse:active,.btn-inverse.active,.btn-inverse.disabled,.btn-inverse[disabled]{background-color:#262626;} 132 | .btn-inverse:active,.btn-inverse.active{background-color:#0c0c0c \9;} 133 | button.btn,input[type="submit"].btn{*padding-top:2px;*padding-bottom:2px;}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0;} 134 | button.btn.large,input[type="submit"].btn.large{*padding-top:7px;*padding-bottom:7px;} 135 | button.btn.small,input[type="submit"].btn.small{*padding-top:3px;*padding-bottom:3px;} 136 | .navbar{overflow:visible;margin-bottom:18px;} 137 | .navbar-inner{padding-left:20px;padding-right:20px;background-color:#2c2c2c;background-image:-moz-linear-gradient(top, #333333, #222222);background-image:-ms-linear-gradient(top, #333333, #222222);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222));background-image:-webkit-linear-gradient(top, #333333, #222222);background-image:-o-linear-gradient(top, #333333, #222222);background-image:linear-gradient(top, #333333, #222222);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);-moz-box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);} 138 | .btn-navbar{display:none;float:right;padding:7px 10px;margin-left:5px;margin-right:5px;background-color:#2c2c2c;background-image:-moz-linear-gradient(top, #333333, #222222);background-image:-ms-linear-gradient(top, #333333, #222222);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222));background-image:-webkit-linear-gradient(top, #333333, #222222);background-image:-o-linear-gradient(top, #333333, #222222);background-image:linear-gradient(top, #333333, #222222);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0);border-color:#222222 #222222 #000000;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.075);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.075);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.075);}.btn-navbar:hover,.btn-navbar:active,.btn-navbar.active,.btn-navbar.disabled,.btn-navbar[disabled]{background-color:#222222;} 139 | .btn-navbar:active,.btn-navbar.active{background-color:#080808 \9;} 140 | .btn-navbar .icon-bar{display:block;width:18px;height:2px;background-color:#f5f5f5;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;-webkit-box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);-moz-box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);} 141 | .btn-navbar .icon-bar+.icon-bar{margin-top:3px;} 142 | .nav-collapse.collapse{height:auto;} 143 | .navbar .brand:hover{text-decoration:none;} 144 | .navbar .brand{float:left;display:block;padding:8px 20px 12px;margin-left:-20px;font-size:20px;font-weight:200;line-height:1;color:#ffffff;} 145 | .navbar .navbar-text{margin-bottom:0;line-height:40px;color:#999999;}.navbar .navbar-text a:hover{color:#ffffff;background-color:transparent;} 146 | .navbar .btn,.navbar .btn-group{margin-top:5px;} 147 | .navbar .btn-group .btn{margin-top:0;} 148 | .navbar-form{margin-bottom:0;*zoom:1;}.navbar-form:before,.navbar-form:after{display:table;content:"";} 149 | .navbar-form:after{clear:both;} 150 | .navbar-form input,.navbar-form select{display:inline-block;margin-top:5px;margin-bottom:0;} 151 | .navbar-form .radio,.navbar-form .checkbox{margin-top:5px;} 152 | .navbar-form input[type="image"],.navbar-form input[type="checkbox"],.navbar-form input[type="radio"]{margin-top:3px;} 153 | .navbar-form .input-append,.navbar-form .input-prepend{margin-top:6px;white-space:nowrap;}.navbar-form .input-append input,.navbar-form .input-prepend input{margin-top:0;} 154 | .navbar-search{position:relative;float:left;margin-top:6px;margin-bottom:0;}.navbar-search .search-query{padding:4px 9px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:1;color:#ffffff;color:rgba(255, 255, 255, 0.75);background:#666;background:rgba(255, 255, 255, 0.3);border:1px solid #111;-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.15);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.15);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.15);-webkit-transition:none;-moz-transition:none;-ms-transition:none;-o-transition:none;transition:none;}.navbar-search .search-query :-moz-placeholder{color:#eeeeee;} 155 | .navbar-search .search-query::-webkit-input-placeholder{color:#eeeeee;} 156 | .navbar-search .search-query:hover{color:#ffffff;background-color:#999999;background-color:rgba(255, 255, 255, 0.5);} 157 | .navbar-search .search-query:focus,.navbar-search .search-query.focused{padding:5px 10px;color:#333333;text-shadow:0 1px 0 #ffffff;background-color:#ffffff;border:0;-webkit-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);-moz-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);box-shadow:0 0 3px rgba(0, 0, 0, 0.15);outline:0;} 158 | .navbar-fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030;} 159 | .navbar-fixed-top .navbar-inner{padding-left:0;padding-right:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;} 160 | .navbar .nav{position:relative;left:0;display:block;float:left;margin:0 10px 0 0;} 161 | .navbar .nav.pull-right{float:right;} 162 | .navbar .nav>li{display:block;float:left;} 163 | .navbar .nav>li>a{float:none;padding:10px 10px 11px;line-height:19px;color:#999999;text-decoration:none;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);} 164 | .navbar .nav>li>a:hover{background-color:transparent;color:#ffffff;text-decoration:none;} 165 | .navbar .nav .active>a,.navbar .nav .active>a:hover{color:#ffffff;text-decoration:none;background-color:#222222;} 166 | .navbar .divider-vertical{height:40px;width:1px;margin:0 9px;overflow:hidden;background-color:#222222;border-right:1px solid #333333;} 167 | .navbar .nav.pull-right{margin-left:10px;margin-right:0;} 168 | .navbar .dropdown-menu{margin-top:1px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}.navbar .dropdown-menu:before{content:'';display:inline-block;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-bottom-color:rgba(0, 0, 0, 0.2);position:absolute;top:-7px;left:9px;} 169 | .navbar .dropdown-menu:after{content:'';display:inline-block;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #ffffff;position:absolute;top:-6px;left:10px;} 170 | .navbar .nav .dropdown-toggle .caret,.navbar .nav .open.dropdown .caret{border-top-color:#ffffff;} 171 | .navbar .nav .active .caret{opacity:1;filter:alpha(opacity=100);} 172 | .navbar .nav .open>.dropdown-toggle,.navbar .nav .active>.dropdown-toggle,.navbar .nav .open.active>.dropdown-toggle{background-color:transparent;} 173 | .navbar .nav .active>.dropdown-toggle:hover{color:#ffffff;} 174 | .navbar .nav.pull-right .dropdown-menu{left:auto;right:0;}.navbar .nav.pull-right .dropdown-menu:before{left:auto;right:12px;} 175 | .navbar .nav.pull-right .dropdown-menu:after{left:auto;right:13px;} 176 | .alert{padding:8px 35px 8px 14px;margin-bottom:18px;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} 177 | .alert,.alert-heading{color:#c09853;} 178 | .alert .close{position:relative;top:-2px;right:-21px;line-height:18px;} 179 | .alert-success{background-color:#dff0d8;border-color:#d6e9c6;} 180 | .alert-success,.alert-success .alert-heading{color:#468847;} 181 | .alert-danger,.alert-error{background-color:#f2dede;border-color:#eed3d7;} 182 | .alert-danger,.alert-error,.alert-danger .alert-heading,.alert-error .alert-heading{color:#b94a48;} 183 | .alert-info{background-color:#d9edf7;border-color:#bce8f1;} 184 | .alert-info,.alert-info .alert-heading{color:#3a87ad;} 185 | .alert-block{padding-top:14px;padding-bottom:14px;} 186 | .alert-block>p,.alert-block>ul{margin-bottom:0;} 187 | .alert-block p+p{margin-top:5px;} 188 | .tooltip{position:absolute;z-index:1020;display:block;visibility:visible;padding:5px;font-size:11px;opacity:0;filter:alpha(opacity=0);}.tooltip.in{opacity:0.8;filter:alpha(opacity=80);} 189 | .tooltip.top{margin-top:-2px;} 190 | .tooltip.right{margin-left:2px;} 191 | .tooltip.bottom{margin-top:2px;} 192 | .tooltip.left{margin-left:-2px;} 193 | .tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #000000;} 194 | .tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-left:5px solid #000000;} 195 | .tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-bottom:5px solid #000000;} 196 | .tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-right:5px solid #000000;} 197 | .tooltip-inner{max-width:200px;padding:3px 8px;color:#ffffff;text-align:center;text-decoration:none;background-color:#000000;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} 198 | .tooltip-arrow{position:absolute;width:0;height:0;} 199 | .modal-open .dropdown-menu{z-index:2050;} 200 | .modal-open .dropdown.open{*z-index:2050;} 201 | .modal-open .popover{z-index:2060;} 202 | .modal-open .tooltip{z-index:2070;} 203 | .modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000000;}.modal-backdrop.fade{opacity:0;} 204 | .modal-backdrop,.modal-backdrop.fade.in{opacity:0.8;filter:alpha(opacity=80);} 205 | .modal{position:fixed;top:50%;left:50%;z-index:1050;max-height:500px;overflow:auto;width:560px;margin:-250px 0 0 -280px;background-color:#ffffff;border:1px solid #999;border:1px solid rgba(0, 0, 0, 0.3);*border:1px solid #999;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-moz-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;}.modal.fade{-webkit-transition:opacity .3s linear, top .3s ease-out;-moz-transition:opacity .3s linear, top .3s ease-out;-ms-transition:opacity .3s linear, top .3s ease-out;-o-transition:opacity .3s linear, top .3s ease-out;transition:opacity .3s linear, top .3s ease-out;top:-25%;} 206 | .modal.fade.in{top:50%;} 207 | .modal-header{padding:9px 15px;border-bottom:1px solid #eee;}.modal-header .close{margin-top:2px;} 208 | .modal-body{padding:15px;} 209 | .modal-body .modal-form{margin-bottom:0;} 210 | .modal-footer{padding:14px 15px 15px;margin-bottom:0;background-color:#f5f5f5;border-top:1px solid #ddd;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;-webkit-box-shadow:inset 0 1px 0 #ffffff;-moz-box-shadow:inset 0 1px 0 #ffffff;box-shadow:inset 0 1px 0 #ffffff;*zoom:1;}.modal-footer:before,.modal-footer:after{display:table;content:"";} 211 | .modal-footer:after{clear:both;} 212 | .modal-footer .btn{float:right;margin-left:5px;margin-bottom:0;} 213 | .close{float:right;font-size:20px;font-weight:bold;line-height:18px;color:#000000;text-shadow:0 1px 0 #ffffff;opacity:0.2;filter:alpha(opacity=20);}.close:hover{color:#000000;text-decoration:none;opacity:0.4;filter:alpha(opacity=40);cursor:pointer;} 214 | .pull-right{float:right;} 215 | .pull-left{float:left;} 216 | .hide{display:none;} 217 | .show{display:block;} 218 | .invisible{visibility:hidden;} 219 | .fade{-webkit-transition:opacity 0.15s linear;-moz-transition:opacity 0.15s linear;-ms-transition:opacity 0.15s linear;-o-transition:opacity 0.15s linear;transition:opacity 0.15s linear;opacity:0;}.fade.in{opacity:1;} 220 | .collapse{-webkit-transition:height 0.35s ease;-moz-transition:height 0.35s ease;-ms-transition:height 0.35s ease;-o-transition:height 0.35s ease;transition:height 0.35s ease;position:relative;overflow:hidden;height:0;}.collapse.in{height:auto;} 221 | -------------------------------------------------------------------------------- /flask_split/static/css/dashboard.css: -------------------------------------------------------------------------------- 1 | tfoot { 2 | font-weight: bold; 3 | } 4 | 5 | .experiment { 6 | margin: 36px 0; 7 | } 8 | 9 | .experiment-header { 10 | border-bottom: 1px solid #e5e5e5; 11 | margin-bottom: 18px; 12 | } 13 | 14 | .experiment-header h2 { 15 | float: left; 16 | } 17 | 18 | .experiment table form { 19 | margin-bottom: 0; 20 | } 21 | 22 | #footer { 23 | margin-top: 45px; 24 | padding: 35px 0 36px; 25 | border-top: 1px solid #e5e5e5; 26 | } 27 | 28 | .inline-controls { 29 | float: right; 30 | } 31 | 32 | .inline-controls form { 33 | margin-bottom: 0; 34 | } 35 | 36 | .inline-controls form { 37 | display: inline-block; 38 | } 39 | 40 | .start-time { 41 | color: #999; 42 | font-size: 18px; 43 | vertical-align: middle; 44 | margin-right: 10px; 45 | } 46 | -------------------------------------------------------------------------------- /flask_split/static/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | /* =================================================== 2 | * bootstrap-transition.js v2.0.1 3 | * http://twitter.github.com/bootstrap/javascript.html#transitions 4 | * =================================================== 5 | * Copyright 2012 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ========================================================== */ 19 | 20 | !function( $ ) { 21 | 22 | $(function () { 23 | 24 | "use strict" 25 | 26 | /* CSS TRANSITION SUPPORT (https://gist.github.com/373874) 27 | * ======================================================= */ 28 | 29 | $.support.transition = (function () { 30 | var thisBody = document.body || document.documentElement 31 | , thisStyle = thisBody.style 32 | , support = thisStyle.transition !== undefined || thisStyle.WebkitTransition !== undefined || thisStyle.MozTransition !== undefined || thisStyle.MsTransition !== undefined || thisStyle.OTransition !== undefined 33 | 34 | return support && { 35 | end: (function () { 36 | var transitionEnd = "TransitionEnd" 37 | if ( $.browser.webkit ) { 38 | transitionEnd = "webkitTransitionEnd" 39 | } else if ( $.browser.mozilla ) { 40 | transitionEnd = "transitionend" 41 | } else if ( $.browser.opera ) { 42 | transitionEnd = "oTransitionEnd" 43 | } 44 | return transitionEnd 45 | }()) 46 | } 47 | })() 48 | 49 | }) 50 | 51 | }( window.jQuery ); 52 | /* ========================================================= 53 | * bootstrap-modal.js v2.0.1 54 | * http://twitter.github.com/bootstrap/javascript.html#modals 55 | * ========================================================= 56 | * Copyright 2012 Twitter, Inc. 57 | * 58 | * Licensed under the Apache License, Version 2.0 (the "License"); 59 | * you may not use this file except in compliance with the License. 60 | * You may obtain a copy of the License at 61 | * 62 | * http://www.apache.org/licenses/LICENSE-2.0 63 | * 64 | * Unless required by applicable law or agreed to in writing, software 65 | * distributed under the License is distributed on an "AS IS" BASIS, 66 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 67 | * See the License for the specific language governing permissions and 68 | * limitations under the License. 69 | * ========================================================= */ 70 | 71 | 72 | !function( $ ){ 73 | 74 | "use strict" 75 | 76 | /* MODAL CLASS DEFINITION 77 | * ====================== */ 78 | 79 | var Modal = function ( content, options ) { 80 | this.options = options 81 | this.$element = $(content) 82 | .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this)) 83 | } 84 | 85 | Modal.prototype = { 86 | 87 | constructor: Modal 88 | 89 | , toggle: function () { 90 | return this[!this.isShown ? 'show' : 'hide']() 91 | } 92 | 93 | , show: function () { 94 | var that = this 95 | 96 | if (this.isShown) return 97 | 98 | $('body').addClass('modal-open') 99 | 100 | this.isShown = true 101 | this.$element.trigger('show') 102 | 103 | escape.call(this) 104 | backdrop.call(this, function () { 105 | var transition = $.support.transition && that.$element.hasClass('fade') 106 | 107 | !that.$element.parent().length && that.$element.appendTo(document.body) //don't move modals dom position 108 | 109 | that.$element 110 | .show() 111 | 112 | if (transition) { 113 | that.$element[0].offsetWidth // force reflow 114 | } 115 | 116 | that.$element.addClass('in') 117 | 118 | transition ? 119 | that.$element.one($.support.transition.end, function () { that.$element.trigger('shown') }) : 120 | that.$element.trigger('shown') 121 | 122 | }) 123 | } 124 | 125 | , hide: function ( e ) { 126 | e && e.preventDefault() 127 | 128 | if (!this.isShown) return 129 | 130 | var that = this 131 | this.isShown = false 132 | 133 | $('body').removeClass('modal-open') 134 | 135 | escape.call(this) 136 | 137 | this.$element 138 | .trigger('hide') 139 | .removeClass('in') 140 | 141 | $.support.transition && this.$element.hasClass('fade') ? 142 | hideWithTransition.call(this) : 143 | hideModal.call(this) 144 | } 145 | 146 | } 147 | 148 | 149 | /* MODAL PRIVATE METHODS 150 | * ===================== */ 151 | 152 | function hideWithTransition() { 153 | var that = this 154 | , timeout = setTimeout(function () { 155 | that.$element.off($.support.transition.end) 156 | hideModal.call(that) 157 | }, 500) 158 | 159 | this.$element.one($.support.transition.end, function () { 160 | clearTimeout(timeout) 161 | hideModal.call(that) 162 | }) 163 | } 164 | 165 | function hideModal( that ) { 166 | this.$element 167 | .hide() 168 | .trigger('hidden') 169 | 170 | backdrop.call(this) 171 | } 172 | 173 | function backdrop( callback ) { 174 | var that = this 175 | , animate = this.$element.hasClass('fade') ? 'fade' : '' 176 | 177 | if (this.isShown && this.options.backdrop) { 178 | var doAnimate = $.support.transition && animate 179 | 180 | this.$backdrop = $('