diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..f7e953cf5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +; Ignore modules from make file. +modules/contrib +modules/aegir +themes/contrib +libraries diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..3798a26ad --- /dev/null +++ b/.travis.yml @@ -0,0 +1,97 @@ +language: php + +sudo: required + +# Only run test when committing to 1.x branch. +branches: + only: + - 7.x-1.x + +env: + global: + - DEVSHOP_VERSION=1.x + - SITE_HOSTS='dev.drup.devshop.travis dev.projectname.devshop.travis live.projectname.devshop.travis testenv.drpl8.devshop.travis' + + matrix: + - test="Upgrade" + COMMAND="robo up --test-upgrade" + UPGRADE_FROM_VERSION="1.0.0-beta10" + + - test="Docker Install" + COMMAND="robo up --test" + + - test="Ansible on Ubuntu 14.04 Apache" + COMMAND="robo up --mode=install.sh --test" + + - test="Ansible on Ubuntu 16.04 Apache" + COMMAND='robo up --mode=install.sh --install-sh-image=geerlingguy/docker-ubuntu1604-ansible --test' + +# - test="Install with Ansible on Ubuntu 14.04 with NGINX" +# COMMAND="robo up --mode=install.sh --test --install-sh-options='--server-webserver=nginx'" + +# - test="Ansible Install on CentOS 7" +# COMMAND="robo up --mode=install.sh --install-sh-image=geerlingguy/docker-centos7-ansible --test" + +# - test="Install with Ansible on Fedora 25" +# COMMAND="robo up --mode=install.sh --install-sh-image=centos:7 --test" + +services: + - docker + +before_install: + - pwd + - env + + # Install Robo + - wget https://github.com/consolidation/Robo/releases/download/1.0.5/robo.phar + - sudo mv robo.phar /usr/local/bin/robo + - sudo chmod +x /usr/local/bin/robo + + # Install Drush + - wget https://github.com/drush-ops/drush/releases/download/8.1.9/drush.phar + - sudo mv drush.phar /usr/local/bin/drush + - sudo chmod +x /usr/local/bin/drush + + # Install drupalorg_drush, for validating the makefile. + - drush dl drupalorg_drush-7.x-1.x + - drush verify-makefile + + # Clone devshop + - cd .. + - git clone http://github.com/opendevshop/devshop + - cd devshop + - git checkout -qf ${DEVSHOP_VERSION} + - git status + - pwd + - cp build-devmaster-travis-forks.make.yml build-devmaster-dev.make.yml + - cat build-devmaster-dev.make.yml + + # Prepare devshop CLI. + - composer install + +script: + - ${COMMAND} -n --fork + +# - container_id=$(mktemp) +# # Run container in detached state +# - 'sudo docker run --detach --name devshop_container --volume="${PWD}/devshop":/usr/share/devshop:rw --volume="${PWD}":/usr/share/devshop/devmaster:rw ${run_opts} -h devshop.travis --add-host "${SITE_HOSTS}":127.0.0.1 ${distribution}-${version}:ansible "${init}" > "${container_id}"' +# +# # Install script. +# - 'sudo docker exec devshop_container env TRAVIS=true TERM=xterm TRAVIS_BRANCH=$TRAVIS_BRANCH TRAVIS_REPO_SLUG=$TRAVIS_REPO_SLUG DEVSHOP_UPGRADE_TO_VERSION=$DEVSHOP_UPGRADE_TO_VERSION TRAVIS_PULL_REQUEST_BRANCH=$TRAVIS_PULL_REQUEST_BRANCH ${install_command} --makefile=/usr/share/devshop/devmaster/build-devmaster-test.make' +# +# # Hostmaster Status +# - 'sudo docker exec devshop_container env TERM=xterm sudo su - aegir -c "drush @hostmaster status"' +# +# # Turn off hosting queued, and the hosting task queue. +# - 'sudo docker exec devshop_container env sudo su - aegir -c "drush @hostmaster dis hosting_queued -y -v"' +# - 'sudo docker exec devshop_container env sudo su - aegir -c "drush @hostmaster vset hosting_queue_tasks_enabled 0 -y"' +# +# # Build and Run Tests +# - 'sudo docker exec devshop_container env TERM=xterm sudo su - -c "cd /usr/share/devshop/tests && composer update"' +# - 'sudo docker exec devshop_container env TERM=xterm sudo su - aegir -c "devshop devmaster:test"' +# +# # Stop container. +# - 'sudo docker stop devshop_container' + +notifications: + slack: thinkdrop:pb05x3ZL3qumHs0RjqEXvYfA diff --git a/API.txt b/API.txt new file mode 100644 index 000000000..fbb7fb258 --- /dev/null +++ b/API.txt @@ -0,0 +1,13 @@ +DevShop Provision API +===================== + +Document in Progress. + +Drush Hooks +----------- + +You can add drush hooks to act before or after devshop tasks. + +An example of a post-deploy hook is in the file deploy_hooks_examples/devshop.drush.inc + +Copy this file to sites/all/drush and use it for your specific site. \ No newline at end of file diff --git a/CODE-OF-CONDUCT.md b/CODE-OF-CONDUCT.md new file mode 100644 index 000000000..b31d53387 --- /dev/null +++ b/CODE-OF-CONDUCT.md @@ -0,0 +1,74 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at [INSERT EMAIL ADDRESS]. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + diff --git a/README.md b/README.md new file mode 100644 index 000000000..76f721ca9 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +DevShop DevMaster +================= + +This is the DevShop web-based front-end, called Devmaster. + +It is a Drupal Install Profile and Makefile, otherwise known as a "Distribution". + +Please fork this repo if you wish to contribute to the Drupal based front-end of DevShop. + +More information about DevShop can be found in the [main project repository](https://github.com/opendevshop/devshop). + +Issues & Development +==================== + +All issues for any DevShop repository are located in the main project: [https://github.com/opendevshop/devshop/issues](https://github.com/opendevshop/devshop/issues) + +To contribute to development, please see the [Development](https://docs.opendevshop.com/development.html) section of the documentation. diff --git a/VERSION.txt b/VERSION.txt new file mode 100644 index 000000000..525a4bd0e --- /dev/null +++ b/VERSION.txt @@ -0,0 +1 @@ +1.x \ No newline at end of file diff --git a/build-devmaster.make b/build-devmaster.make new file mode 100644 index 000000000..850ba28dc --- /dev/null +++ b/build-devmaster.make @@ -0,0 +1,13 @@ +; +; Loads the DevMaster install profile from drupal.org. +; +; This makefile is used by the DevShop standalone installer to build devmaster. +; + +core = 7.x +api = 2 + +includes[] = drupal-org-core.make +projects[devmaster][type] = profile +projects[devmaster][download][type] = git +projects[devmaster][download][branch] = 7.x-1.x \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 000000000..cd017603a --- /dev/null +++ b/composer.json @@ -0,0 +1,14 @@ +{ + "require": { + "cpliakas/git-wrapper": "~1.0", + "knplabs/github-api": "~1.2", + "sensiolabs/ansi-to-html": "^1.1", + "symfony/yaml": "^3.0", + "symfony/process": "^2.7" + }, + "config": { + "platform": { + "php": "5.6.0" + } + } +} \ No newline at end of file diff --git a/composer.lock b/composer.lock new file mode 100644 index 000000000..2e353da8e --- /dev/null +++ b/composer.lock @@ -0,0 +1,495 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "3177468b6bfc86758c5512500dda5296", + "packages": [ + { + "name": "cpliakas/git-wrapper", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/cpliakas/git-wrapper.git", + "reference": "1a2f1131ec9ebe04a0b729b141396fa55f992d44" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cpliakas/git-wrapper/zipball/1a2f1131ec9ebe04a0b729b141396fa55f992d44", + "reference": "1a2f1131ec9ebe04a0b729b141396fa55f992d44", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "symfony/event-dispatcher": "~2.3|~3.0", + "symfony/process": "~2.3|~3.0" + }, + "require-dev": { + "pdepend/pdepend": "~1.0", + "phploc/phploc": "~2.0", + "phpmd/phpmd": "~1.0", + "phpunit/phpunit": "~3.0", + "psr/log": "~1.0", + "scrutinizer/ocular": "~1.0", + "sebastian/phpcpd": "~2.0", + "symfony/filesystem": "~2.0" + }, + "suggest": { + "monolog/monolog": "Enables logging of executed git commands" + }, + "type": "library", + "autoload": { + "psr-0": { + "GitWrapper": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Pliakas", + "email": "opensource@chrispliakas.com" + } + ], + "description": "A PHP wrapper around the Git command line utility.", + "homepage": "https://github.com/cpliakas/git-wrapper", + "keywords": [ + "git" + ], + "time": "2016-04-19T16:12:33+00:00" + }, + { + "name": "guzzle/guzzle", + "version": "v3.9.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle3.git", + "reference": "0645b70d953bc1c067bbc8d5bc53194706b628d9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle3/zipball/0645b70d953bc1c067bbc8d5bc53194706b628d9", + "reference": "0645b70d953bc1c067bbc8d5bc53194706b628d9", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": ">=5.3.3", + "symfony/event-dispatcher": "~2.1" + }, + "replace": { + "guzzle/batch": "self.version", + "guzzle/cache": "self.version", + "guzzle/common": "self.version", + "guzzle/http": "self.version", + "guzzle/inflection": "self.version", + "guzzle/iterator": "self.version", + "guzzle/log": "self.version", + "guzzle/parser": "self.version", + "guzzle/plugin": "self.version", + "guzzle/plugin-async": "self.version", + "guzzle/plugin-backoff": "self.version", + "guzzle/plugin-cache": "self.version", + "guzzle/plugin-cookie": "self.version", + "guzzle/plugin-curlauth": "self.version", + "guzzle/plugin-error-response": "self.version", + "guzzle/plugin-history": "self.version", + "guzzle/plugin-log": "self.version", + "guzzle/plugin-md5": "self.version", + "guzzle/plugin-mock": "self.version", + "guzzle/plugin-oauth": "self.version", + "guzzle/service": "self.version", + "guzzle/stream": "self.version" + }, + "require-dev": { + "doctrine/cache": "~1.3", + "monolog/monolog": "~1.0", + "phpunit/phpunit": "3.7.*", + "psr/log": "~1.0", + "symfony/class-loader": "~2.1", + "zendframework/zend-cache": "2.*,<2.3", + "zendframework/zend-log": "2.*,<2.3" + }, + "suggest": { + "guzzlehttp/guzzle": "Guzzle 5 has moved to a new package name. The package you have installed, Guzzle 3, is deprecated." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.9-dev" + } + }, + "autoload": { + "psr-0": { + "Guzzle": "src/", + "Guzzle\\Tests": "tests/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Guzzle Community", + "homepage": "https://github.com/guzzle/guzzle/contributors" + } + ], + "description": "PHP HTTP client. This library is deprecated in favor of https://packagist.org/packages/guzzlehttp/guzzle", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "abandoned": "guzzlehttp/guzzle", + "time": "2015-03-18T18:23:50+00:00" + }, + { + "name": "knplabs/github-api", + "version": "1.7.1", + "source": { + "type": "git", + "url": "https://github.com/KnpLabs/php-github-api.git", + "reference": "98d0bcd2c4c96a40ded9081f8f6289907f73823c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/KnpLabs/php-github-api/zipball/98d0bcd2c4c96a40ded9081f8f6289907f73823c", + "reference": "98d0bcd2c4c96a40ded9081f8f6289907f73823c", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "guzzle/guzzle": "~3.7", + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.0", + "sllh/php-cs-fixer-styleci-bridge": "~1.3" + }, + "suggest": { + "knplabs/gaufrette": "Needed for optional Gaufrette cache" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8.x-dev" + } + }, + "autoload": { + "psr-4": { + "Github\\": "lib/Github/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Thibault Duplessis", + "email": "thibault.duplessis@gmail.com", + "homepage": "http://ornicar.github.com" + }, + { + "name": "KnpLabs Team", + "homepage": "http://knplabs.com" + } + ], + "description": "GitHub API v3 client", + "homepage": "https://github.com/KnpLabs/php-github-api", + "keywords": [ + "api", + "gh", + "gist", + "github" + ], + "time": "2016-07-26T08:49:38+00:00" + }, + { + "name": "sensiolabs/ansi-to-html", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/sensiolabs/ansi-to-html.git", + "reference": "8b5d787dca714bd98dd770c078d76528320a8286" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sensiolabs/ansi-to-html/zipball/8b5d787dca714bd98dd770c078d76528320a8286", + "reference": "8b5d787dca714bd98dd770c078d76528320a8286", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "suggest": { + "twig/twig": "Provides nice templating features" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-0": { + "SensioLabs\\AnsiConverter": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "A library to convert a text with ANSI codes to HTML", + "time": "2017-05-02T00:53:29+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v2.8.48", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "a77e974a5fecb4398833b0709210e3d5e334ffb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/a77e974a5fecb4398833b0709210e3d5e334ffb0", + "reference": "a77e974a5fecb4398833b0709210e3d5e334ffb0", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "^2.0.5|~3.0.0", + "symfony/dependency-injection": "~2.6|~3.0.0", + "symfony/expression-language": "~2.6|~3.0.0", + "symfony/stopwatch": "~2.3|~3.0.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony EventDispatcher Component", + "homepage": "https://symfony.com", + "time": "2018-11-21T14:20:20+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.10.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "e3d826245268269cd66f8326bd8bc066687b4a19" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19", + "reference": "e3d826245268269cd66f8326bd8bc066687b4a19", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + }, + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "time": "2018-08-06T14:22:27+00:00" + }, + { + "name": "symfony/process", + "version": "v2.8.48", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "c3591a09c78639822b0b290d44edb69bf9f05dc8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/c3591a09c78639822b0b290d44edb69bf9f05dc8", + "reference": "c3591a09c78639822b0b290d44edb69bf9f05dc8", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Process Component", + "homepage": "https://symfony.com", + "time": "2018-11-11T11:18:13+00:00" + }, + { + "name": "symfony/yaml", + "version": "v2.8.48", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "02c1859112aa779d9ab394ae4f3381911d84052b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/02c1859112aa779d9ab394ae4f3381911d84052b", + "reference": "02c1859112aa779d9ab394ae4f3381911d84052b", + "shasum": "" + }, + "require": { + "php": ">=5.3.9", + "symfony/polyfill-ctype": "~1.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Yaml Component", + "homepage": "https://symfony.com", + "time": "2018-11-11T11:18:13+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "platform-overrides": { + "php": "5.3.9" + } +} diff --git a/devmaster.info b/devmaster.info new file mode 100644 index 000000000..713f7d7c2 --- /dev/null +++ b/devmaster.info @@ -0,0 +1,79 @@ +name = OpenDevShop DevMaster +description = Web interface for OpenDevShop. +core = 7.x + +; Drupal core +dependencies[] = block +dependencies[] = help +dependencies[] = menu +dependencies[] = node +dependencies[] = system +dependencies[] = user +dependencies[] = update + +; Aegir core +dependencies[] = hosting +dependencies[] = hosting_task +dependencies[] = hosting_client +dependencies[] = hosting_db_server +dependencies[] = hosting_site +dependencies[] = hosting_package +dependencies[] = hosting_platform +dependencies[] = hosting_web_server +dependencies[] = hosting_server +dependencies[] = hosting_clone +dependencies[] = hosting_cron +dependencies[] = hosting_migrate +dependencies[] = hosting_git_checkout +dependencies[] = hosting_git_tag +dependencies[] = hosting_git_commit + +; DevShop Aegir +dependencies[] = hosting_alias +dependencies[] = hosting_queued +dependencies[] = hosting_http_basic_auth +dependencies[] = hosting_sync + +dependencies[] = hosting_filemanager +dependencies[] = hosting_logs +dependencies[] = hosting_tasks_extra +dependencies[] = hosting_backup_queue +dependencies[] = hosting_site_backup_manager +dependencies[] = hosting_https +dependencies[] = hosting_apache_https + +;dependencies[] = fix_permissions +;dependencies[] = fix_ownership + +; Drupal contrib +dependencies[] = navbar +dependencies[] = betterlogin +dependencies[] = chosen +dependencies[] = composer_manager +dependencies[] = views +dependencies[] = views_bulk_operations +dependencies[] = actions_permissions +dependencies[] = r4032login +dependencies[] = jquery_update +dependencies[] = module_filter +dependencies[] = hosting_letsencrypt + +; This breaks hosting.install. Removing it for now. +;dependencies[] = adminrole + +; DevShop +dependencies[] = devshop_hosting +dependencies[] = devshop_dothooks +dependencies[] = devshop_projects +dependencies[] = devshop_pull +dependencies[] = devshop_github +dependencies[] = devshop_testing +dependencies[] = aegir_ssh +dependencies[] = aegir_download +dependencies[] = aegir_update +dependencies[] = aegir_config +dependencies[] = devshop_permissions +dependencies[] = devshop_stats +dependencies[] = devshop_remotes +dependencies[] = devshop_support_network_client + diff --git a/devmaster.make b/devmaster.make new file mode 100644 index 000000000..b59a66edc --- /dev/null +++ b/devmaster.make @@ -0,0 +1,8 @@ +core = 7.x +api = 2 + +; Includes + +; This makefile will make sure we get the development code from the Aegir +; modules instead of the tagged releases. +includes[devmaster] = drupal-org.make diff --git a/devmaster.profile b/devmaster.profile new file mode 100644 index 000000000..1be0e6507 --- /dev/null +++ b/devmaster.profile @@ -0,0 +1,352 @@ +platform->server->http_service_type === 'nginx') { + module_enable(array('hosting_nginx')); + } + + // Bootstrap and create all the initial nodes + devmaster_bootstrap(); + + // Finalize and setup themes, menus, optional modules etc + devmaster_task_finalize(); +} + +function devmaster_bootstrap() { + /* Default node types and default node */ + $types = node_types_rebuild(); + + variable_set('install_profile', 'devmaster'); + + // Initialize the hosting defines + hosting_init(); + + /* Default client */ + $node = new stdClass(); + $node->uid = 1; + $node->type = 'client'; + $node->title = drush_get_option('client_name', 'admin'); + $node->status = 1; + node_save($node); + variable_set('hosting_default_client', $node->nid); + variable_set('hosting_admin_client', $node->nid); + + $client_id = $node->nid; + + /* Default server */ + $node = new stdClass(); + $node->uid = 1; + $node->type = 'server'; + $node->title = php_uname('n'); + $node->status = 1; + $node->hosting_name = 'server_master'; + $node->services = array(); + + /* Make it compatible with more than apache and nginx */ + $master_server = d()->platform->server; + + // Force https_apache + hosting_services_add($node, 'http', 'https_apache', array( + 'restart_cmd' => $master_server->http_restart_cmd, + 'port' => 80, + 'https_port' => 443, + 'available' => 1, + )); + + // Add Certificate service. + hosting_services_add($node, 'Certificate', 'LetsEncrypt', array( + 'letsencrypt_ca' => 'production' + )); + + /* examine the db server associated with the hostmaster site */ + $db_server = d()->db_server; + $master_db = parse_url($db_server->master_db); + /* if it's not the same server as the master server, create a new node + * for it */ + if ($db_server->remote_host == $master_server->remote_host) { + $db_node = $node; + } else { + $db_node = new stdClass(); + $db_node->uid = 1; + $db_node->type = 'server'; + $db_node->title = $master_db['host']; + $db_node->status = 1; + $db_node->hosting_name = 'server_' . $db_server->remote_host; + $db_node->services = array(); + } + hosting_services_add($db_node, 'db', $db_server->db_service_type, array( + 'db_type' => $master_db['scheme'], + 'db_user' => urldecode($master_db['user']), + 'db_passwd' => isset($master_db['pass']) ? urldecode($master_db['pass']) : '', + 'port' => 3306, + 'available' => 1, + )); + + drupal_set_message(st('Creating master server node')); + node_save($node); + if ($db_server->remote_host != $master_server->remote_host) { + drupal_set_message(st('Creating db server node')); + node_save($db_node); + } + variable_set('hosting_default_web_server', $node->nid); + variable_set('hosting_own_web_server', $node->nid); + + variable_set('hosting_default_db_server', $db_node->nid); + variable_set('hosting_own_db_server', $db_node->nid); + + // Create the hostmaster platform & packages + $node = new stdClass(); + $node->uid = 1; + $node->title = 'Drupal'; + $node->type = 'package'; + $node->package_type = 'platform'; + $node->short_name = 'drupal'; + $node->old_short_name = 'drupal'; + $node->description = 'Drupal code-base.'; + $node->status = 1; + node_save($node); + $package_id = $node->nid; + + // @TODO: We need to still call these nodes "hostmaster" because the aliases are still @hostmaster and @platform_hostmaster + $node = new stdClass(); + $node->uid = 1; + $node->type = 'platform'; + $node->title = 'hostmaster'; + $node->publish_path = d()->root; + $node->makefile = ''; + $node->verified = 1; + $node->web_server = variable_get('hosting_default_web_server', 2); + $node->platform_status = 1; + $node->status = 1; + $node->make_working_copy = 0; + node_save($node); + $platform_id = $node->nid; + variable_set('hosting_own_platform', $node->nid); + + $instance = new stdClass(); + $instance->rid = $node->nid; + $instance->version = VERSION; + $instance->filename = ''; + $instance->version_code = 1; + //$instance->schema_version = drupal_get_installed_schema_version('system'); + $instance->schema_version = 0; + $instance->package_id = $package_id; + $instance->status = 0; + $instance->platform = $platform_id; + hosting_package_instance_save($instance); + + // Create the hostmaster profile package node + $node = new stdClass(); + $node->uid = 1; + $node->title = 'devmaster'; + $node->type = 'package'; + $node->old_short_name = 'devmaster'; + $node->description = 'The Devmaster profile.'; + $node->package_type = 'profile'; + $node->short_name = 'devmaster'; + $node->status = 1; + node_save($node); + $profile_id = $node->nid; + + $instance = new stdClass(); + $instance->rid = $node->nid; + $instance->version = VERSION; + $instance->filename = ''; + $instance->version_code = 1; + //$instance->schema_version = drupal_get_installed_schema_version('system'); + $instance->schema_version = 0; + $instance->package_id = $profile_id; + $instance->status = 0; + $instance->platform = $platform_id; + hosting_package_instance_save($instance); + + // Create the main Aegir site node + $node = new stdClass(); + $node->uid = 1; + $node->type = 'site'; + $node->title = d()->uri; + $node->platform = $platform_id; + $node->client = $client_id; + $node->db_name = ''; + $node->db_server = $db_node->nid; + $node->profile = $profile_id; + $node->import = true; + $node->hosting_name = 'hostmaster'; + $node->site_status = 1; + $node->verified = 1; + $node->status = 1; + + // If this site's hostname has a public DNS record, then LetsEncrypt will + // work, so set the hostmaster site node https_enabled = HOSTING_HTTPS_REQUIRED + $records = dns_get_record($node->title); + foreach ($records as $record) { + if ( + ($record['type'] == 'A' || $record['type'] == 'CNAME') && + $record['ip'] != '127.0.0.1' && + $record['ip'] != '127.0.1.1') { + $node->https_enabled = HOSTING_HTTPS_ENABLED; + + drupal_set_message(t('Public DNS found for !url. Enabling HTTPS with LetsEncrypt.', array( + '!url' => $node->title, + )), 'success'); + } + } + if ($node->https_enabled != HOSTING_HTTPS_ENABLED) { + drupal_set_message(t('No public DNS record found for !url. Not enabling LetsEncrypt HTTPS for Hostmaster site.', array( + '!url' => $node->title, + )), 'warning'); + $node->https_enabled = HOSTING_HTTPS_DISABLED; + } + + node_save($node); + + // Save the hostmaster site nid. + variable_set('aegir_hostmaster_site_nid', $node->nid); + + // Enable the hosting features of modules that we enable by default. + // The module will already be enabled, + // this makes sure we also set the default permissions. + $default_hosting_features = array( + 'hosting_web_server' => 'web_server', + 'hosting_db_server' => 'db_server', + 'hosting_platform' => 'platform', + 'hosting_client' => 'client', + 'hosting_task' => 'task', + 'hosting_server' => 'server', + 'hosting_package' => 'package', + 'hosting_site' => 'site', + 'hosting' => 'hosting', + ); + hosting_features_enable($default_hosting_features, $rebuild = TRUE, $enable = FALSE); + + // Set the frontpage + variable_set('site_frontpage', 'devshop'); + + // Set the sitename + variable_set('site_name', 'DevShop'); + + // do not allow user registration: the signup form will do that + variable_set('user_register', 0); + + // This is saved because the config generation script is running via drush, and does not have access to this value + variable_set('install_url' , $GLOBALS['base_url']); + + // Disable backup queue for sites by default. + variable_set('hosting_backup_queue_default_enabled', 0); + + // Set hosting_logs default folder. + variable_set('provision_logs_file_path', '/var/log/aegir'); +} + +function devmaster_task_finalize() { + + // Enable "boots" theme. + drupal_set_message(st('Enabling "boots" theme')); + $theme = 'boots'; + theme_enable(array($theme)); + variable_set('theme_default', $theme); + + // Disable the default Bartik theme + theme_disable(array('bartik')); + + drupal_set_message(st('Configuring default blocks')); + devmaster_place_blocks($theme); + + // Save "menu_options" for our content types, so they don't offer to be put in menus. + variable_set('menu_options_client', ''); + variable_set('menu_options_platform', ''); + variable_set('menu_options_server', ''); + variable_set('menu_options_site', ''); + +// drupal_set_message(st('Configuring default blocks')); +// install_add_block('devshop_hosting', 'devshop_tasks', $theme, 1, 5, 'header', 1); +// +// // @TODO: CREATE DEVSHOP ROLES! +// drupal_set_message(st('Configuring roles')); +// install_remove_permissions(install_get_rid('anonymous user'), array('access content', 'access all views')); +// install_remove_permissions(install_get_rid('authenticated user'), array('access content', 'access all views')); +// install_add_permissions(install_get_rid('anonymous user'), array('access disabled sites')); +// install_add_permissions(install_get_rid('authenticated user'), array('access disabled sites')); +// +// // Create administrator role +// $rid = install_add_role('administrator'); +// variable_set('user_admin_role', $rid); + + // Hide errors from the screen. + variable_set('error_level', 0); + + // Disable Aegir's "Welcome" page + variable_set('hosting_welcome_page', 0); + + // Disable menu settings for projects + variable_set('menu_options_project', ''); + + // Force things to delete even if things fail. + variable_set('hosting_delete_force', 1); + + // Don't require users to have a client to create a site. + variable_set('hosting_client_require_client_to_create_site', 0); + + // Don't automatically import sites. + variable_set('hosting_platform_automatic_site_import', 0); + + // Make sure "chosen" widget allows "contains" string searching. + variable_set('chosen_search_contains', 1); + variable_set('chosen_jquery_selector', 'select:visible'); + variable_set('chosen_minimum_single', 0); + variable_set('chosen_minimum_multiple', 0); + + // Make sure blocks are setup properly. +// _block_rehash(); + + // Rebuild node access permissions. + node_access_rebuild(); +} + +/** + * Helper function to place block. + */ +function devmaster_place_blocks($theme) { + $blocks = array( + array( + 'module' => 'devshop_projects', + 'delta' => 'project_nav', + 'theme' => $theme, + 'status' => 1, + 'weight' => -1, + 'region' => 'header', + 'visibility' => 0, + 'pages' => '', + 'cache' => -1, + ), + array( + 'module' => 'devshop_projects', + 'delta' => 'project_create', + 'theme' => $theme, + 'status' => 1, + 'weight' => -1, + 'region' => 'sidebar_first', + 'visibility' => 0, + 'pages' => '', + 'cache' => -1, + ), + ); + + $query = db_insert('block')->fields(array('module', 'delta', 'theme', 'status', 'weight', 'region', 'visibility', 'pages', 'cache')); + + foreach ($blocks as $block) { + $query->values($block); + } + $query->execute(); + +} diff --git a/drupal-org-core.make b/drupal-org-core.make new file mode 100644 index 000000000..c9bcc1b0c --- /dev/null +++ b/drupal-org-core.make @@ -0,0 +1,11 @@ +core = 7.x +api = 2 + +; PLEASE NOTE: DevShop installs from the makefile https://github.com/opendevshop/devshop/blob/1.x/build-devmaster.make +; THIS VERSION NUMBER HAS NO EFFECT when using the devshop install process. This only affects drupal.org tarballs. +; Make sure you change Drupal core version number in the files: +; - https://github.com/opendevshop/devshop/blob/1.x/build-devmaster.make +; - https://github.com/opendevshop/devshop/blob/1.x/build-devmaster-dev.yml +; - https://github.com/opendevshop/devshop/blob/1.x/build-devmaster-travis-forks.make.yml +projects[drupal][type] = core +projects[drupal][version] = 7.65 diff --git a/drupal-org.make b/drupal-org.make new file mode 100644 index 000000000..1ab390b71 --- /dev/null +++ b/drupal-org.make @@ -0,0 +1,114 @@ +core = 7.x +api = 2 + +defaults[projects][subdir] = "contrib" +defaults[projects][type] = "module" + +; Update this with each new release of devshop +projects[devshop_stats][version] = 1.x +projects[devshop_stats][subdir] = "contrib" + +; Aegir Modules +; For development, use latest branch. +; For release, use tagged version +projects[hosting][subdir] = aegir +projects[hosting][version] = "3.170" +projects[hosting][patch][] = "https://www.drupal.org/files/issues/2018-12-12/3020169-permission-check.patch" +projects[hosting][patch][] = "https://www.drupal.org/files/issues/2018-12-05/3018114-client-optional.patch" +projects[hosting][patch][] = "https://www.drupal.org/files/issues/2018-12-10/3019462-administer-servers.patch" + +; Aegir Core not included in hosting.module +projects[eldir][type] = theme +projects[eldir][version] = "3.170" + +projects[hosting_git][subdir] = aegir +projects[hosting_git][version] = "3.170" + +projects[hosting_https][subdir] = aegir +projects[hosting_https][version] = "3.171" + +projects[hosting_remote_import][subdir] = aegir +projects[hosting_remote_import][version] = "3.170" + +projects[hosting_site_backup_manager][subdir] = aegir +projects[hosting_site_backup_manager][version] = "3.170" + +projects[hosting_tasks_extra][subdir] = aegir +projects[hosting_tasks_extra][version] = "3.170" + +projects[hosting_logs][subdir] = aegir +projects[hosting_logs][version] = "3.170" + +projects[hosting_filemanager][subdir] = aegir +projects[hosting_filemanager][version] = "1.x" + +projects[aegir_ssh][subdir] = aegir +projects[aegir_ssh][version] = 1.0 + +projects[aegir_config][subdir] = aegir +projects[aegir_config][version] = 1.00-beta1 + +; Not working yet. +;projects[hosting_solr][version] = "1" + +; Contrib Modules +projects[sshkey][version] = "2.0" +projects[betterlogin][version] = 1.5 +projects[composer_manager][version] = 1.8 +projects[entity][version] = 1.9 +projects[openidadmin][version] = 1.0 +projects[overlay_paths][version] = 1.3 +projects[r4032login][version] = 1.8 +projects[admin_menu][version] = "3.0-rc6" +projects[adminrole][version] = "1.1" +projects[jquery_update][version] = "3.0-alpha5" +projects[views][version] = "3.22" +projects[views_bulk_operations][version] = "3.5" +projects[ctools][version] = "1.15" +projects[features][version] = "2.11" +projects[distro_update][version] = "1" +projects[module_filter][version] = "2.2" +projects[libraries][version] = 2.5 +projects[token][version] = 1.7 +; projects[hybridauth][version] = 2.15 +projects[statsd][version] = 1.1 +projects[hosting_statsd][version] = 1.0-beta1 +projects[intercomio][version] = 1.0-beta2 +projects[navbar][version] = 1.7 +projects[chosen][version] = 2.1 + +projects[cas][version] = 1.7 +projects[cas][patch][] = "https://www.drupal.org/files/issues/2018-12-13/3020349-cas-library-path.patch" +projects[cas_attributes][version] = 1.0-rc3 + +; Bootstrap base theme +projects[bootstrap][type] = theme +projects[bootstrap][version] = "3.23" + +; Timeago module +projects[timeago][version] = 2.3 + +; JQuery TimeAgo plugin +libraries[timeago][download][type] = get +libraries[timeago][download][url] = https://raw.githubusercontent.com/rmm5t/jquery-timeago/v1.5.3/jquery.timeago.js +libraries[timeago][destination] = libraries + +; @TODO: Uncomment once it is in the whitelist: https://www.drupal.org/project/drupalorg_whitelist/issues/3024898 +; Library: Modernizr +; libraries[modernizr][download][type] = git +; libraries[modernizr][download][url] = https://github.com/BrianGilbert/modernizer-navbar.git +; libraries[modernizr][download][revision] = 5b89d9225320e88588f1cdc43b8b1e373fa4c60f + +; Library: Backbone +libraries[backbone][download][type] = git +libraries[backbone][download][url] = https://github.com/jashkenas/backbone.git +libraries[backbone][download][tag] = 1.0.0 + +; Library: Underscore +libraries[underscore][download][type] = git +libraries[underscore][download][url] = https://github.com/jashkenas/underscore.git +libraries[underscore][download][tag] = 1.5.0 + +; Library: Chosen +libraries[chosen][download][type] = "git" +libraries[chosen][download][url] = "https://github.com/harvesthq/chosen-package.git" diff --git a/logo.png b/logo.png new file mode 100644 index 000000000..13c356bc0 Binary files /dev/null and b/logo.png differ diff --git a/modules/devshop/README.txt b/modules/devshop/README.txt new file mode 100644 index 000000000..95fcdd2c7 --- /dev/null +++ b/modules/devshop/README.txt @@ -0,0 +1,59 @@ + +DevShop Hosting +=============== +Drupal development infrastructure made easy. + +This module provides the front-end interface needed to +deploy and manage sites using the DevShop git and features +based development workflow. + +About DevShop +------------- +The goals of DevShop are... + +1. to simplify management of multiple environments for multiple Drupal projects. +2. to provide web-based tools that streamline the Drupal site building workflow. +3. to provide a common, open-source infrastructure for Drupal development shops. + + +Installation +------------ +For installation instructions, see INSTALL.txt. + + +DevShop Projects +---------------- +DevShop functionality centers around "Projects". Aegir Project nodes store a +Git URL, the code path, the "base url", and the branches of the remote +repository. + +DevShop allows multiple platforms and sites (for dev, test, or live purposes) +to be created very easily. Platforms can be easily created from existing +branches of your git repositories. + + +Creating Projects +----------------- + +To create a new project, visit either the Projects page or click +"Create Content" > "DevShop Project". + +### Step 1: Git URL and project name. + +Enter your project's Git URL and Project Name. + +NOTE: Your project's git repo must be a complete drupal core file set. It +should match the structure of Drupal core's git repository, and can be a clone +of http://git.drupalcode.org/project/drupal.git + +### Step 2: File Path and Base URL + +Enter the base path to the Project's code. Recommended is /var/aegir/projects + +Enter the base URL for the project. All Project Sites will be on a subdomain of this base URL. + +### Step 3: Choose Platforms + +To complete this step, the Verify Project task must finish. + +On this page, you choose if you want dev, test, or live platforms and what branches each should live on. You can also choose the branches of your git repository you wish to create platforms and sites for. diff --git a/modules/devshop/aegir_download/aegir_download.info b/modules/devshop/aegir_download/aegir_download.info new file mode 100644 index 000000000..6a3ff40ed --- /dev/null +++ b/modules/devshop/aegir_download/aegir_download.info @@ -0,0 +1,4 @@ +name = Aegir Download +description = Adds the ability to download packages to aegir hosted sites. +core = 7.x +dependencies[] = hosting_site \ No newline at end of file diff --git a/modules/devshop/aegir_download/aegir_download.module b/modules/devshop/aegir_download/aegir_download.module new file mode 100644 index 000000000..9c2e6ada7 --- /dev/null +++ b/modules/devshop/aegir_download/aegir_download.module @@ -0,0 +1,84 @@ + t('Download Modules'), + 'description' => t('Add modules or themes to your git repository.'), + 'dialog' => TRUE, + 'icon' => 'download' + ); + return $tasks; +} + +/** + * Implements hook_permission(). + * @return array + */ +function aegir_download_permission() { + return array( + 'create download task' => array( + 'title' => t('create download task'), + 'description' => t('Create "download" task.'), + ), + ); +} + +/** + * @return mixed + */ +function hosting_task_download_form() { + + $form['packages'] = array( + '#type' => 'textarea', + '#title' => t('Drupal modules or themes to download'), + '#description' => '

' . t('Enter the names of the drupal module or themes you would like to download to your project. The names must match the package system name: If you want the module from http://drupal.org/project/views, enter "views" into this field. Separate multiple packages with a space.') . '

' . t('If you enter the name of an existing module, it will overwrite your old version. This is a good way to update your modules. Run Update.php to ensure smooth deployment.') . '

', + ); + $form['commit'] = array( + '#title' => t('Commit to git.'), + '#type' => 'checkbox', + '#default_value' => 1, + ); + $form['message'] = array( + '#type' => 'textarea', + '#title' => t('Git Commit Message'), + '#description' => t('Enter a message to use in the git commit message.'), + ); + $form['update'] = array( + '#title' => t('Run Database Updates'), + '#type' => 'checkbox', + '#default_value' => 1, + '#description' => t('If module updates occur, run update.php to update the database. Only use if you trust the modules you are downloading.'), + ); + return $form; +} + +/** + * Implements hook_devshop_environment_menu(). + * + * Defines the list of tasks that appear under the gear icon. + */ +function aegir_download_devshop_environment_menu($environment) { + + if ($environment->site && $environment->site_status == HOSTING_SITE_ENABLED) { + $items[] = 'download'; + } + return $items; +} + +function drush_aegir_download_pre_hosting_task() +{ + $task =& drush_get_context('HOSTING_TASK'); + if ($task->task_type != 'download') { + return; + } + + drush_log('[AEGIR] Download package enabled...', 'ok'); + + $task->options['packages'] = $task->task_args['packages']; + $task->options['commit'] = $task->task_args['commit']; + $task->options['message'] = $task->task_args['message']; + $task->options['update'] = $task->task_args['update']; +} diff --git a/modules/devshop/aegir_download/hosting.feature.aegir_download.inc b/modules/devshop/aegir_download/hosting.feature.aegir_download.inc new file mode 100644 index 000000000..c5d501920 --- /dev/null +++ b/modules/devshop/aegir_download/hosting.feature.aegir_download.inc @@ -0,0 +1,16 @@ + t('Download Modules'), + 'description' => t('Add modules or themes to your git repository.'), + 'status' => HOSTING_FEATURE_ENABLED, + 'module' => 'aegir_download', + 'group' => 'advanced', + ); + return $features; +} diff --git a/modules/devshop/aegir_update/aegir_update.info b/modules/devshop/aegir_update/aegir_update.info new file mode 100644 index 000000000..7482630bc --- /dev/null +++ b/modules/devshop/aegir_update/aegir_update.info @@ -0,0 +1,4 @@ +name = Aegir Update +description = Adds the ability to update your Drupal site using Aegir. +core = 7.x +dependencies[] = hosting_site \ No newline at end of file diff --git a/modules/devshop/aegir_update/aegir_update.module b/modules/devshop/aegir_update/aegir_update.module new file mode 100644 index 000000000..70a4bc6d7 --- /dev/null +++ b/modules/devshop/aegir_update/aegir_update.module @@ -0,0 +1,84 @@ + t('Update Drupal'), + 'description' => t('Upgrades drupal core and contrib to the latest versions.'), + 'dialog' => TRUE, + 'icon' => 'wrench' + ); + return $tasks; +} + +/** + * Implements hook_permission(). + * @return array + */ +function aegir_update_permission() { + return array( + 'create update_drupal task' => array( + 'title' => t('create update_drupal task'), + 'description' => t('Create "Update Drupal" task.'), + ), + ); +} + +/** + * @return mixed + */ +function hosting_task_update_drupal_form() { + $form = array(); + $form['note'] = array( + '#markup' => t('This will run the command drush pm-update on your Drupal site.'), + '#prefix' => '

', + '#suffix' => '

', + ); + $form['warning'] = array( + '#markup' => t('Running this task may have unexpected consequences. It is not recommended to run on a production site.'), + '#prefix' => '
', + '#suffix' => '
', + ); + $form['commit'] = array( + '#type' => 'checkbox', + '#title' => t('Commit & Push all changes'), + '#description' => t('After running the update, commit and push all changes.'), + '#default_value' => 1, + ); + return $form; +} + +/** + * Implements hook_post_hosting_TASK_TYPE_task(). + * + * Trigger a verify task for the site and the platform. + */ +function aegir_update_post_hosting_update_drupal_task($task, $data) { + + $account = user_load($task->uid); + if (isset($task->task_args['commit']) && $task->task_args['commit']) { + hosting_add_task($task->ref->nid, 'commit', array( + 'push' => 1, + 'name' => $account->name, + 'mail' => $account->mail, + )); + } + + hosting_add_task($task->ref->platform, 'verify'); + hosting_add_task($task->ref->nid, 'verify'); +} + +/** + * Implements hook_devshop_environment_menu(). + * + * Defines the list of tasks that appear under the gear icon. + */ +function aegir_update_devshop_environment_menu($environment) { + + if ($environment->site && $environment->site_status == HOSTING_SITE_ENABLED) { + $items[] = 'update_drupal'; + } + return $items; +} diff --git a/modules/devshop/aegir_update/drush/aegir_update.drush.inc b/modules/devshop/aegir_update/drush/aegir_update.drush.inc new file mode 100644 index 000000000..c3cbce0fb --- /dev/null +++ b/modules/devshop/aegir_update/drush/aegir_update.drush.inc @@ -0,0 +1,55 @@ + 'Updates drupal and contributed packages.', + 'bootstrap' => DRUSH_BOOTSTRAP_DRUSH, + ); + return $items; +} + +/** + * Implements the provision-update command. + */ +function drush_aegir_update_provision_update_drupal() { + drush_errors_on(); + + if (drush_drupal_major_version(d()->root) == 8) { +// drush_log('Drush command provision-update_drupal is not tet compatible with Drupal 8. Run the command `composer update drupal/core webflo/drupal-core-require-dev symfony/* --with-dependencies'); + + $path = d()->platform->repo_path? d()->platform->repo_path: d()->platform->root; + if (file_exists($path . '/composer.json')) { + provision_process('composer update drupal/core webflo/drupal-core-require-dev symfony/* --with-dependencies', $path); + } + else { + drush_set_error('PROVISION_ERROR', dt('Drupal 8 was detected, but a composer.json file was not found in %path. Updating Drupal 8 automatically requires using a composer-based platform. See https://github.com/drupal-composer/drupal-project for the starting template.', [ + '%path' => $path, + ])); + } + } + elseif (drush_drupal_major_version(d()->root) <= 7) { + provision_backend_invoke(d()->name, 'pm-update'); + } + drush_log(dt('Drush pm-update task completed')); +} + +/** + * Map values of site node into command line arguments. + */ +function drush_aegir_update_pre_hosting_task($task) { + $task = &drush_get_context('HOSTING_TASK'); + if ($task->task_type == 'update_drupal' && !empty($task->task_args['commit'])) { + // Pass the argument provision_git expects. + $task->options['commit'] = $task->task_args['commit']; + } +} diff --git a/modules/devshop/aegir_update/hosting.feature.aegir_update.inc b/modules/devshop/aegir_update/hosting.feature.aegir_update.inc new file mode 100644 index 000000000..d6d4e07ec --- /dev/null +++ b/modules/devshop/aegir_update/hosting.feature.aegir_update.inc @@ -0,0 +1,16 @@ + t('Update Drupal'), + 'description' => t('Upgrades drupal core and contrib to the latest versions.'), + 'status' => HOSTING_FEATURE_DISABLED, + 'module' => 'aegir_update', + 'group' => 'advanced', + ); + return $features; +} diff --git a/modules/devshop/devshop.css b/modules/devshop/devshop.css new file mode 100644 index 000000000..31fd15076 --- /dev/null +++ b/modules/devshop/devshop.css @@ -0,0 +1,130 @@ +div#header div.logo a { + text-indent: -999px; + overflow: hidden; + height: 72px !important; + width: 72px !important; + margin: 10px; +} + +#devshop-project-create-step-environments .platform { + clear: both; + border-bottom: 1px solid #e8e8e8; +} +#devshop-project-create-step-environments .platform .form-item { + float: left; + width: 46%; + clear: none; + height: 2.0em; + border-bottom: none; +} +body.aegir #page div.node div.content fieldset.project-environments div.form-item { + padding: 5px; +} + +fieldset.project-environments { + clear: both; +} + +#project-settings-table .description, +#project-settings-table .form-item-labeled label +{ + display: none; +} + +textarea#rsa { + display: none; +} + +.form-submit { + margin-top: 10px; +} +#edit-cancel { + float: right; +} +.command input { + font-family: Courier New, monospace; + width: 100%; +} +#footer { + height: 100px !important; + line-height: inherit !important; +} +#footer h2 { + border: none; + font-weight: normal; + text-transform: uppercase; +} +#footer .block { + float: left; + margin-right: 20px; + +} + +#block-system-0.block { + float: right; +} + +body.node-type-project a.hosting-goto-site-link { + background: none; + padding: inherit; +} + +/** + * Task Block! + */ +#block-devshop_hosting-devshop_tasks { + position: absolute; + top: 0px; + margin: 0px auto; + width: 940px; +} + + +#block-devshop_hosting-devshop_tasks .content { + position: absolute; + right: 0px; + top: 40px; +} + +#block-devshop_hosting-devshop_tasks #devshop-task-status { + font: bold 13px/20px "Helvetica Neue",Arial,sans-serif; + border: 0px; + padding: 5px 10px 5px 30px; + color: #fff; + background: #666; + cursor: pointer; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + width: 200px; +} + +#block-devshop_hosting-devshop_tasks #devshop-task-status.active { + background: #666 5px no-repeat url('devshop_projects/images/spinner-white.gif'); +} + +#block-devshop_hosting-devshop_tasks #devshop-task-status.inactive { + background: #666; +} + + +#hosting-task-queue-block { + display: none; + width: 96%; + margin: 0px auto; +} + + +#block-devshop_hosting-devshop_tasks:hover #hosting-task-queue-block { + display: block; +} + + +#block-devshop_hosting-devshop_tasks .task-logs-link { + display: block; + padding: 5px 10px; + background: #999; + color: white; + text-align: center; + font-size: 10pt; + text-transform: uppercase; +} diff --git a/modules/devshop/devshop_acquia/README.md b/modules/devshop/devshop_acquia/README.md new file mode 100644 index 000000000..b9257134a --- /dev/null +++ b/modules/devshop/devshop_acquia/README.md @@ -0,0 +1,50 @@ +Acquia Cloud Hooks Integration +============================== + +DevShop now supports firing Acquia Cloud Hooks when deploying to environments. + +The code in acquia.drush.inc will detect an acquia repo and trigger your hooks +to run. + +See the Acquia Cloud Hooks repository for more information and samples: https://github.com/acquia/cloud-hooks + +Currently, only the `post-code-update` hook is supported. More to come. + +Hooks +===== + +### post-code-deploy + +Changing branch or tag manually. + +DevShop Equivalent: provision-devshop-deploy when triggered from front-end. + +``` +post-code-deploy site target-env source-branch deployed-tag repo-url repo-type +``` + +### post-code-update + +Deploying code to a branch environment automatically. + +DevShop Equivalent: provision-devshop-deploy + +``` +post-code-deploy site target-env source-branch deployed-tag repo-url repo-type +``` + +### post-db-copy + +**Coming Soon...* + +``` +post-db-copy site target-env db-name source-env +``` + +### post-files-copy + +**Coming Soon...* + +``` +post-files-copy mysite prod dev +``` \ No newline at end of file diff --git a/modules/devshop/devshop_acquia/devshop_acquia.info b/modules/devshop/devshop_acquia/devshop_acquia.info new file mode 100644 index 000000000..46150d210 --- /dev/null +++ b/modules/devshop/devshop_acquia/devshop_acquia.info @@ -0,0 +1,5 @@ +name = DevShop Acquia Integration +description = Allows for Acquia Cloud Hooks to be used as deploy hooks in DevShop. +core = 7.x +package = DevShop +dependencies[] = devshop_hosting diff --git a/modules/devshop/devshop_acquia/devshop_acquia.module b/modules/devshop/devshop_acquia/devshop_acquia.module new file mode 100644 index 000000000..34487da19 --- /dev/null +++ b/modules/devshop/devshop_acquia/devshop_acquia.module @@ -0,0 +1,151 @@ +ref->type != 'site') { + return; + } + + // If this is a deploy task, and acquia hooks is not configured to run, return. + if ($task->task_type == 'devshop-deploy' && (!isset($task->task_args['acquia_hooks']) || $task->task_args['acquia_hooks'] != 1)) { + return; + } + + $environment = (object) $task->ref->environment; + $project = (object) $task->ref->project; + + // If project has no path to drupal, we know it's not acquia. + if ($project->drupal_path != 'docroot' || !file_exists($environment->repo_path . '/hooks')) { + drush_log('Acquia Cloud Hooks error', 'p_command'); + drush_log('./docroot or ./hooks folder is missing, but Acquia hooks are configured to run. Please create the hooks or turn off Acquia Cloud Hooks for the project.', 'p_error'); + return drush_set_error('DRUSH_ERROR'); + } + + // Determine the hook to run + $hooks = array(); + + // If "manual" deployment, it's a post-code-deploy + if (isset($task->task_args['manual']) && $task->task_args['manual'] == 1) { + $hooks[] = 'post-code-deploy'; + } + // If it is automatic, it's a "post-code-update" + elseif ($task->task_type == 'devshop_deploy') { + $hooks[] = 'post-code-update'; + } + // If task is sync... + elseif ($task->task_type == 'sync') { + // If database is copied, run post-db-copy hook. + if ($task->task_args['database']) { + $hooks[] = 'post-db-copy'; + } + // If files are copied, run post-files-copy hook. + if ($task->task_args['files']) { + $hooks[] = 'post-files-copy'; + } + } + + foreach ($hooks as $hook) { + + drush_log("[DEVSHOP] Invoking acquia hooks for hook $hook in environment {$environment->name} ... Scanning {$environment->repo_path}/hooks/common/{$hook} & {$environment->repo_path}/hooks/{$environment->name}/{$hook} ", 'ok'); + + // Collect cloud hook scripts to run for all environments "common". + $files = scandir("{$environment->repo_path}/hooks/common/{$hook}"); + if (empty($files)) $files = array(); + + // The list of files we will run. + $scripts = array(); + + // Remove incorrect entries from scandir. + $files = array_diff($files, array('..', '.', '.gitignore')); + + // Collect the full path to each script. + foreach ($files as $script) { + $scripts[] = realpath("{$environment->repo_path}/hooks/common/{$hook}/{$script}"); + } + + // Collect cloud hook scripts to run for this environment. + if (file_exists("{$environment->repo_path}/hooks/{$environment->name}/{$hook}")) { + + $files = scandir("{$environment->repo_path}/hooks/{$environment->name}/{$hook}"); + + // Remove incorrect entries from scandir. + $files= array_diff($files, array('..', '.', '.gitignore')); + + // Collect the full path to each script. + foreach ($files as $script) { + $scripts[] = realpath("{$environment->repo_path}/hooks/{$environment->name}/{$hook}/{$script}"); + } + } + + // Notify logs how many scripts we've found + $count = count($scripts); + drush_log("[DEVSHOP] Found $count scripts to run...", 'ok'); + + // Run Scripts + // @TODO: Implement using symfony:Process component. + // Usage: post-code-deploy site target-env source-branch deployed-tag repo-url repo-type + foreach ($scripts as $file) { + drush_log('[DEVSHOP] Running Acquia Cloud Hook: ' . $file, 'ok'); + $hook_path = str_replace($environment->repo_path, '', $file); + drush_log("[Acquia Cloud Hook $hook] $hook_path", 'p_command'); + + // This is a trick. DevShop doesn't have aliases like $project.$environment. + // When Acquia cloud hook writers create a script, they will use something like: + // + // drush_alias=$site'.'$target_env + // drush @$drush_alias cc all + // + // So, by passing "environment->name" as $site, and project.hostname as $target_env, + // we can trick the Acquia cloud script to use the right alias. + $alias = explode('.', $environment->drush_alias); + $environment_name = substr($alias[0], 1); + + $command = "sh $file {$environment_name} {$project->base_url} {$environment_name} old_branch {$environment->git_ref} repo_url repo_type"; + + drush_log('[Running] ' . $command, 'p_log'); + + $process = new Process($command); + $process->setTimeout(NULL); + $exit_code = $process->run(function ($type, $buffer) { + if (Process::ERR === $type) { + drush_log($buffer, 'p_ok'); + } + else { + drush_log($buffer, 'p_ok'); + } + }); + + // check exit code + if ($exit_code === 0) { + drush_log("[.hooks] Command executed successfully.", 'ok'); + } + else { + drush_log("[.hooks] Hook command failed: $command", 'error'); + drush_log($command, 'p_error'); + } + } + } +} diff --git a/modules/devshop/devshop_acquia/example.slack.sh b/modules/devshop/devshop_acquia/example.slack.sh new file mode 100644 index 000000000..3c4f848d1 --- /dev/null +++ b/modules/devshop/devshop_acquia/example.slack.sh @@ -0,0 +1,39 @@ +#!/bin/sh +# +# Altered from https://github.com/acquia/cloud-hooks/tree/master/samples/slack +# Requires a /var/aegir/slack_settings file that contains: +# +# SLACK_WEBHOOK_URL=https://example.slack.com/services/hooks/incoming-webhook?token=TOKEN +# +# Cloud Hook: post-code-deploy +# +# The post-code-deploy hook is run whenever you use the Workflow page to +# deploy new code to an environment, either via drag-drop or by selecting +# an existing branch or tag from the Code drop-down list. See +# ../README.md for details. +# +# Usage: post-code-deploy site target-env source-branch deployed-tag repo-url +# repo-type + +site="$1" +target_env="$2" +source_branch="$3" +deployed_tag="$4" +repo_url="$5" +repo_type="$6" +source="$7" +site_url="$8" + +# Load the Slack webhook URL (which is not stored in this repo). +. $HOME/slack_settings + +if [ $source = 'devshop' ]; then + source="DevShop" + image="https://www.drupal.org/files/project-images/devshop-icon.png" +else + source="Acquia Cloud" + image="https://pbs.twimg.com/profile_images/1901642489/cloud_icon_150.png" +fi + +# Post deployment notice to Slack +curl -X POST --data-urlencode "payload={\"channel\": \"#dev-engageny\", \"username\": \"$source\", \"text\": \"Git branch \`$deployed_tag\` updated on *$target_env*: $site_url.\", \"icon_url\": \"$image\"}" $SLACK_WEBHOOK_URL diff --git a/modules/devshop/devshop_bitbucket/devshop_bitbucket.drush.inc b/modules/devshop/devshop_bitbucket/devshop_bitbucket.drush.inc new file mode 100644 index 000000000..cb5400855 --- /dev/null +++ b/modules/devshop/devshop_bitbucket/devshop_bitbucket.drush.inc @@ -0,0 +1,122 @@ +task_type == 'clone') { +// $platform = node_load($task->task_args['target_platform']); +// $environment = $platform->environment; +// $project = $platform->project; +// } +// // Subsequent tasks are "deploy" and "verify" +// elseif (($task->task_type == 'verify' && $task->ref->type == 'site') || $task->task_type == 'devshop-deploy' || $task->task_type == 'test') { +// $environment = $task->ref->environment; +// $project = $task->ref->project; +// } +// else { +// return; +// } +// +// // If a pull request object is available... +// if (isset($environment->bitbucket_pull_request->pull_request_object->deployment)) { +// +// // Create a deployment status +// $owner = $project->bitbucket_owner; +// $repo = $project->bitbucket_repo; +// $deployment_id = $environment->bitbucket_pull_request->pull_request_object->deployment->id; +// +// try { +// $token = variable_get('devshop_bitbucket_token', ''); +// $client = new \bitbucket\Client(); +// $client->authenticate($token, bitbucket\Client::AUTH_HTTP_TOKEN); +// +// $params = new stdClass(); +// if ($status == HOSTING_TASK_SUCCESS || $status == HOSTING_TASK_WARNING) { +// $params->state = $state = 'success'; +// } +// else { +// $params->state = $state = 'failure'; +// } +// +// // If task is a test run, only submit a commit status for devshop/tests context. +// if ($task->task_type == 'test') { +// $sha = $environment->bitbucket_pull_request->pull_request_object->head->sha; +// +// $params = new stdClass(); +// $params->state = $state; +// $params->target_url = url("node/{$task->nid}/revisions/{$task->vid}/view",array('absolute' => TRUE)); +// +// if ($status == HOSTING_TASK_WARNING) { +// $params->description = t('DevShop: Tests passed with warnings'); +// } +// else { +// $params->description = t('DevShop: Tests !status!', array('!status' => $state)); +// } +// $params->context = 'devshop/tests'; +// +// $status = $client->getHttpClient()->post("/repos/$owner/$repo/statuses/$sha", json_encode($params)); +// drush_log('Commit status created for devshop/tests!', 'success'); +// } +// // Otherwise we create a deployment and a commit status. +// else { +// +// $params->target_url = $environment->url; +// $params->description = t('Visit !url', array('!url' => $task->ref->environment->url)); +// $post_url = "/repos/$owner/$repo/deployments/{$deployment_id}/statuses"; +// +// drush_log('Attempting to create bitbucket deployment status: ' . $post_url, 'success'); +// +// $deployment_status = $client->getHttpClient()->post($post_url, json_encode($params)); +// drush_log('Deployment status created!', 'success'); +// +// +// // Update Status API +// +// // Create a status +// $sha = $environment->bitbucket_pull_request->pull_request_object->head->sha; +// +// $params = new stdClass(); +// $params->state = $state; +// $params->target_url = url("node/{$task->nid}", array('absolute' => TRUE));; +// +// if ($status == HOSTING_TASK_WARNING) { +// $params->description = t('DevShop: Deploy success with warnings. [!url]', array( +// '!url' => $environment->url, +// )); +// } +// else { +// $params->description = t('DevShop: Deploy !status [!url]', array( +// '!status' => $state, +// '!url' => $environment->url, +// )); +// } +// $params->context = 'devshop/deploy'; +// +// $deployment_status = $client->getHttpClient()->post("/repos/$owner/$repo/statuses/$sha", json_encode($params)); +// drush_log('Commit status created!', 'success'); +// +// // If deploy task fails, tests won't run. +// if ($environment->settings->deploy['test'] && $status == HOSTING_TASK_ERROR) { +// +// $params = new stdClass(); +// $params->state = $state; +// $params->description = t('DevShop: Tests not run due to Deploy Fail'); +// $params->context = 'devshop/tests'; +// +// $deployment_status = $client->getHttpClient()->post("/repos/$owner/$repo/statuses/$sha", json_encode($params)); +// drush_log('Commit status created for devshop/tests', 'success'); +// } +// } +// } catch (bitbucket\Exception\RuntimeException $e) { +// drush_log('bitbucket API Error: ' . $e->getMessage(), 'error'); +// } +// } +//} diff --git a/modules/devshop/devshop_bitbucket/devshop_bitbucket.info b/modules/devshop/devshop_bitbucket/devshop_bitbucket.info new file mode 100644 index 000000000..648e8f885 --- /dev/null +++ b/modules/devshop/devshop_bitbucket/devshop_bitbucket.info @@ -0,0 +1,7 @@ +name = DevShop BitBucket +description = Support for BitBucket Webhooks +core = 7.x +package = DevShop +files[] = includes/add-key.inc +files[] = includes/admin.inc +dependencies[] = devshop_projects diff --git a/modules/devshop/devshop_bitbucket/devshop_bitbucket.install b/modules/devshop/devshop_bitbucket/devshop_bitbucket.install new file mode 100644 index 000000000..cee08e732 --- /dev/null +++ b/modules/devshop/devshop_bitbucket/devshop_bitbucket.install @@ -0,0 +1,95 @@ + array( + 'id' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'description' => 'Pull Request ID', + ), + 'number' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'description' => 'Pull Request Number', + ), + 'project_nid' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'description' => "The project's Node ID.", + ), + 'environment_name' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => 64, + 'default' => '', + 'description' => 'Environment name for this pull request environment.', + ), + 'pull_request_object' => array( + 'type' => 'text', + 'not null' => FALSE, + 'size' => 'big', + 'description' => 'A serialized array of settings for this environment.', + ), + ), + 'primary key' => array('id'), + ); + return $schema; +} + +/** + * Implements hook_install(). + */ +function devshop_bitbucket_install() { + + // Push devshop_bitbucket's system weight to 1. + db_update('system') + ->fields(array( + 'weight' => 2 + )) + ->condition('name', 'devshop_bitbucket') + ->execute(); + + // Display a message about setting a bitbucket personal token. + drupal_set_message(t('DevShop bitbucket module has been enabled. You must add an access token to enable full functionality at !link.', array( + '!link' => l(t('the settings page'), 'admin/devshop/bitbucket'), + ))); +} + +/** + * Set a weight higher than devshop_project so our form doesn't get obliterated by + * devshop_projects_form_project_node_form_alter() + */ +function devshop_bitbucket_update_7000() { + db_update('system') + ->fields(array( + 'weight' => 1 + )) + ->condition('name', 'devshop_bitbucket') + ->execute(); +} + +/** + * Set a weight higher than devshop_project so our form doesn't get obliterated by + * devshop_projects_form_project_node_form_alter() + */ +function devshop_bitbucket_update_7001() { + db_update('system') + ->fields(array( + 'weight' => 2 + )) + ->condition('name', 'devshop_bitbucket') + ->execute(); +} diff --git a/modules/devshop/devshop_bitbucket/devshop_bitbucket.module b/modules/devshop/devshop_bitbucket/devshop_bitbucket.module new file mode 100644 index 000000000..d7284e800 --- /dev/null +++ b/modules/devshop/devshop_bitbucket/devshop_bitbucket.module @@ -0,0 +1,744 @@ + 'BitBucket', +// 'description' => 'DevShop BitBucket Integration Settings', +// 'page callback' => 'drupal_get_form', +// 'page arguments' => array('devshop_bitbucket_settings_form'), +// 'access arguments' => array('administer projects'), +// 'file' => 'admin.inc', +// 'file path' => drupal_get_path('module', 'devshop_bitbucket') . '/includes', +// 'type' => MENU_LOCAL_TASK, +// ); +// $items['admin/devshop/bitbucket/add-key'] = array( +// 'title' => 'Add public key to bitbucket Account', +// 'page callback' => 'drupal_get_form', +// 'page arguments' => array('devshop_bitbucket_add_key_to_account'), +// 'access arguments' => array('administer projects'), +// 'file' => 'add-key.inc', +// 'file path' => drupal_get_path('module', 'devshop_bitbucket') . '/includes', +// 'type' => MENU_CALLBACK, +// ); + return $items; +} + +/** + * Implements hook_form_FORM_ID_alter() for project_node_form(). + */ +function devshop_bitbucket_form_project_node_form_alter(&$form, &$form_state, $form_id) { + $node = $form['#node']; + + if ($node->project->git_provider != 'bitbucket') { + return; + } + + //All settings git pull in project page + $form['project']['settings']['bitbucket'] = array( + '#type' => 'fieldset', + '#group' => 'project_settings', + '#collapsible' => TRUE, + '#collapsed' => arg(1) != $node->nid, + '#title' => t('BitBucket Integration'), + ); + + // Pull Requests create environments? + // $form['bitbucket']['pull_request_environments'] = array( + $form['project']['settings']['bitbucket']['pull_request_environments'] = array( + '#type' => 'checkbox', + '#title' => t('Create Environments for Pull Requests'), + '#default_value' => isset($node->project->settings->bitbucket) ? $node->project->settings->bitbucket['pull_request_environments'] : FALSE, + '#description' => t('If using bitbucket, create a new environment when a new Pull Request is created.'), + ); + + // Delete Pull Request environments? + // $form['bitbucket']['pull_request_environments_delete'] = array( + $form['project']['settings']['bitbucket']['pull_request_environments_delete'] = array( + '#type' => 'checkbox', + '#title' => t('Delete Pull Request Environments'), + '#default_value' => isset($node->project->settings->bitbucket) ? $node->project->settings->bitbucket['pull_request_environments_delete'] : FALSE, + '#description' => t('When Pull Requests are closed, delete the environment.'), + ); + + // Pull Request Environment method. + // $form['bitbucket']['pull_request_environments_method'] = array( + + $environments = array_keys($node->project->environments); + $options = array( + t('Install Drupal') => array( + 'devshop__bitbucket__install' => empty($node->project->install_profile)? t('Default install profile.'): $node->project->install_profile, + ), + t('Clone another environment') => array(), + ); + + if (empty($environments)) { + $options[t('Clone another environment')][] = t('No environments available.'); + } + else { + $options[t('Clone another environment')] = array_combine($environments, $environments); + } + $form['project']['settings']['bitbucket']['pull_request_environments_method'] = array( + '#type' => 'select', + '#title' => t('Pull Request Environment Creation Method'), + '#default_value' => isset($node->project->settings->bitbucket) ? + $node->project->settings->bitbucket['pull_request_environments_method'] : 'devshop__bitbucket__install', + '#description' => t('Select the method for creating the pull request environments.'), + '#options' => $options, + ); +} + +/** + * Implements hook_form_alter(). + */ +function devshop_bitbucket_form_devshop_project_create_step_git_alter(&$form, &$form_state, $form_id) { + + // Look for Token +// $token = variable_get('devshop_bitbucket_token', ''); +// +// if (empty($token)) { +// $form['connect']['#description'] = '
' . t('bitbucket API Token was not found.') . ' ' . l(t('Configure DevShop bitbucket Settings'), 'admin/devshop/bitbucket') . '
'; +// return; +// } +// +// if (devshop_bitbucket_check_key()) { +// $form['connect']['#description'] = '
' . t('The DevShop Public SSH Key has been found in your bitbucket account.') . '
'; +// } +// else { +// $form['connect']['#description'] = t('The DevShop Public SSH Key was not found in your account.') . ' '; +// +// $form['connect']['#description'] .= l(t('Add devshop public key to your bitbucket account'), 'admin/devshop/bitbucket/add-key', array( +// 'query' => array( +// 'destination' => $_GET['q'], +// ), +// 'attributes' => array( +// 'class' => 'btn btn-success' +// ), +// )) . ''; +// $form['connect']['#prefix'] = '
'; +// $form['connect']['#suffix'] = '
'; +// } +} + + +/** + * Implements hook_form_FORM_ID_alter() for project_node_form(). + */ +function devshop_bitbucket_form_devshop_project_create_step_sites_alter(&$form, &$form_state) { +// +// // Return if project isn't ready. +// if (empty($form['install_profile'])) { +// return; +// } +// +// // Return if there is no bitbucket token +// $bitbucket_token = variable_get('devshop_bitbucket_token', ''); +// if (empty($bitbucket_token)) { +// return; +// } +// +// // Load project and bitbucket +// $project_node = node_load($form['nid']['#value']); +// $project = $project_node->project; +// +// // Return if provider is not bitbucket. +// if ($project->git_provider != 'bitbucket') { +// return; +// } +// +// // Return if deploy method is not webhook. +// if ($project->settings->deploy['method'] != 'webhook') { +// return; +// } +// +// $repo = $project->bitbucket_owner . '/' . $project->bitbucket_repo; +// $repo_url = "http://bitbucket.com/" . $repo; +// +// $form['bitbucket_webhook'] = array( +// '#title' => t('Setup bitbucket Webhook'), +// '#description' => t('Leave this box checked to automatically add a webhook to your bitbucket Repository !link.', array( +// '!link' => l($repo, $repo_url, array( +// 'attributes' => array( +// 'target' => '_blank', +// ), +// )) +// )), +// '#type' => 'checkbox', +// '#default_value' => 1, +// ); +// +// $form['#validate'][] = 'devshop_bitbucket_project_create_webhook'; +// $form['#submit'][] = 'devshop_bitbucket_project_create_webhook'; +} + +/** + * Extra submit hook for last step of project create form. + */ +function devshop_bitbucket_project_create_webhook($form, $form_state) { +// +// // Return if box is not checked. +// if (empty($form_state['values']['bitbucket_webhook'])) { +// return; +// } +// +// // Get Project +// $project_node = node_load($form['nid']['#value']); +// $project = $project_node->project; +// +// // Get bitbucket client +// $client = new bitbucket\Client(); +// $bitbucket_token = variable_get('devshop_bitbucket_token', ''); +// $client->authenticate($bitbucket_token, bitbucket\Client::AUTH_HTTP_TOKEN); +// +// // Create the webhook. +// try { +// $hook = $client->repo()->hooks()->create($project->bitbucket_owner, $project->bitbucket_repo, array( +// 'name' => 'web', +// 'active' => true, +// 'events' => array( +// 'push', +// 'pull_request', +// 'delete', +// 'release', +// ), +// 'config' => array( +// 'url' => $project->webhook_url, +// 'content_type' => 'json', +// 'insecure_ssl' => '1', +// ), +// )); +// } +// catch (bitbucket\Exception\ValidationFailedException $e) { +// // For some reason, bitbucket always returns an exception here when there is already webhooks on the project. +// if ($e->getMessage() == 'Validation Failed: Hook already exists on this repository') { +// drupal_set_message(t("bitbucket webhook added, but there is already an existing webhook. Please check your repository's !link.", array( +// '!link' => l(t('Webhook Settings'), $project->git_repo_url . '/settings/hooks'), +// )), 'warning'); +// } +// else { +// drupal_set_message(t('bitbucket Validation Exception: !exception', array('!exception' => $e->getMessage())), 'error'); +// } +// } +// catch (bitbucket\Exception\RuntimeException $e) { +// drupal_set_message(t('bitbucket Runtime Exception: !exception', array('!exception' => $e->getMessage())), 'error'); +// } +} + +/** + * Implements hook_node_load(). + */ +function devshop_bitbucket_node_load($nodes, $types) { + if (count(array_intersect(array('project'), $types))) { + foreach ($nodes as $nid => $node) { + + // Look for a pull request object. + $pull_requests = db_query('SELECT * FROM {hosting_devshop_bitbucket_pull_requests} WHERE project_nid = :project_nid', array(':project_nid' => $nid)); + foreach ($pull_requests as $pull_request) { + if (!empty($pull_request->pull_request_object)) { + $pull_request->pull_request_object = unserialize($pull_request->pull_request_object); + if (isset($node->project->environments[$pull_request->environment_name])) { + $node->project->environments[$pull_request->environment_name]->bitbucket_pull_request = $pull_request; + } + } + } + + // Parse bitbucket owner and repo. + if (isset($node->project) && $node->project->git_provider == 'bitbucket') { + $node->project->bitbucket_owner = + $parts = explode('/', parse_url($node->project->git_repo_url, PHP_URL_PATH)); + $node->project->bitbucket_owner = $parts[1]; + $node->project->bitbucket_repo = $parts[2]; + } + } + } + +} + +/** + * Implements hook_node_update() for task insert. + * + * If task is a test run, send a "pending" commit status. + */ +function devshop_bitbucket_node_update($node) { +// +// // Only act on test triggers. +// if ($node->type != 'task' || $node->type == 'task' && ($node->task_type != 'test' || $node->task_type != 'deploy')) { +// return; +// } +// +// // Load the site and check for environment. +// $site = node_load($node->rid); +// if (empty($site->environment) || empty($site->environment->bitbucket_pull_request) || empty($site->environment->settings->deploy['test'])) { +// return; +// } +// +// try { +// $token = variable_get('devshop_bitbucket_token', ''); +// $client = new \bitbucket\Client(); +// $client->authenticate($token, bitbucket\Client::AUTH_HTTP_TOKEN); +// +// // Create a deployment status +// $project = $site->project; +// $owner = $project->bitbucket_owner; +// $repo = $project->bitbucket_repo; +// $sha = $site->environment->bitbucket_pull_request->pull_request_object->head->sha; +// +// $params = new stdClass(); +// $params->state = 'pending'; +// +// if ($node->task_type != 'test') { +// $params->target_url = url( +// "node/{$node->nid}", +// array('absolute' => true) +// ); +// $params->description = t('DevShop: Run Tests'); +// $params->context = 'devshop/tests'; +// } +// elseif ($node->task_type != 'deploy') { +// $params->target_url = $site->environment->url; +// $params->description = t('DevShop: Deploy'); +// $params->context = 'devshop/deploy'; +// } +// +// // Post status to bitbucket +// $status = $client->getHttpClient()->post("/repos/$owner/$repo/statuses/$sha", json_encode($params)); +// +// watchdog('devshop_bitbucket','Test run initiated. bitbucket has been notified.'); +// } catch (bitbucket\Exception\RuntimeException $e) { +// watchdog('devshop_bitbucket', 'bitbucket Runtime Error in devshop_bitbucket_node_update(): ' . $e->getMessage()); +// } +} + + +/** + * + */ +function devshop_bitbucket_comment($task, $status) { + + $output = array(); + $output[] = '> **DEVSHOP**'; + $output[] = '> ' . ucfirst($task->task_type) . ": " . _hosting_parse_error_code($status); + $output[] = '> Site: ' . $task->ref->environment->url; + $output[] = '> Project: ' . url("node/{$task->ref->project->nid}", array('absolute' => TRUE)); + + if ($task->task_type == 'test') { + $output[] = 'Results: ' . url("node/{$task->nid}", array('absolute' => TRUE)); + } + + if ($task->task_type == 'import') { + $output[] = t('Your environment is now available.'); + } + + return implode("\n", $output); +} + + + +/** + * bitbucket action to take on webhook init + */ +function devshop_bitbucket_webhook($project_node) { + $headers = getallheaders(); + $project = $project_node->project; + + // Create a bitbucket deployment +// require_once 'vendor/autoload.php'; + + // @TODO: Handle form content from bitbucket as well. + if ($headers['Content-Type'] == 'application/json' || $headers['content-type'] == 'application/json') { + $data = json_decode(file_get_contents('php://input')); + +// watchdog('debug', $GLOBALS['HTTP_RAW_POST_DATA']); +// watchdog('debug', print_r($data, 1)); + + $args = array(); + $args['cache'] = 1; + + if (isset($headers['X-Event-Key'])) { + $bitbucket_event = $headers['X-Event-Key']; + } + else { + $bitbucket_event = ''; + } + + switch ($bitbucket_event) { + case 'ping': + $message = 'Pong!'; + break; + case 'repo:push': + + // If push is for a deleted branch, don't do anything. +// if ($data->deleted && $data->after == "0000000000000000000000000000000000000000") { +// $message = 'Deleted ref detected.'; +// break; +// } + + // Limit "Deploy" tasks to only run for the branches we have new code for.. + $git_ref = $data->push->changes[0]->new->name; + + // Check for environments set to pull + $environments_to_pull = array(); + foreach ($project->environments as $environment_name => $environment) { + + // Only pull if deploy is not disabled or if environment is tracking a tag. + if ($git_ref == $environment->git_ref && !$environment->settings->pull_disabled && !in_array($environment->git_ref, $project->settings->git['tags'])) { + $environments_to_pull[] = $environment->name; + + + // Default args to the environments deploy settings. + $args = $environment->settings->deploy; + $args['git_ref'] = $environment->git_ref; + + if (isset($environment->site) && $node = node_load($environment->site)) { + hosting_add_task($environment->site, 'devshop-deploy', $args); + } + } + } + + $message = "Push Received for git ref $git_ref. Deploying code to environments: " . implode(', ', $environments_to_pull); + break; + + case 'pullrequest:created': + case 'pullrequest:updated': + case 'pullrequest:declined': + case 'pullrequest:merged': + + // If pull request environments is enabled... + if ($project->settings->bitbucket['pull_request_environments']) { + $message = 'Pull Request Received.'; + + // @TODO: Handle forks? + $branch = $data->pullrequest->source->branch->name; + + // Determine environment branch. + // @TODO: Make Configurable, allow branch names to be env name + // $environment_name = "pr" . $data->pull_request->number; + $environment_name = 'branch_' . strtr($branch, array( + '-' => '_', + '/' => '_', + )); + $already_have_pr_info = FALSE; + + // When PR is opened... create new environment. +// if ($data->action == 'opened' || $data->action == 'reopened') { + if ($data->pullrequest->state == 'OPEN') { + $message = "Detected Pull Request creation for $branch \n"; + if (isset($project->environments[$environment_name])) { + $message = "Environment $environment_name already exists! Not creating one... \n"; + + // @TODO: Check for environments that are being deleted. + if (isset($project->environments[$environment_name]->bitbucket_pull_request)) { + $message .= "Already have a PR for $environment_name ... not inserting."; + $already_have_pr_info = TRUE; + } + } + else { + // If method is "install"... + if ($project->settings->bitbucket['pull_request_environments_method'] == 'devshop__bitbucket__install') { + hosting_create_environment($project, $environment_name, $branch); + $message .= "Environment $environment_name created for $project_node->title via installation profile.\n"; + } + // Otherwise, it is a clone from live. + elseif (isset($project->environments[$project->settings->bitbucket['pull_request_environments_method']] )) { + $source_env = $project->settings->bitbucket['pull_request_environments_method']; + hosting_create_environment($project, $environment_name, $branch, $source_env); + $message .= "Environment $environment_name created for $project_node->title via cloning $source_env \n"; + } + else { + $message .= 'Unable to determine what to do! Check "Pull Request Environment Method" setting.'; + } + } +// +// $owner = $project->bitbucket_owner; +// $repo = $project->bitbucket_repo; +// $message .= "About to try to create a deployment for $owner/$repo... \n"; +// +// // Send a "deployment" to bitbucket. +// try { +// $token = variable_get('devshop_bitbucket_token', ''); +// $client = new \bitbucket\Client(); +// $client->authenticate($token, bitbucket\Client::AUTH_HTTP_TOKEN); +// +// $sha = $data->pull_request->head->sha; +// $environment_name_url = str_replace('_', '-', $environment_name); +// $url = "http://{$environment_name_url}.{$project->base_url}"; +// +// $params = new stdClass(); +// $params->ref = $sha; +// $params->environment = $url; +// $params->required_contexts = array(); +// $post_url = "/repos/$owner/$repo/deployments"; +// $deployment = json_decode($client->getHttpClient()->post($post_url, json_encode($params))->getBody(TRUE)); +// +// // Save deployment to pull request data for later access. +// $data->pull_request->deployment = $deployment; +// +// $message .= " Deployment Created! \n"; +// +// // Create deployment status +// $params = new stdClass(); +// $params->state = 'pending'; +// $params->target_url = $url; +// $params->description = t('New environment is being created. Please stand by.'); +// $deployment_status = $client->getHttpClient()->post("/repos/$owner/$repo/deployments/{$deployment->id}/statuses", json_encode($params)); +// +// $message .= " Deployment Status Created! \n"; +// +// // Set a commit status for this REF for devshop/deploy context +// $sha = $data->pull_request->head->sha; +// +// $params = new stdClass(); +// $params->state = 'pending'; +// $params->target_url = url("node/$project->nid", array('absolute' => TRUE)); +// $params->description = t('DevShop: Deploy'); +// $params->context = 'devshop/deploy'; +// +// // Post status to bitbucket +// $status = $client->getHttpClient()->post("/repos/$owner/$repo/statuses/$sha", json_encode($params)); +// $message .= " Commit Status Created! \n"; +// +// // Determine if we are going to run tests. +// // For now it is using the "live environment" setting. +// // @TODO: Once we add "deploy hooks" to "Project Settings: Environment Defaults" we will have to change this. +// +// // If live environment is set to run tests on deploy... +// $live_environment = $project->settings->live['live_environment']; +// if ($project->environments[$live_environment]->settings->deploy['test']) { +// $params = new stdClass(); +// $params->state = 'pending'; +// $params->target_url = url( +// "node/$project->nid", +// array('absolute' => true) +// ); +// $params->description = t('DevShop: Run Tests'); +// $params->context = 'devshop/tests'; +// +// // Post status to bitbucket +// $status = $client->getHttpClient()->post( +// "/repos/$owner/$repo/statuses/$sha", +// json_encode($params) +// ); +// $message .= " Commit Status for pending test run Created! \n"; +// } +// +// } catch (bitbucket\Exception\RuntimeException $e) { +// watchdog('devshop_bitbucket', 'bitbucket Runtime Error: ' . $e->getMessage()); +// $message .= 'bitbucket RunTimeException during PR Create: ' . $e->getMessage() . $e->getCode(); +// if ($e->getCode() == '409') { +// $message .= "\n Branch is out of date! alert the developer!"; +// +// // Send a failed commit status to alert to developer +// $params = new stdClass(); +// $params->state = 'failure'; +// $params->target_url = $project->git_repo_url; +// $params->description = t('Branch is out of date! Merge from default branch.'); +// $params->context = 'devshop/merge'; +// +// // Post status to bitbucket +// $status = $client->getHttpClient()->post("/repos/$owner/$repo/statuses/$sha", json_encode($params)); +// } +// +// } catch (bitbucket\Exception\ValidationFailedException $e) { +// watchdog('devshop_bitbucket', 'bitbucket Validation Failed Error: ' . $e->getMessage()); +// $message .= 'bitbucket ValidationFailedException Error: ' . $e->getMessage(); +// } + + // Insert PR record + if (!$already_have_pr_info) { + $info = new stdClass(); + $info->id = $data->pullrequest->id; + $info->number = $data->pullrequest->id; + $info->project_nid = $project->nid; + $info->environment_name = $environment_name; + $info->pull_request_object = serialize($data->pullrequest); + + // Save environment record. + + // @TODO : We have to alter our schema. BitBucket doesn't offer unique PR Ids +// if (drupal_write_record('hosting_devshop_bitbucket_pull_requests', $info)) { +// $message .= ' ' . t('Pull Request info saved to DevShop.'); +// } + } + } + + // When PR is updated, send a new deployment status environment. + elseif ($data->action == 'synchronize') { +// +// // Create a new deployment +// $owner = $project->bitbucket_owner; +// $repo = $project->bitbucket_repo; +// $message .= "About to set deployment status for $owner/$repo... \n"; +// +// try { +// $token = variable_get('devshop_bitbucket_token', ''); +// $client = new \bitbucket\Client(); +// $client->authenticate($token, bitbucket\Client::AUTH_HTTP_TOKEN); +// +// $sha = $data->pull_request->head->sha; +// $environment = (object) $project->environments[$environment_name]; +// +// $params = new stdClass(); +// $params->ref = $sha; +// $params->environment = $environment->url; +// $params->required_contexts = array(); +// $post_url = "/repos/$owner/$repo/deployments"; +// $deployment = json_decode($client->getHttpClient()->post($post_url, json_encode($params))->getBody(TRUE)); +// +// // Save deployment to pull request data for later access. +// $data->pull_request->deployment = $deployment; +// +// $message .= " Deployment Created! \n"; +// +// // Create deployment status +// $deployment_id = $deployment->id; +// +// $params = new stdClass(); +// $params->state = 'pending'; +// $params->target_url = $environment->url; +// $params->description = t('Code is being deployed. Please stand by.'); +// +// $post_url = "/repos/$owner/$repo/deployments/{$deployment_id}/statuses"; +// $message .= "Attempting to create deployment status: $post_url \n"; +// +// $deployment_status = $client->getHttpClient()->post($post_url, json_encode($params)); +// +// $message .= " Deployment Status Created! \n"; +// +// // Set a commit status for this REF for devshop/deploy context +// $sha = $data->pull_request->head->sha; +// +// $params = new stdClass(); +// $params->state = 'pending'; +// $params->target_url = url("node/$project->nid", array('absolute' => TRUE)); +// $params->description = t('DevShop: Deploy'); +// $params->context = 'devshop/deploy'; +// +// // Post status to bitbucket +// $status = $client->getHttpClient()->post("/repos/$owner/$repo/statuses/$sha", json_encode($params)); +// +// $message .= " Commit Status Created! \n"; +// +// // If environment is configured to run tests, add another status. +// if (!empty($environment->settings->deploy['test'])) { +// $params = new stdClass(); +// $params->state = 'pending'; +// +// // @TODO: Add the link to the last "test" task here instead of the project. +// $params->target_url = url("node/$project->nid", array('absolute' => TRUE)); +// $params->description = t('DevShop: Run Tests'); +// $params->context = 'devshop/tests'; +// +// // Post status to bitbucket +// $status = $client->getHttpClient()->post("/repos/$owner/$repo/statuses/$sha", json_encode($params)); +// +// $message .= " Commit Status Created for test runs! \n"; +// } +// +// } catch (bitbucket\Exception\RuntimeException $e) { +// $log = 'bitbucket API Error during PR Syncronize: ' . $e->getMessage() . $e->getCode(); +// watchdog('devshop_bitbucket', $log); +// +// if ($e->getCode() == '409') { +// $log .= "\n Out of date! alert the developer!"; +// +// // Send a failed commit status to alert to developer +// $params = new stdClass(); +// $params->state = 'failure'; +// $params->target_url = $project->git_repo_url; +// $params->description = t('Branch is out of date! Merge from default branch.'); +// $params->context = 'devshop/merge'; +// +// // Post status to bitbucket +// $status = $client->getHttpClient()->post("/repos/$owner/$repo/statuses/$sha", json_encode($params)); +// } +// $message .= $log . "\n"; +// } + + + // Update the PR record + $info = new stdClass(); + $info->id = $data->pullrequest->id; + $info->number = $data->pullrequest->id; + $info->project_nid = $project->nid; + $info->environment_name = $environment_name; + $info->pull_request_object = serialize($data->pullrequest); + + // Save environment record. + if (isset($project->environments[$environment_name]->bitbucket_pull_request)) { + $update = array('id'); + } + else { + $update = array(); + } + + // @TODO: We have to alter our schema. +// if (drupal_write_record('hosting_devshop_bitbucket_pull_requests', $info, $update)) { +// $message .= ' ' . t('Pull Request info saved to DevShop.'); +// } + } + // When PR is closed, delete environment. + elseif ($data->pullrequest->state == 'rejected' || $data->pullrequest->state == 'fulfilled') { + + $message .= "Pull Request Closed \n"; + if ($project->settings->bitbucket['pull_request_environments_delete']) { + + // If environment has a site... trigger it's deletion. + // Platform deletion triggers after site deletion completes. + if ($project->environments[$environment_name]->site) { + hosting_add_task($project->environments[$environment_name]->site, 'delete'); + $message .= "Environment $environment_name (Site Node: {$project->environments[$environment_name]->site}) scheduled for deletion."; + } + // If environment has a platform... trigger it's deletion. + elseif ($project->environments[$environment_name]->platform) { + hosting_add_task($project->environments[$environment_name]->platform, 'delete'); + $message .= "Environment $environment_name (Platform Node: {$project->environments[$environment_name]->platform}) scheduled for deletion."; + } + } + } + } + break; + } + + } + else { + $message = 'bitbucket Request Received, but not in JSON. Please make sure to configure the webhook to use Payload version: application/vnd.bitbucket.v3+json'; + } + return $message; +} + +/** + * Check the bitbucket account for an SSH key. + * + * @return bool + */ +function devshop_bitbucket_check_key() { +// $devshop_key = variable_get('devshop_public_key', ''); +// $token = variable_get('devshop_bitbucket_token', ''); +// +// $client = new \bitbucket\Client(); +// $client->authenticate($token, bitbucket\Client::AUTH_HTTP_TOKEN); +// +// try { +// $keys = $client->currentUser()->keys()->all(); +// } +// // Happens when user has no public keys. +// catch (\bitbucket\Exception\RuntimeException $e) { +// return FALSE; +// } +// +// foreach ($keys as $key) { +// if (strpos($devshop_key, $key['key']) === 0) { +// return TRUE; +// } +// } +} diff --git a/modules/devshop/devshop_bitbucket/includes/add-key.inc b/modules/devshop/devshop_bitbucket/includes/add-key.inc new file mode 100644 index 000000000..79c696fa7 --- /dev/null +++ b/modules/devshop/devshop_bitbucket/includes/add-key.inc @@ -0,0 +1,52 @@ + $devshop_key, + 'title' => t('Added by DevShop from !url', array( + '!url' => $_SERVER['HTTP_HOST'], + )), + ); + + // Create public key + $client = new \bitbucket\Client(); + $client->authenticate($token, bitbucket\Client::AUTH_HTTP_TOKEN); + try { + $return = $client->currentUser()->keys()->create($params); + } + catch (\bitbucket\Exception\ValidationFailedException $e) { + drupal_set_message($e->getMessage(), 'error'); + drupal_set_message('Code ' . $e->getCode(), 'error'); + } + + if ($return['verified']) { + drupal_set_message('SSH Key added to your account.'); + drupal_goto('admin/devshop/bitbucket'); + } else { + drupal_set_message('Something went wrong. SSH Key was not added to your account.', 'error'); + drupal_goto('admin/devshop/bitbucket'); + } +} diff --git a/modules/devshop/devshop_bitbucket/includes/admin.inc b/modules/devshop/devshop_bitbucket/includes/admin.inc new file mode 100644 index 000000000..bf5d52a23 --- /dev/null +++ b/modules/devshop/devshop_bitbucket/includes/admin.inc @@ -0,0 +1,59 @@ + t('bitbucket API Token'), + '#type' => 'password', + '#description' => t('A bitbucket API Token or Personal Access Token'), + '#default_value' => variable_get('devshop_bitbucket_token', ''), + ); + $bitbucket_url = "https://bitbucket.com/settings/tokens"; + $form['help'] = array( + '#markup' => t('Manage your tokens under account settings at !link.', array( + '!link' => l($bitbucket_url, $bitbucket_url, array('attributes' => array('target' => '_blank'))), + )) . '

' . t('Be sure to select the following Oauth Scopes: repo, repo:status, repo:deployment, public_repo, write:public_key, write_repo_hook'), + '#prefix' => '

', + '#suffix' => '

', + ); + + + // Warn the user if this is empty. + $token = variable_get('devshop_bitbucket_token', ''); + if (empty($token)) { + $form['devshop_bitbucket_token']['#description'] .= ' ' . t('No Token was found.') . ''; + $form['devshop_bitbucket_token']['#description'] .= ' ' . l(t('Create a new token'), 'https://bitbucket.com/settings/tokens/new', array('attributes' => array('target' => '_blank'))); + } + else { + $form['devshop_bitbucket_token']['#suffix'] .= ' ' . t('Your token is saved.') . ''; + } + + // Test for public key access. + if (!empty($token)) { + + $form['bitbucket_public_keys'] = array( + '#markup' => '', + ); + + if (devshop_bitbucket_check_key()) { + $form['bitbucket_public_keys']['#markup'] = '
' . t('The DevShop Public SSH Key has been found in your bitbucket account.') . '
'; + } + else { + $form['bitbucket_public_keys']['#markup'] = t('The DevShop Public SSH Key was not found in your account.') . ' '; + + $form['bitbucket_public_keys']['#markup'] .= ''; + $form['bitbucket_public_keys']['#prefix'] = '
'; + $form['bitbucket_public_keys']['#suffix'] = l(' ' . t('Add devshop public key to your bitbucket account'), 'admin/devshop/bitbucket/add-key', array( + 'attributes' => array( + 'class' => 'btn btn-primary btn-large' + ), + 'html' => TRUE, + )) . '
'; + } + } + + return system_settings_form($form); +} diff --git a/modules/devshop/devshop_cloud/drush/LICENSE b/modules/devshop/devshop_cloud/drush/LICENSE deleted file mode 100644 index d6a93266f..000000000 --- a/modules/devshop/devshop_cloud/drush/LICENSE +++ /dev/null @@ -1,340 +0,0 @@ -GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - {description} - Copyright (C) {year} {fullname} - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - {signature of Ty Coon}, 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. - diff --git a/modules/devshop/devshop_cloud/drush/Provision/Service/provider.php b/modules/devshop/devshop_cloud/drush/Provision/Service/provider.php deleted file mode 100644 index d22ffecb4..000000000 --- a/modules/devshop/devshop_cloud/drush/Provision/Service/provider.php +++ /dev/null @@ -1,50 +0,0 @@ -setProperty('provider'); - $context->setProperty('provider_options'); - $context->setProperty('provider_data'); - $context->setProperty('provider_server_identifier'); - } - - /** - * Stub for init_server(); - * - * Call from child classes: - * parent::init_server(); - * - * This function is called many times during a server verify. - * Use sparingly. - */ - function init_server() { - } - - /** - * Saves server options to drush options so they will be picked up by - * devshop_cloud_post_hosting_verify_task() - */ - function verify_server_cmd() { - drush_set_option('provider_data', $this->server->provider_data); - drush_set_option('provider_server_identifier', $this->server->provider_server_identifier); - - if (!empty($this->server->ip_addresses)) { - drush_set_option('ip_addresses', $this->server->ip_addresses); - } - } - - static function option_documentation() { - return array( - '--provider' => 'The provider of this server. Must match an available Provision_Service_provider', - '--provider_options' => 'An array of options to send to the provider.', - ); - } -} diff --git a/modules/devshop/devshop_cloud/drush/Provision/Service/provider/SoftLayer/softlayer-api-php-client/.gitignore b/modules/devshop/devshop_cloud/drush/Provision/Service/provider/SoftLayer/softlayer-api-php-client/.gitignore deleted file mode 100644 index be7384552..000000000 --- a/modules/devshop/devshop_cloud/drush/Provision/Service/provider/SoftLayer/softlayer-api-php-client/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -.DS_Store -*.swp -Thumbs.db -.svn -._* diff --git a/modules/devshop/devshop_cloud/drush/Provision/Service/provider/SoftLayer/softlayer-api-php-client/LICENSE.textile b/modules/devshop/devshop_cloud/drush/Provision/Service/provider/SoftLayer/softlayer-api-php-client/LICENSE.textile deleted file mode 100644 index cd0f96d85..000000000 --- a/modules/devshop/devshop_cloud/drush/Provision/Service/provider/SoftLayer/softlayer-api-php-client/LICENSE.textile +++ /dev/null @@ -1,9 +0,0 @@ -Copyright (c) 2009 - 2010, "SoftLayer Technologies, Inc.":http://www.softlayer.com/ All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - * Neither SoftLayer Technologies, Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/modules/devshop/devshop_cloud/drush/Provision/Service/provider/SoftLayer/softlayer-api-php-client/README.textile b/modules/devshop/devshop_cloud/drush/Provision/Service/provider/SoftLayer/softlayer-api-php-client/README.textile deleted file mode 100644 index b3f1851af..000000000 --- a/modules/devshop/devshop_cloud/drush/Provision/Service/provider/SoftLayer/softlayer-api-php-client/README.textile +++ /dev/null @@ -1,98 +0,0 @@ -h1. A SoftLayer API PHP client. - -h2. Overview - -The SoftLayer API PHP client classes provide a simple method for connecting to and making calls from the SoftLayer API and provides support for many of the SoftLayer API's features. Method calls and client management are handled by the PHP SOAP and XML-RPC extensions. - -Making API calls using the SoftLayer_SoapClient or SoftLayer_XmlrpcClient classes is done in the following steps: - -# Instantiate a new SoftLayer_SoapClient or SoftLayer_XmlrpcClient object using the SoftLayer_SoapClient::getClient() or SoftLayer_XmlrpcClient::getClient() methods. Provide the name of the service that you wish to query, an optional id number of the object that you wish to instantiate, your SoftLayer API username, your SoftLayer API key, and an optional API endpoint base URL. The client classes default to connect over the public Internet. Enter SoftLayer_SoapClient::API_PRIVATE_ENDPOINT or SoftLayer_XmlrpcClient::API_PRIVATE_ENDPOINT to connect to the API over SoftLayer's private network. The system making API calls must be connected to SoftLayer's private network (eg. purchased from SoftLayer or connected via VPN) in order to use the private network API endpoints. -# Define and add optional headers to the client, such as object masks and result limits. -# Call the API method you wish to call as if it were local to your client object. This class throws exceptions if it's unable to execute a query, so it's best to place API method calls in try / catch statements for proper error handling. - -Once your method is executed you may continue using the same client if you need to connect to the same service or define another client object if you wish to work with multiple services at once. - -The most up to date version of this library can be found on the SoftLayer github public repositories: "http://github.com/softlayer/":http://github.com/softlayer/ . Please post to the SoftLayer forums <"http://forums.softlayer.com/":http://forums.softlayer.com/> or open a support ticket in the SoftLayer customer portal if you have any questions regarding use of this library. - -h2. System Requirements - -The SoftLayer_SoapClient class requires at least PHP 5.2.3 and the PHP SOAP enxtension installed. The SoftLayer_Xmlrpc class requires PHP at least PHP 5 and the PHP XML-RPC extension installed. - -A valid API username and key are required to call the SoftLayer API. A connection to the SoftLayer private network is required to connect to SoftLayer's private network API endpopints. - -h2. Installation - -Download and copy the SoftLayer API client to a directory local to your PHP project or a path within your PHP installation's include_path. - -h2. Usage - -These examples use the SoftLayer_SoapClient class. If you wish to use the XML-RPC API then replace mentions of SoapClient.class.php with XmlrpcClient.class.php and SoftLayer_SoapClient with SoftLayer_XmlrpcClient. - -Here's a simple usage example that retrieves account information by calling the "getObject()":http://sldn.softlayer.com/reference/services/SoftLayer_Account/getObject method in the "SoftLayer_Account":http://sldn.softlayer.com/reference/services/SoftLayer_Account service: - -

-getObject();
-    print_r($account);
-} catch (Exception $e) {
-    die('Unable to retrieve account information: ' . $e->getMessage());
-}
-
- -For a more complex example we'll retrieve a support ticket with id 123456 along with the ticket's updates, the user it's assigned to, the servers attached to it, and the datacenter those servers are in. We'll retrieve our extra information using a nested object mask. After we have the ticket we'll update it with the text 'Hello!'. - -

-updates;
-$objectMask->assignedUser;
-$objectMask->attachedHardware->datacenter;
-$client->setObjectMask($objectMask);
-
-// Retrieve the ticket record
-try {
-    $ticket = $client->getObject();
-} catch (Exception $e) {
-    die('Unable to retrieve ticket record: ' . $e->getMessage());
-}
-
-// Update the ticket
-$update = new stdClass();
-$update->entry = 'Hello!';
-
-try {
-    $update = $client->addUpdate($update);
-    echo "Updated ticket 123456. The new update's id is " . $update[0]->id . '.');
-} catch (Exception $e) {
-    die('Unable to update ticket: ' . $e->getMessage());
-}
-
- -h2. Author - -This software is written by the SoftLayer Development Team <"sldn@softlayer.com":mailto:sldn@softlayer.com>. - -h2. Copyright - -This software is Copyright (c) 2009 - 2010 "SoftLayer Technologies, Inc":http://www.softlayer.com/. See the bundled LICENSE.textile file for more information. diff --git a/modules/devshop/devshop_cloud/drush/Provision/Service/provider/SoftLayer/softlayer-api-php-client/SoftLayer/Common/ObjectMask.class.php b/modules/devshop/devshop_cloud/drush/Provision/Service/provider/SoftLayer/softlayer-api-php-client/SoftLayer/Common/ObjectMask.class.php deleted file mode 100644 index b470c6df2..000000000 --- a/modules/devshop/devshop_cloud/drush/Provision/Service/provider/SoftLayer/softlayer-api-php-client/SoftLayer/Common/ObjectMask.class.php +++ /dev/null @@ -1,84 +0,0 @@ -datacenter = new StdClass(); - * $objectMask->serverRoom = new StdClass(); - * $objectMask->provisionDate = new StdClass(); - * $objectMask->softwareComponents = new StdClass(); - * $objectMask->softwareComponents->passwords = new StdClass(); - * - * Building an object mask using SoftLayer_ObjectMask is a bit easier to - * type: - * - * $objectMask = new SoftLayer_ObjectMask(); - * $objectMask->datacenter; - * $objectMask->serverRoom; - * $objectMask->provisionDate; - * $objectMask->sofwareComponents->passwords; - * - * Use SoftLayer_SoapClient::setObjectMask() to set these object masks before - * making your SoftLayer API calls. - * - * For more on object mask usage in the SoftLayer API please see - * http://sldn.softlayer.com/article/Using_Object_Masks_in_the_SoftLayer_API . - * - * The most up to date version of this library can be found on the SoftLayer - * github public repositories: http://github.com/softlayer/ . Please post to - * the SoftLayer forums or open a support ticket - * in the SoftLayer customer portal if you have any questions regarding use of - * this library. - * - * @author SoftLayer Technologies, Inc. - * @copyright Copyright (c) 2009 - 2010, Softlayer Technologies, Inc - * @license http://sldn.softlayer.com/article/License - * @see SoftLayer_SoapClient::setObjectMask() - * @see SoftLayer_XmlrpcClient::setObjectMask() - */ -class SoftLayer_ObjectMask -{ - /** - * Define an object mask value - * - * @param string $var - */ - public function __get($var) - { - $this->{$var} = new SoftLayer_ObjectMask(); - - return $this->{$var}; - } -} diff --git a/modules/devshop/devshop_cloud/drush/Provision/Service/provider/SoftLayer/softlayer-api-php-client/SoftLayer/SoapClient.class.php b/modules/devshop/devshop_cloud/drush/Provision/Service/provider/SoftLayer/softlayer-api-php-client/SoftLayer/SoapClient.class.php deleted file mode 100644 index 68b3c9700..000000000 --- a/modules/devshop/devshop_cloud/drush/Provision/Service/provider/SoftLayer/softlayer-api-php-client/SoftLayer/SoapClient.class.php +++ /dev/null @@ -1,503 +0,0 @@ - or open a support ticket - * in the SoftLayer customer portal if you have any questions regarding use of - * this library. - * - * @author SoftLayer Technologies, Inc. - * @copyright Copyright (c) 2009 - 2010, Softlayer Technologies, Inc - * @license http://sldn.softlayer.com/article/License - * @link http://sldn.softlayer.com/article/The_SoftLayer_API The SoftLayer API - * @see SoftLayer_SoapClient_AsynchronousAction - */ -class Softlayer_SoapClient extends SoapClient -{ - /** - * Your SoftLayer API username. You may overide this value when calling - * getClient(). - * - * @var string - */ - const API_USER = 'set me'; - - /** - * Your SoftLayer API user's authentication key. You may overide this value - * when calling getClient(). - * - * @link https://manage.softlayer.com/Administrative/apiKeychain API key management in the SoftLayer customer portal - * @var string - */ - const API_KEY = 'set me'; - - /** - * The base URL of the SoftLayer SOAP API's WSDL files over the public - * Internet. - * - * @var string - */ - const API_PUBLIC_ENDPOINT = 'https://api.softlayer.com/soap/v3/'; - - /** - * The base URL of the SoftLayer SOAP API's WSDL files over SoftLayer's - * private network. - * - * @var string - */ - const API_PRIVATE_ENDPOINT = 'http://api.service.softlayer.com/soap/v3/'; - - /** - * The namespace to use for calls to the API - * - * $var string - */ - const DEFAULT_NAMESPACE = 'http://api.service.softlayer.com/soap/v3/'; - - - /** - * The API endpoint base URL used by the client. - * - * @var string - */ - const API_BASE_URL = SoftLayer_SoapClient::API_PUBLIC_ENDPOINT; - - /** - * An optional SOAP timeout if you want to set a timeout independent of - * PHP's socket timeout. - * - * @var int - */ - const SOAP_TIMEOUT = null; - - /** - * The SOAP headers to send along with a SoftLayer API call - * - * @var array - */ - protected $_headers = array(); - - /** - * The name of the SoftLayer API service you wish to query. - * - * @link http://sldn.softlayer.com/reference/services A list of SoftLayer API services - * @var string - */ - protected $_serviceName; - - /** - * The base URL to the SoftLayer API's WSDL files being used by this - * client. - * - * @var string - */ - protected $_endpointUrl; - - /** - * Whether or not the current call is an asynchronous call. - * - * @var bool - */ - protected $_asynchronous = false; - - /** - * The object that handles asynchronous calls if the current call is an - * asynchronous call. - * - * @var SoftLayer_SoapClient_AsynchronousAction - */ - private $_asyncAction = null; - - /** - * If making an asynchronous call, then this is the name of the function - * we're calling. - * - * @var string - */ - public $asyncFunctionName = null; - - /** - * If making an asynchronous call, then this is the result of an - * asynchronous call as retuned from the - * SoftLayer_SoapClient_AsynchronousAction class. - * - * @var object - */ - private $_asyncResult = null; - - /** - * Used when making asynchronous calls. - * - * @var bool - */ - public $oneWay; - - /** - * Execute a SoftLayer API method - * - * @return object - */ - public function __call($functionName, $arguments = null) - { - // Determine if we shoud be making an asynchronous call. If so strip - // "Async" from the end of the method name. - if ($this->_asyncResult == null) { - $this->_asynchronous = false; - $this->_asyncAction = null; - - if (preg_match('/Async$/', $functionName) == 1) { - $this->_asynchronous = true; - $functionName = str_replace('Async', '', $functionName); - - $this->asyncFunctionName = $functionName; - } - } - - try { - $result = parent::__call($functionName, $arguments, null, $this->_headers, null); - } catch (SoapFault $e) { - throw new Exception('There was an error querying the SoftLayer API: ' . $e->getMessage()); - } - - if ($this->_asynchronous == true) { - return $this->_asyncAction; - } - - // remove the resultLimit header if they set it - $this->removeHeader('resultLimit'); - - return $result; - } - - /** - * Create a SoftLayer API SOAP Client - * - * Retrieve a new SoftLayer_SoapClient object for a specific SoftLayer API - * service using either the class' constants API_USER and API_KEY or a - * custom username and API key for authentication. Provide an optional id - * value if you wish to instantiate a particular SoftLayer API object. - * - * @param string $serviceName The name of the SoftLayer API service you wish to query - * @param int $id An optional object id if you're instantiating a particular SoftLayer API object. Setting an id defines this client's initialization parameter header. - * @param string $username An optional API username if you wish to bypass SoftLayer_SoapClient's built-in username. - * @param string $username An optional API key if you wish to bypass SoftLayer_SoapClient's built-in API key. - * @param string $endpointUrl The API endpoint base URL you wish to connect to. Set this to SoftLayer_SoapClient::API_PRIVATE_ENDPOINT to connect via SoftLayer's private network. - * @return SoftLayer_SoapClient - */ - public static function getClient($serviceName, $id = null, $username = null, $apiKey = null, $endpointUrl = null) - { - $serviceName = trim($serviceName); - - if ($serviceName == null) { - throw new Exception('Please provide a SoftLayer API service name.'); - } - - /* - * Default to use the public network API endpoint, otherwise use the - * endpoint defined in API_PUBLIC_ENDPOINT, otherwise use the one - * provided by the user. - */ - if (isset($endpointUrl)) { - $endpointUrl = trim($endpointUrl); - - if ($endpointUrl == null) { - throw new Exception('Please provide a valid API endpoint.'); - } - } elseif (self::API_BASE_URL != null) { - $endpointUrl = self::API_BASE_URL; - } else { - $endpointUrl = SoftLayer_SoapClient::API_PUBLIC_ENDPOINT; - } - - if (is_null(self::SOAP_TIMEOUT)) { - $soapClient = new SoftLayer_SoapClient($endpointUrl . $serviceName . '?wsdl'); - } else { - $soapClient = new SoftLayer_SoapClient($endpointUrl . $serviceName . '?wsdl', array('connection_timeout' => self::SOAP_TIMEOUT)); - } - - $soapClient->_serviceName = $serviceName; - $soapClient->_endpointUrl = $endpointUrl; - - if ($username != null && $apiKey != null) { - $soapClient->setAuthentication($username, $apiKey); - } else { - $soapClient->setAuthentication(self::API_USER, self::API_KEY); - } - - if ($id !== null) { - $soapClient->setInitParameter($id); - } - - return $soapClient; - } - - /** - * Set a SoftLayer API call header - * - * Every header defines a customization specific to an SoftLayer API call. - * Most API calls require authentication and initialization parameter - * headers, but can also include optional headers such as object masks and - * result limits if they're supported by the API method you're calling. - * - * @see removeHeader() - * @param string $name The name of the header you wish to set - * @param object $value The object you wish to set in this header - * @return SoftLayer_SoapClient - */ - public function addHeader($name, $value) - { - $this->_headers[$name] = new SoapHeader(self::DEFAULT_NAMESPACE, $name, $value); - return $this; - } - - /** - * Remove a SoftLayer API call header - * - * Removing headers may cause API queries to fail. - * - * @see addHeader() - * @param string $name The name of the header you wish to remove - * @return SoftLayer_SoapClient - */ - public function removeHeader($name) - { - unset($this->_headers[$name]); - return $this; - } - - /** - * Set a user and key to authenticate a SoftLayer API call - * - * Use this method if you wish to bypass the API_USER and API_KEY class - * constants and set custom authentication per API call. - * - * @link https://manage.softlayer.com/Administrative/apiKeychain API key management in the SoftLayer customer portal - * @param string $username - * @param string $apiKey - * @return SoftLayer_SoapClient - */ - public function setAuthentication($username, $apiKey) - { - $username = trim($username); - $apiKey = trim($apiKey); - - if ($username == null) { - throw new Exception('Please provide a SoftLayer API username.'); - } - - if ($apiKey == null) { - throw new Exception('Please provide a SoftLayer API key.'); - } - - $header = new stdClass(); - $header->username = $username; - $header->apiKey = $apiKey; - - $this->addHeader('authenticate', $header); - return $this; - } - - - /** - * Set an initialization parameter header on a SoftLayer API call - * - * Initialization parameters instantiate a SoftLayer API service object to - * act upon during your API method call. For instance, if your account has a - * server with id number 1234, then setting an initialization parameter of - * 1234 in the SoftLayer_Hardware_Server Service instructs the API to act on - * server record 1234 in your method calls. - * - * @link http://sldn.softlayer.com/article/Using_Initialization_Parameters_in_the_SoftLayer_API Using Initialization Parameters in the SoftLayer API - * @param int $id The ID number of the SoftLayer API object you wish to instantiate. - * @return SoftLayer_SoapClient - */ - public function setInitParameter($id) - { - $id = trim($id); - - if (!is_null($id)) { - $initParameters = new stdClass(); - $initParameters->id = $id; - $this->addHeader($this->_serviceName . 'InitParameters', $initParameters); - } - - return $this; - } - - /** - * Set an object mask to a SoftLayer API call - * - * Use an object mask to retrieve data related your API call's result. - * Object masks are skeleton objects or strings that define nested relational - * properties to retrieve along with an object's local properties. - * - * @see SoftLayer_ObjectMask - * @link http://sldn.softlayer.com/article/Using-Object-Masks-SoftLayer-API Using object masks in the SoftLayer API - * @param object $mask The object mask you wish to define - * @return SoftLayer_SoapClient - */ - public function setObjectMask($mask) - { - if (!is_null($mask)) { - $header = 'SoftLayer_ObjectMask'; - - if ($mask instanceof SoftLayer_ObjectMask) { - $header = sprintf('%sObjectMask', $this->_serviceName); - } - - $objectMask = new stdClass(); - $objectMask->mask = $mask; - $this->addHeader($header, $objectMask); - } - - return $this; - } - - /** - * Set an object filter to a SoftLayer API call - * - * Use an object filter to limit what data you get back - * from the API. Very similar to objectMasks - * - * @see SoftLayer_ObjectMask - * @param object $filter The object filter you wish to define - * @return SoftLayer_SoapClient - */ - public function setObjectFilter($objectFilter) - { - if (!is_null($objectFilter)) { - $header = sprintf('%sObjectFilter', $this->_serviceName); - $this->addHeader($header, $objectFilter); - } - return $this; - } - /** - * Set a result limit on a SoftLayer API call - * - * Many SoftLayer API methods return a group of results. These methods - * support a way to limit the number of results retrieved from the SoftLayer - * API in a way akin to an SQL LIMIT statement. - * - * @link http://sldn.softlayer.com/article/Using_Result_Limits_in_the_SoftLayer_API Using Result Limits in the SoftLayer API - * @param int $limit The number of results to limit your SoftLayer API call to. - * @param int $offset An optional offset to begin your SoftLayer API call's returned result set at. - * @return SoftLayer_SoapClient - */ - public function setResultLimit($limit, $offset = 0) - { - $resultLimit = new stdClass(); - $resultLimit->limit = intval($limit); - $resultLimit->offset = intval($offset); - - $this->addHeader('resultLimit', $resultLimit); - return $this; - } - - /** - * Process a SOAP request - * - * We've overwritten the PHP SoapClient's __doRequest() to allow processing - * asynchronous SOAP calls. If an asynchronous call was deected in the - * __call() method then send processing to the - * SoftLayer_SoapClient_AsynchronousAction class. Otherwise use the - * SoapClient's built-in __doRequest() method. The results of this method - * are sent back to __call() for post-processing. Asynchronous calls use - * handleAsyncResult() to send he results of the call back to __call(). - * - * @return object - */ - public function __doRequest($request, $location, $action, $version, $one_way = false) - { - // Don't make a call if we already have an asynchronous result. - if ($this->_asyncResult != null) { - $result = $this->_asyncResult; - unset($this->_asyncResult); - - return $result; - } - - if ($this->oneWay == true) { - $one_way = true; - $this->oneWay = false; - } - - // Use either the SoapClient or SoftLayer_SoapClient_AsynchronousAction - // class to handle the call. - if ($this->_asynchronous == false) { - $result = parent::__doRequest($request, $location, $action, $version, $one_way); - - return $result; - } else { - $this->_asyncAction = new SoftLayer_SoapClient_AsynchronousAction($this, $this->asyncFunctionName, $request, $location, $action); - return ''; - } - } - - /** - * Process the results of an asynchronous call. - * - * The SoftLayer_SoapClient_AsynchronousAction class uses - * handleAsyncResult() to return it's call resuls back to this classes' - * __call() method for post-pocessing. - * - * @param string $functionName The name of the SOAP method called. - * @param string $result The raw SOAP XML output from a SOAP call - * @return object - */ - public function handleAsyncResult($functionName, $result) - { - $this->_asynchronous = false; - $this->_asyncResult = $result; - - return $this->__call($functionName, array()); - } -} diff --git a/modules/devshop/devshop_cloud/drush/Provision/Service/provider/SoftLayer/softlayer-api-php-client/SoftLayer/SoapClient/AsynchronousAction.class.php b/modules/devshop/devshop_cloud/drush/Provision/Service/provider/SoftLayer/softlayer-api-php-client/SoftLayer/SoapClient/AsynchronousAction.class.php deleted file mode 100644 index 416ad3a24..000000000 --- a/modules/devshop/devshop_cloud/drush/Provision/Service/provider/SoftLayer/softlayer-api-php-client/SoftLayer/SoapClient/AsynchronousAction.class.php +++ /dev/null @@ -1,224 +0,0 @@ - receive style of transmission. Response - * time for a call is dependent on the latency between the SOAP client and SOAP - * server and the time required by the server to process the SOAP call. Sending - * multiple SOAP calls in serial over the public Internet can be a time - * consuming process. - * - * Asynchronous calls allow you to send multiple SOAP calls in parallel to the - * SoftLayer API. Parallel calls reduce the latency involved handling multiple - * calls to the time it takes for the longest SOAP call to execute, dramatically - * reducing the time it takes to send multiple SOAP calls in most cases. - * - * Asynchronous calls are handled identically to standard API calls with two - * differences: - * - * 1) The SoftLayer_SoapClient class knows to make an asynchronous call when the - * method called ends with "Async". For example, to make a standard call to the - * method getObject() you would execute $client->geObject(). It's asynchronous - * counterpart is execued with the code $client->getObjectAsync(). Once the - * asynchronous call is made the results of your API command are sent to this - * classes' socket property. - * - * 2) The results of an asynchronous method call are stored in a - * SoftLayer_SoapClient_AsynchronousAction object. Use the wait() method to - * retrieve data off the internal socket and return the result back to the - * SoftLayer_SoapClient for processing. For example if you wish to retrieve the - * results of the method getObject() execute the following statements: - * - * $result = $client->getObjectAsync(); // Make the call and start geting data back. - * $result = $result->wait(); // Return the results of the API call - * - * To chain multiple asynchronous requests together call multiple Async requests - * in succession then call their associated wait() methods in succession. - * - * Here's a simple usage example that retrieves account information, a PDF of an - * account's next invoice and enables VLAN spanning on that same account by - * calling three methods in the SoftLayer_Account service in parallel: - * - * ---------- - * - * // Initialize an API client for the SoftLayer_Account service. - * $client = SoftLayer_SoapClient::getClient('SoftLayer_Account'); - * - * try { - * // Request our account information. - * $account = $client->getObjectAsync(); - * - * // Request a PDF of our next invoice. This can take much longer than - * // getting simple account information. - * $nextInvoicePdf = $client->getNextInvoicePdfAsync(); - * - * // While we're at it we'll enable VLAN spanning on our account. - * $vlanSpanResult = $client->setVlanSpanAsync(true); - * - * // The three requests are now processing in parallel. Use the wait() - * // method to retrieve the resuls of our requests. The wait time involved - * // is roughly the same time as the longest API call. - * $account = $account->wait(); - * $nextInvoicePdf = $nextInvoicePdf->wait(); - * $vlanSpanResult = $vlanSpanResult->wait(); - * - * // Finally, display our results. - * var_dump($account); - * var_dump($nextInvoicePdf); - * var_dump($vlanSpanResult); - * } catch (Exception $e) { - * die('Unable to retrieve account information: ' . $e->getMessage()); - * } - * - * ---------- - * - * The most up to date version of this library can be found on the SoftLayer - * github public repositories: http://github.com/softlayer/ . Please post to - * the SoftLayer forums or open a support ticket - * in the SoftLayer customer portal if you have any questions regarding use of - * this library. - * - * @author SoftLayer Technologies, Inc. - * @copyright Copyright (c) 2009 - 2010, Softlayer Technologies, Inc - * @license http://sldn.softlayer.com/article/License - * @see SoftLayer_SoapClient - */ -class SoftLayer_SoapClient_AsynchronousAction -{ - /** - * The SoftLayer SOAP client making an asynchronous call - * - * @var SoftLayer_SoapClient - */ - protected $_soapClient; - - /** - * The name of the function we're calling - * - * @var string - */ - protected $_functionName; - - /** - * A socket connection to the SoftLayer SOAP API - * - * @var resource - */ - protected $_socket; - - /** - * Perform an asynchgronous SoftLayer SOAP call - * - * Create a raw socket connection to the URL specified by the - * SoftLayer_SoapClient class and send SOAP HTTP headers and request XML to - * that socket. Throw exceptions if we're unable to make the socket - * connection or send data to that socket. - * - * @param SoftLayer_SoapClient $soapClient The SoftLayer SOAP client making the asynchronous call. - * @param string $functionName The name of the function we're calling. - * @param string $request The full XML SOAP request we wish to make. - * @param string $location The URL of the web service we wish to call. - * @param string $action The value of the HTTP SOAPAction header in our SOAP call. - */ - public function __construct($soapClient, $functionName, $request, $location, $action) - { - preg_match('%^(http(?:s)?)://(.*?)(/.*?)$%', $location, $matches); - - $this->_soapClient = $soapClient; - $this->_functionName = $functionName; - - $protocol = $matches[1]; - $host = $matches[2]; - $endpoint = $matches[3]; - - $headers = array( - 'POST ' . $endpoint . ' HTTP/1.1', - 'Host: ' . $host, - 'User-Agent: PHP-SOAP/' . phpversion(), - 'Content-Type: text/xml; charset=utf-8', - 'SOAPAction: "' . $action . '"', - 'Content-Length: ' . strlen($request), - 'Connection: close', - ); - - if ($protocol == 'https') { - $host = 'ssl://' . $host; - $port = 443; - } else { - $port = 80; - } - - $data = implode("\r\n", $headers) . "\r\n\r\n" . $request . "\r\n"; - $this->_socket = fsockopen($host, $port, $errorNumber, $errorMessage); - - if ($this->_socket === false) { - $this->_socket = null; - throw new Exception('Unable to make an asynchronous SoftLayer API call: ' . $errorNumber . ': ' . $errorMessage); - } - - if (fwrite($this->_socket, $data) === false) { - throw new Exception('Unable to write data to an asynchronous SoftLayer API call.'); - } - } - - /** - * Process and return the results of an asyncrhonous SoftLayer API call - * - * Read data from our socket and process the raw SOAP result from the - * SoftLayer_SoapClient instance that made the asynchronous call. wait() - * *must* be called in order to recieve the results from your API call. - * - * @return object - */ - public function wait() - { - $soapResult = ''; - - while (!feof($this->_socket)) { - $soapResult .= fread($this->_socket, 8192); - } - - // separate the SOAP result into headers and data. - list($headers, $data) = explode("\r\n\r\n", $soapResult); - - return $this->_soapClient->handleAsyncResult($this->_functionName, $data); - } - - /** - * Close the socket created when the SOAP request was created. - */ - public function __destruct() - { - if ($this->_socket != null) { - fclose($this->_socket); - } - } -} diff --git a/modules/devshop/devshop_cloud/drush/Provision/Service/provider/SoftLayer/softlayer-api-php-client/SoftLayer/XmlrpcClient.class.php b/modules/devshop/devshop_cloud/drush/Provision/Service/provider/SoftLayer/softlayer-api-php-client/SoftLayer/XmlrpcClient.class.php deleted file mode 100644 index 31db1313e..000000000 --- a/modules/devshop/devshop_cloud/drush/Provision/Service/provider/SoftLayer/softlayer-api-php-client/SoftLayer/XmlrpcClient.class.php +++ /dev/null @@ -1,437 +0,0 @@ - or open a support ticket - * in the SoftLayer customer portal if you have any questions regarding use of - * this library. - * - * @author SoftLayer Technologies, Inc. - * @copyright Copyright (c) 2009 - 2010, Softlayer Technologies, Inc - * @license http://sldn.softlayer.com/article/License - * @link http://sldn.softlayer.com/article/The_SoftLayer_API The SoftLayer API - */ -class Softlayer_XmlrpcClient -{ - /** - * Your SoftLayer API username. You may overide this value when calling - * getClient(). - * - * @var string - */ - const API_USER = 'set me'; - - /** - * Your SoftLayer API user's authentication key. You may overide this value - * when calling getClient(). - * - * @link https://manage.softlayer.com/Administrative/apiKeychain API key management in the SoftLayer customer portal - * @var string - */ - const API_KEY = 'set me'; - - /** - * The base URL of SoftLayer XML-RPC API's public network endpoints. - * - * @var string - */ - const API_PUBLIC_ENDPOINT = 'https://api.softlayer.com/xmlrpc/v3/'; - - /** - * The base URL of SoftLayer XML-RPC API's private network endpoints. - * - * @var string - */ - const API_PRIVATE_ENDPOINT = 'http://api.service.softlayer.com/xmlrpc/v3/'; - - /** - * The API endpoint base URL used by the client. - * - * @var string - */ - const API_BASE_URL = SoftLayer_XmlrpcClient::API_PUBLIC_ENDPOINT; - - /** - * The headers to send along with a SoftLayer API call - * - * @var array - */ - protected $_headers = array(); - - /** - * The name of the SoftLayer API service you wish to query. - * - * @link http://sldn.softlayer.com/reference/services A list of SoftLayer API services - * @var string - */ - protected $_serviceName; - - /** - * The base URL of SoftLayer XML-RPC API's endpoints used by this client. - * - * @var string - */ - protected $_endpointUrl; - - /** - * Execute a SoftLayer API method - * - * @return object - */ - public function __call($functionName, $arguments = null) - { - $request = array(); - $request[0] = array('headers' => $this->_headers); - $request = array_merge($request, $arguments); - - try { - $encodedRequest = xmlrpc_encode_request($functionName, $request); - - // Making the XML-RPC call and interpreting the response is adapted - // from the PHP manual: - // http://www.php.net/manual/en/function.xmlrpc-encode-request.php - $context = stream_context_create(array( - 'http' => array( - 'method' => 'POST', - 'header' => 'Content-Type: text/xml', - 'content' => $encodedRequest - ))); - - $file = file_get_contents($this->_endpointUrl . $this->_serviceName, false, $context); - - if ($file === false) { - throw new Exception('Unable to contact the SoftLayer API at ' . $this->_endpointUrl . $serviceName . '.'); - } - - $result = xmlrpc_decode($file); - } catch (Exception $e) { - throw new Exception('There was an error querying the SoftLayer API: ' . $e->getMessage()); - } - - if (is_array($result) && xmlrpc_is_fault($result)) { - throw new Exception('There was an error querying the SoftLayer API: ' . $result['faultString']); - } - - // remove the resultLimit header if they set it - $this->removeHeader('resultLimit'); - - return self::_convertToObject(self::_convertXmlrpcTypes($result)); - } - - /** - * Create a SoftLayer API XML-RPC Client - * - * Retrieve a new SoftLayer_XmlrpcClient object for a specific SoftLayer API - * service using either the class' constants API_USER and API_KEY or a - * custom username and API key for authentication. Provide an optional id - * value if you wish to instantiate a particular SoftLayer API object. - * - * @param string $serviceName The name of the SoftLayer API service you wish to query - * @param int $id An optional object id if you're instantiating a particular SoftLayer API object. Setting an id defines this client's initialization parameter header. - * @param string $username An optional API username if you wish to bypass SoftLayer_XmlrpcClient's built-in username. - * @param string $username An optional API key if you wish to bypass SoftLayer_XmlrpcClient's built-in API key. - * @param string $endpointUrl The API endpoint base URL you wish to connect to. Set this to SoftLayer_XmlrpcClient::API_PRIVATE_ENDPOINT to connect via SoftLayer's private network. - * @return SoftLayer_XmlrpcClient - */ - public static function getClient($serviceName, $id = null, $username = null, $apiKey = null, $endpointUrl = null) - { - $serviceName = trim($serviceName); - $id = trim($id); - $username = trim($username); - $apiKey = trim($apiKey); - - if ($serviceName == null) { - throw new Exception('Please provide a SoftLayer API service name.'); - } - - $client = new Softlayer_XmlrpcClient(); - - /* - * Default to use the public network API endpoint, otherwise use the - * endpoint defined in API_PUBLIC_ENDPOINT, otherwise use the one - * provided by the user. - */ - if (isset($endpointUrl)) { - $endpointUrl = trim($endpointUrl); - - if ($endpointUrl == null) { - throw new Exception('Please provide a valid API endpoint.'); - } - - $client->_endpointUrl = $endpointUrl; - } elseif (self::API_BASE_URL != null) { - $client->_endpointUrl = self::API_BASE_URL; - } else { - $client->_endpointUrl = SoftLayer_XmlrpcClient::API_PUBLIC_ENDPOINT; - } - - if ($username != null && $apiKey != null) { - $client->setAuthentication($username, $apiKey); - } else { - $client->setAuthentication(self::API_USER, self::API_KEY); - } - - $client->_serviceName = $serviceName; - - if ($id != null) { - $client->setInitParameter($id); - } - - return $client; - } - - /** - * Set a SoftLayer API call header - * - * Every header defines a customization specific to an SoftLayer API call. - * Most API calls require authentication and initialization parameter - * headers, but can also include optional headers such as object masks and - * result limits if they're supported by the API method you're calling. - * - * @see removeHeader() - * @param string $name The name of the header you wish to set - * @param object $value The object you wish to set in this header - * @return SoftLayer_XmlrpcClient - */ - public function addHeader($name, $value) - { - if (is_object($value)) { - $value = (array)$value; - } - - $this->_headers[$name] = $value; - return $this; - } - - /** - * Remove a SoftLayer API call header - * - * Removing headers may cause API queries to fail. - * - * @see addHeader() - * @param string $name The name of the header you wish to remove - * @return SoftLayer_XmlrpcClient - */ - public function removeHeader($name) - { - unset($this->_headers[$name]); - return $this; - } - - /** - * Set a user and key to authenticate a SoftLayer API call - * - * Use this method if you wish to bypass the API_USER and API_KEY class - * constants and set custom authentication per API call. - * - * @link https://manage.softlayer.com/Administrative/apiKeychain API key management in the SoftLayer customer portal - * @param string $username - * @param string $apiKey - * @return SoftLayer_XmlrpcClient - */ - public function setAuthentication($username, $apiKey) - { - $username = trim($username); - $apiKey = trim($apiKey); - - if ($username == null) { - throw new Exception('Please provide a SoftLayer API username.'); - } - - if ($apiKey == null) { - throw new Exception('Please provide a SoftLayer API key.'); - } - - $header = new stdClass(); - $header->username = $username; - $header->apiKey = $apiKey; - - $this->addHeader('authenticate', $header); - return $this; - } - - /** - * Set an initialization parameter header on a SoftLayer API call - * - * Initialization parameters instantiate a SoftLayer API service object to - * act upon during your API method call. For instance, if your account has a - * server with id number 1234, then setting an initialization parameter of - * 1234 in the SoftLayer_Hardware_Server Service instructs the API to act on - * server record 1234 in your method calls. - * - * @link http://sldn.softlayer.com/article/Using_Initialization_Parameters_in_the_SoftLayer_API Using Initialization Parameters in the SoftLayer API - * @param int $id The ID number of the SoftLayer API object you wish to instantiate. - * @return SoftLayer_XmlrpcClient - */ - public function setInitParameter($id) - { - $id = trim($id); - - if (!is_null($id)) { - $initParameters = new stdClass(); - $initParameters->id = $id; - $this->addHeader($this->_serviceName . 'InitParameters', $initParameters); - } - - return $this; - } - - /** - * Set an object mask to a SoftLayer API call - * - * Use an object mask to retrieve data related your API call's result. - * Object masks are skeleton objects or strings that define nested relational - * properties to retrieve along with an object's local properties. - * - * @see SoftLayer_ObjectMask - * @link http://sldn.softlayer.com/article/Using-Object-Masks-SoftLayer-API Using object masks in the SoftLayer API - * @param object $mask The object mask you wish to define - * @return SoftLayer_SoapClient - */ - public function setObjectMask($mask) - { - if (!is_null($mask)) { - $header = 'SoftLayer_ObjectMask'; - - if ($mask instanceof SoftLayer_ObjectMask) { - $header = sprintf('%sObjectMask', $this->_serviceName); - } - - $objectMask = new stdClass(); - $objectMask->mask = $mask; - $this->addHeader($header, $objectMask); - } - - return $this; - } - - /** - * Set a result limit on a SoftLayer API call - * - * Many SoftLayer API methods return a group of results. These methods - * support a way to limit the number of results retrieved from the SoftLayer - * API in a way akin to an SQL LIMIT statement. - * - * @link http://sldn.softlayer.com/article/Using_Result_Limits_in_the_SoftLayer_API Using Result Limits in the SoftLayer API - * @param int $limit The number of results to limit your SoftLayer API call to. - * @param int $offset An optional offset to begin your SoftLayer API call's returned result set at. - * @return SoftLayer_XmlrpcClient - */ - public function setResultLimit($limit, $offset = 0) - { - $resultLimit = new stdClass(); - $resultLimit->limit = intval($limit); - $resultLimit->offset = intval($offset); - - $this->addHeader('resultLimit', $resultLimit); - return $this; - } - - /** - * Remove PHP xmlrpc type definition structures from a decoded request array - * - * Certain xmlrpc types like base64 are decoded in PHP to a stdClass with a - * scalar property containing the decoded value of the xmlrpc member and an - * xmlrpc_type property describing which xmlrpc type is being described. This - * function removes xmlrpc_type data and moves the scalar value into the root of - * the xmlrpc value for known xmlrpc types. - * - * @param mixed $result The decoded xmlrpc request to process - * @return mixed - */ - private static function _convertXmlrpcTypes($result) { - if (is_array($result)) { - - // Return case 1: The result is an empty array. Return the empty - // array. - if (count($result) == 0) { - return $result; - } else { - - // Return case 2: The result is a non-empty array. Loop through - // array elements and recursively translate every element. - // Return the fully translated array. - foreach ($result as $key => $value) { - $result[$key] = self::_convertXmlrpcTypes($value); - } - - return $result; - } - - // Return case 3: The result is an xmlrpc scalar. Convert it to a normal - // variable and return it. - } elseif (is_object($result) && $result->scalar != null && $result->xmlrpc_type != null) { - - // Convert known xmlrpc types, otherwise unset the value. - switch ($result->xmlrpc_type) { - case 'base64': - return $result->scalar; - break; - default: - return null; - break; - } - - // Return case 4: Otherwise the result is a non-array and non xml-rpc - // scalar variable. Return it unmolested. - } else { - return $result; - } - } - - /** - * Recursively convert an array to an object - * - * Since xmlrpc_decode_result returns an array, but we want an object - * result, so cast all array parts in our result set as objects. - * - * @param mixed $result A result or portion of a result to convert - * @return mixed - */ - private static function _convertToObject($result) { - return is_array($result) ? (object) array_map('SoftLayer_XmlrpcClient::_convertToObject', $result) : $result; - } -} diff --git a/modules/devshop/devshop_cloud/drush/Provision/Service/provider/SoftLayer/softlayer-api-php-client/example.php b/modules/devshop/devshop_cloud/drush/Provision/Service/provider/SoftLayer/softlayer-api-php-client/example.php deleted file mode 100644 index 9c4665257..000000000 --- a/modules/devshop/devshop_cloud/drush/Provision/Service/provider/SoftLayer/softlayer-api-php-client/example.php +++ /dev/null @@ -1,113 +0,0 @@ -, [username], [API key]); - * - * API Service: The name of the API service you wish to connect to. - * id: An optional id to initialize your API service with, if you're - * interacting with a specific object. If you don't need to specify - * an id then pass null to the client. - * username: Your SoftLayer API username. - * API key: Your SoftLayer API key, - */ -$client = SoftLayer_SoapClient::getClient('SoftLayer_Account', null, $apiUsername, $apiKey); - -/** - * Once your client object is created you can call API methods for that service - * directly against your client object. A call may throw an exception on error, - * so it's best to try your call and catch exceptions. - * - * This example calls the getObject() method in the SoftLayer_Account API - * service. - * It retrieves basic account information, and is a great way to test your API - * account and connectivity. - */ -try { - print_r($client->getObject()); -} catch (Exception $e) { - die($e->getMessage()); -} - -/** - * For a more complex example we’ll retrieve a support ticket with id 123456 - * along with the ticket’s updates, the user it’s assigned to, the servers - * attached to it, and the datacenter those servers are in. We’ll retrieve our - * extra information using a nested object mask. After we have the ticket we’ll - * update it with the text ‘Hello!’. - */ - -// Declare an API client to connect to the SoftLayer_Ticket API service. -$client = SoftLayer_SoapClient::getClient('SoftLayer_Ticket', 123456, $apiUsername, $apiKey); - -// Assign an object mask to our API client: -$objectMask = new SoftLayer_ObjectMask(); -$objectMask->updates; -$objectMask->assignedUser; -$objectMask->attachedHardware->datacenter; -$client->setObjectMask($objectMask); - -// Retrieve the ticket record. -try { - $ticket = $client->getObject(); - print_r($ticket); -} catch (Exception $e) { - die('Unable to retrieve ticket record: ' . $e->getMessage()); -} - -// Now update the ticket. -$update = new stdClass(); -$update->entry = 'Hello!'; - -try { - $update = $client->addUpdate($update); - echo "Updated ticket 123456. The new update's id is " . $update[0]->id . '.'; -} catch (Exception $e) { - die('Unable to update ticket: ' . $e->getMessage()); -} diff --git a/modules/devshop/devshop_cloud/drush/Provision/Service/provider/digitalocean.php b/modules/devshop/devshop_cloud/drush/Provision/Service/provider/digitalocean.php deleted file mode 100644 index 48921b85b..000000000 --- a/modules/devshop/devshop_cloud/drush/Provision/Service/provider/digitalocean.php +++ /dev/null @@ -1,42 +0,0 @@ -server: Provision_Context_server - */ - function save_server() { - - // Look for provider_server_identifier - $server_identifier = $this->server->provider_server_identifier; - - // If server ID is already found, move on. - if (!empty($server_identifier)) { - drush_log('[DEVSHOP] Server Identifier Found: ' . $server_identifier . ' Not creating new server.', 'ok'); - } - // If there is no server ID, create the server. - else { - - drush_log('[DEVSHOP] Server Identifier not found. Creating new server!', 'ok'); - - // Faking our provider_data response. - $this->server->setProperty('provider_data', array( - 'hello' => 'do', - 'fake data' => 'from digitalocean', - )); - - // Faking our provider server identifier. - $this->server->setProperty('provider_server_identifier', '123456789'); - - $this->server->setProperty('ip_addresses', array( - '1.2.3.4' - )); - - drush_log('[DEVSHOP] Server Identifier found: 123456789. Assumed server was created.', 'ok'); - } - } -} \ No newline at end of file diff --git a/modules/devshop/devshop_cloud/drush/Provision/Service/provider/rackspace.php b/modules/devshop/devshop_cloud/drush/Provision/Service/provider/rackspace.php deleted file mode 100644 index e4641db63..000000000 --- a/modules/devshop/devshop_cloud/drush/Provision/Service/provider/rackspace.php +++ /dev/null @@ -1,137 +0,0 @@ -server->rackspace_id = drush_get_option('rackspace_id', ''); -// $this->server->rackspace_image = drush_get_option('rackspace_image', ''); -// $this->server->rackspace_flavor = drush_get_option('rackspace_flavor', ''); -// $this->server->attributes_json = drush_get_option('attributes_json', ''); -// $this->server->role = drush_get_option('role', ''); -// -// } -// -// /** -// * This is run immediately after provision saves the server config files. -// * -// * -// * Provision client home path /var/aegir/clients is writable. -// * [DEVUDO] Verifying Server anotherfaker -// */ -// function verify_server_cmd() { -// drush_log('[DEVUDO] Verifying Server ' . d()->remote_host, 'ok'); -// -// $server_fqdn = d()->remote_host; -// $role = $this->server->role; -// $rackspace_flavor = $this->server->rackspace_flavor; // 2 -// $rackspace_image = $this->server->rackspace_image; -// $rackspace_id = $this->server->rackspace_id; -// $attributes = $this->server->attributes_json; -// -// $ips = array(); -// -// // Look for this chef node on Chef Server -// drush_log("[DEVUDO] Looking for chef node $server_fqdn on chef server", 'ok'); -// $chef_node = shop_get_server($server_fqdn); -// -// // If shop_get_server() returns a string, knife node show didn't work. -// if (is_string($chef_node)) { -// -// // If the error is NOT object not found, there was a more serious error -// if (strpos($chef_node, 'ERROR: The object you are looking for could not be found') !== 0){ -// return drush_set_error(DRUSH_DEVUDO_ERROR, '[DEVUDO] knife failed: ' . $chef_node); -// } -// // Otherwise, we just don't have a chef node of that name yet. -// // So, create a new server. -// drush_log("[DEVUDO] Chef Node not found. Creating server...", 'ok'); -// -// drush_log("[DEVUDO] Running: drush server-create $server_fqdn --role=$role --rackspace_flavor=$rackspace_flavor --rackspace_image=$rackspace_image --attributes=$attributes", 'ok'); -// -// drush_set_option('rackspace_flavor', $rackspace_flavor); -// drush_set_option('rackspace_image', $rackspace_image); -// drush_set_option('role', $role); -// drush_set_option('attributes', $attributes); -// -// $data = drush_shop_provision_server_create($server_fqdn); -// $ips[] = $data['Public IP Address']; -// $ips[] = $data['Private IP Address']; -// -// // Save for shop_hosting_post_hosting_verify_task() -// drush_set_option('rackspace_id', $data['Instance ID']); -// drush_set_option('ip_addresses', $ips); -// } -// // If we got a server node... run chef-client to update it. -// else { -// $ip = $chef_node->automatic->ipaddress; -// drush_log("[DEVUDO] Chef node found with name:$server_fqdn ip:$ip Preparing attributes...", 'ok'); -// -// // @TODO: Copy the attributes file and run chef-client again. -// // Save new json data to file -// -// $json_path = "/tmp/$server_fqdn.json"; -// $attributes_json = $attributes; -// file_put_contents($json_path, $attributes_json); -// -// // Sync file to server -// // Use IP in case something is wrong with DNS -// if (!empty($ip)){ -// $host = $ip; -// } -// else { -// $host = $server_fqdn; -// } -// -// // @TODO: This line implies that aegir already has ssh access to devudo@host -// shop_exec("scp $json_path devudo@$host:~/attributes.json"); -// -// // Run chef-client to update the server itself. -// $chef_client_cmd = "sudo /usr/bin/chef-client -j attributes.json"; -// $chef_client_cmd_exec = escapeshellarg($chef_client_cmd); -// drush_log("[DEVUDO] Running chef-client on $server_fqdn:", 'ok'); -// shop_exec("knife ssh name:$server_fqdn -x devudo $chef_client_cmd_exec -a ipaddress"); -// } -// parent::verify_server_cmd(); -// } -// -// -// -// function config_data($config = null, $class = null) { -// $data = parent::config_data($config, $class); -// $data['rackspace_id'] = $this->server->rackspace_id; -// $data['rackspace_image'] = $this->server->rackspace_image; -// $data['rackspace_flavor'] = $this->server->rackspace_flavor; -// $data['role'] = $this->server->role; -// $data['attributes_json'] = $this->server->attributes_json; -// return $data; -// } -// -// static function option_documentation() { -// return array( -// '--rackspace_id' => 'The unique rackspace server ID.', -// '--rackspace_image' => 'The rackspace server image.', -// '--rackspace_flavor' => 'The rackspace server flavor.', -// '--role' => 'The chef role.', -// '--attributes_json' => 'JSON encoded attributes.', -// ); -// } -// -// /** -// * Ask the web server to check for and load configuration changes. -// */ -// function parse_configs() { -// return TRUE; -// } -//} diff --git a/modules/devshop/devshop_cloud/drush/Provision/Service/provider/softlayer.php b/modules/devshop/devshop_cloud/drush/Provision/Service/provider/softlayer.php deleted file mode 100644 index 0782615a9..000000000 --- a/modules/devshop/devshop_cloud/drush/Provision/Service/provider/softlayer.php +++ /dev/null @@ -1,88 +0,0 @@ -server->provider_server_identifier; - - // If server ID is already found, move on. - if ($server_identifier) { - drush_log('[DEVSHOP] Server Identifier Found. Not creating new server.', 'ok'); - } - // If there is no server ID, create the server. - else { - - drush_log('[DEVSHOP] Server Identifier not found. Creating new server!', 'ok'); - - $server_fqdn = d()->remote_host; - - drush_log('[DEVSHOP|softlayer] Creating Server ' . $server_fqdn . '...', 'notice'); - - // Initialize an API client for the SoftLayer_Account service. - $virtual_guest = $this->softlayer_client('SoftLayer_Virtual_Guest'); - $provider_options = $this->prepare_provider_options(); - - // Retrieve our account record - try { - - // @TODO: Add more robust simulation. - //$server = array( - // 'id' => '00000', - // 'stuff' => 'from softlayer', - //); - $server = (array) $virtual_guest->createObject($provider_options); - - drush_log('[DEVSHOP|softlayer] Created server in softlayer: ' . $server['id'], 'ok'); - } catch (Exception $e) { - return drush_set_error('DEVSHOP_CLOUD_API_ACCESS_DENIED', $e->getMessage()); - } - - $provider_data = (array) $server; - $this->server->setProperty('provider_data', $provider_data); - $this->server->setProperty('provider_server_identifier', $provider_data['id']); - } - } - - function prepare_provider_options() { - $devshop_cloud_provider_options = (object) drush_get_option('provider_options', ''); - - // Break up title into hostname (subdomain) and domain. - $provider_options = new stdClass(); - $domain = explode('.', d()->remote_host); - $provider_options->hostname = $domain[0]; - $provider_options->domain = implode('.', array_slice($domain, 1)); - $provider_options->startCpus = $devshop_cloud_provider_options->processors; - $provider_options->maxMemory = $devshop_cloud_provider_options->memory; - $provider_options->hourlyBillingFlag = TRUE; - $provider_options->localDiskFlag = TRUE; - $provider_options->dedicatedAccountHostOnlyFlag = FALSE; - $provider_options->operatingSystemReferenceCode = $devshop_cloud_provider_options->operatingSystems; - - $provider_options->datacenter = new stdClass(); - $provider_options->datacenter->name = $devshop_cloud_provider_options->datacenter; - - return $provider_options; - } - - /** - * Helper for getting a softlayer client. - * @param $service - * @return \Softlayer_SoapClient - */ - private function softlayer_client($service, $id = null) { - $api_key = drush_get_option('softlayer_api_key'); - $username = drush_get_option('softlayer_api_username'); - - // Initialize an API client for the SoftLayer_Account service. - $client = SoftLayer_SoapClient::getClient($service, $id, $username, $api_key); - return $client; - } -} \ No newline at end of file diff --git a/modules/devshop/devshop_cloud/drush/README.md b/modules/devshop/devshop_cloud/drush/README.md deleted file mode 100644 index 591c74eb7..000000000 --- a/modules/devshop/devshop_cloud/drush/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# devshop_cloud -Cloud server provisioning for devshop. diff --git a/modules/devshop/devshop_cloud/drush/devshop_cloud.drush.inc b/modules/devshop/devshop_cloud/drush/devshop_cloud.drush.inc deleted file mode 100644 index e44fe5fb5..000000000 --- a/modules/devshop/devshop_cloud/drush/devshop_cloud.drush.inc +++ /dev/null @@ -1,43 +0,0 @@ - NULL); -} diff --git a/modules/devshop/devshop_cloud/drush/tools.inc b/modules/devshop/devshop_cloud/drush/tools.inc deleted file mode 100644 index bce0161d3..000000000 --- a/modules/devshop/devshop_cloud/drush/tools.inc +++ /dev/null @@ -1,45 +0,0 @@ - 'devshop_dothooks')); +} diff --git a/modules/devshop/devshop_dothooks/devshop_dothooks.module b/modules/devshop/devshop_dothooks/devshop_dothooks.module new file mode 100644 index 000000000..6bfc6bb5b --- /dev/null +++ b/modules/devshop/devshop_dothooks/devshop_dothooks.module @@ -0,0 +1,151 @@ +settings->deploy['dothooks']) || empty($environment->settings->deploy['dothooks'])){ + return; + } + + if (file_exists($environment->repo_path . '/.hooks.yaml')) { + $hooks_file = '.hooks.yaml'; + $hooks_path = $environment->repo_path . '/.hooks.yaml'; + } + elseif (file_exists($environment->repo_path . '/.hooks.yml')) { + $hooks_file = '.hooks.yml'; + $hooks_path = $environment->repo_path . '/.hooks.yml'; + } + elseif (file_exists($environment->repo_path . '/.hooks')) { + $hooks_file = '.hooks'; + $hooks_path = $environment->repo_path . '/.hooks'; + } + + $environment->dothooks_file_name = $hooks_file; + $environment->dothooks_file_path = $hooks_path; + + // Attempt to parse + if (!empty($environment->dothooks_file_name)) { + try { + $environment->dothooks = $yaml->parse(file_get_contents($hooks_path)); + } catch (\Symfony\Component\Yaml\Exception\ParseException $e) { + $environment->warnings[] = array( + 'text' => t('Invalid YAML in !file: !message', array( + '!file' => $hooks_path, + '!message' => $e->getMessage(), + )), + 'type' => 'error', + ); + } + } +} + +/** + * Implements hook_help() + * @param $path + * @param $arg + * @return string + */ +function devshop_dothooks_help($path, $arg) +{ + switch ($path) { + // Main module help for the block module + case 'admin/help#devshop_dothooks': + $note = t( + 'You can control what happens on deploy through a .hooks file in your repository.' + ); + + return <<$note

+
+# Fires after an environment is installed.
+install: |
+  drush {{alias}} vset site_name "Hooks Hooks Hooks"
+
+# Fires after code is deployed. A "deployment" happens when you push to your
+# git repository or select a new branch or tag for your environment.
+deploy: |
+  drush {{alias}} updb -y
+  drush {{alias}} cc all
+
+# Fires after "verify" task.
+verify: |
+  drush {{alias}} status
+
+# Fires after "Run Tests" task.
+test: |
+  drush {{alias}} uli
+
+
+# Fires after "Deploy Data (Sync)" task.
+sync: |
+  drush {{alias}} en devel -y
+
+
+HTML; + + } +} + + +/** + * Runs a hook for a task. + * @param $hook + * @param $task + */ +function devshop_dothooks_run_hook($hook, $environment) { + + // Reload the alter hook in case there are new hooks. + devshop_dothooks_devshop_environment_alter($environment, $environment->project); + + // Respect drush option, but default to environment settings. + if (!drush_get_option('dothooks', $environment->settings->deploy['dothooks'])) { + drush_log('[.hooks] Environment not configured to run .hooks commands.', 'info'); + return; + } + + // If no dothooks file is found, throw an error. + if (empty($environment->dothooks)) { + drush_log(dt('Hook file not found, but the project is configured to use them. Create a .hooks file or turn off "Run deploy commands in the .hooks file".'), 'notice'); + return; + } + + drush_log('[.hooks] Hook file found: ' . $environment->dothooks_file_name, 'ok'); + + // Allow big string or lists of commands. + if (is_array($environment->dothooks[$hook])) { + $hooks = array_filter($environment->dothooks[$hook]); + } + else { + $hooks = array_filter(explode("\n", $environment->dothooks[$hook])); + } + + $env = $_SERVER; + $env['DEVSHOP_PROJECT'] = $environment->project_name; + $env['DEVSHOP_ENVIRONMENT'] = $environment->name; + + // Prepare and run each command. + foreach ($hooks as $hook_line) { + $hook_line = strtr($hook_line, array( + '{{alias}}' => $environment->system_alias, + )); + provision_process($hook_line, $environment->repo_path, 'DevShop .hooks.yml'); + } +} + diff --git a/modules/devshop/devshop_dothooks/hooks.php b/modules/devshop/devshop_dothooks/hooks.php new file mode 100644 index 000000000..ccc9830ce --- /dev/null +++ b/modules/devshop/devshop_dothooks/hooks.php @@ -0,0 +1,53 @@ +ref->type == 'site') { + devshop_dothooks_run_hook('verify', $task->ref->environment); + } +} + +/** + * Implementation of hook_post_hosting_TASK_TYPE_task() + * for DevShop Deploy tasks. + * + * Runs the "deploy" dotHook + */ +function devshop_dothooks_post_hosting_devshop_deploy_task($task, $data) { + devshop_dothooks_run_hook('deploy', $task->ref->environment); +} + +/** + * Implementation of hook_post_hosting_TASK_TYPE_task() + * for Run Tests tasks. + * + * Runs the "test" dotHook + */ +function devshop_dothooks_post_hosting_test_task($task, $data) { + devshop_dothooks_run_hook('test', $task->ref->environment); +} + +/** + * Implementation of hook_post_hosting_TASK_TYPE_task() + * for Sync tasks. + * + * Runs the "sync" dotHook + */ +function devshop_dothooks_post_hosting_sync_task($task, $data) { + devshop_dothooks_run_hook('sync', $task->ref->environment); +} + +/** + * Implementation of hook_post_hosting_TASK_TYPE_task() + * for Install tasks. + * + * Runs the "install" dotHook + */ +function devshop_dothooks_post_hosting_install_task($task, $data) { + devshop_dothooks_run_hook('install', $task->ref->environment); +} \ No newline at end of file diff --git a/modules/devshop/devshop_extra_users/README.md b/modules/devshop/devshop_extra_users/README.md new file mode 100644 index 000000000..9abb25762 --- /dev/null +++ b/modules/devshop/devshop_extra_users/README.md @@ -0,0 +1,18 @@ +DevShop Extras: Users +===================== + +This module provides an example of how to use the DevShop front-end to take action +after an environment is installed. + +Enable it, then visit the "Create Environment" form for a project. + +You will see a form field for "Manager Email". This field gets saved into the environment settings automatically. + +Then, in `devshop_extras_users.drush.inc` during the `hook_post_hosting_TASKTYPE_task` +hook, this module creates a new user in your drupal site immediately after the +installation. + +Nothing else happens here. Typically you would want to set a role or send an +email. + +Use this code as an example for extending your own environments. \ No newline at end of file diff --git a/modules/devshop/devshop_extra_users/devshop_extra_users.drush.inc b/modules/devshop/devshop_extra_users/devshop_extra_users.drush.inc new file mode 100644 index 000000000..9b9b75e27 --- /dev/null +++ b/modules/devshop/devshop_extra_users/devshop_extra_users.drush.inc @@ -0,0 +1,34 @@ +ref->type == 'site' && isset($task->ref->environment->settings->manager_email)) { + + // Create an extra user. + $email = $task->ref->environment->settings->manager_email; + $password = provision_password(); + $arguments = array('name' => $email); + $data = array( + 'mail' => $email, + 'password' => $password, + ); + + drush_log('Manager email is found! running user-create...', 'ok'); + provision_backend_invoke($task->ref->title, 'user-create', $arguments, $data); + + drush_log(dt('User !user has been created with password !password', array( + '!user' => $email, + '!password' => $password, + )), 'ok'); + + drush_log('No email has been sent! Please notify your new user.'); + } +} diff --git a/modules/devshop/devshop_extra_users/devshop_extra_users.info b/modules/devshop/devshop_extra_users/devshop_extra_users.info new file mode 100644 index 000000000..bcc11ca1d --- /dev/null +++ b/modules/devshop/devshop_extra_users/devshop_extra_users.info @@ -0,0 +1,5 @@ +name = DevShop Extra Install +description = Example module for loading extra info for an install profile. +core = 7.x +package = DevShop + diff --git a/modules/devshop/devshop_extra_users/devshop_extra_users.module b/modules/devshop/devshop_extra_users/devshop_extra_users.module new file mode 100644 index 000000000..e1d13d86a --- /dev/null +++ b/modules/devshop/devshop_extra_users/devshop_extra_users.module @@ -0,0 +1,15 @@ + 'textfield', + '#title' => t('Manager Email'), + '#description' => t('Enter an email address and a user will be created.'), + ); + } +} diff --git a/modules/devshop/devshop_github/.gitignore b/modules/devshop/devshop_github/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/modules/devshop/devshop_github/composer.json b/modules/devshop/devshop_github/composer.json new file mode 100644 index 000000000..56e09bfdb --- /dev/null +++ b/modules/devshop/devshop_github/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "knplabs/github-api": "~1.2" + } +} \ No newline at end of file diff --git a/modules/devshop/devshop_github/devshop_github.drush.inc b/modules/devshop/devshop_github/devshop_github.drush.inc new file mode 100644 index 000000000..01f7ad763 --- /dev/null +++ b/modules/devshop/devshop_github/devshop_github.drush.inc @@ -0,0 +1,117 @@ +task_type, $types) && isset($task->ref->project) && !empty($task->ref->project) && isset($task->ref->environment) && !empty($task->ref->environment)) { + $project = $task->ref->project; + $environment = $task->ref->environment; + } + else { + return; + } + + // If a pull request object is available... + if (isset($environment->github_pull_request->pull_request_object->deployment)) { + + // If project is configured to reinstall every time, only react on "install" tasks. Otherwise, we get two github deployments because both a "deploy" (git pull) and a "install" task are run on each git push. + if ($project->settings->github['pull_request_reinstall'] && $task->task_type == 'devshop-deploy') { + return; + } + + // Create a deployment status + $owner = $project->github_owner; + $repo = $project->github_repo; + $deployment_id = $environment->github_pull_request->pull_request_object->deployment->id; + + try { + $token = variable_get('devshop_github_token', ''); + $client = new \Github\Client(); + $client->authenticate($token, Github\Client::AUTH_HTTP_TOKEN); + + $params = new stdClass(); + if ($status == HOSTING_TASK_SUCCESS || $status == HOSTING_TASK_WARNING) { + $params->state = $state = 'success'; + } + else { + $params->state = $state = 'failure'; + } + + // If task is a test run, only submit a commit status for devshop/tests context. + if ($task->task_type == 'test') { + $sha = $environment->github_pull_request->pull_request_object->head->sha; + + $params = new stdClass(); + $params->state = $state; + $params->target_url = url("node/{$task->nid}/view", array('absolute' => TRUE)); + + if ($status == HOSTING_TASK_WARNING) { + $params->description = t('DevShop: Tests passed with warnings'); + } + else { + $params->description = t('DevShop: Tests !status!', array('!status' => $state)); + } + $params->context = "devshop/{$project->name}/tests"; + + $status = $client->getHttpClient()->post("/repos/$owner/$repo/statuses/$sha", json_encode($params)); + drush_log('Commit status created for devshop/tests!', 'success'); + } + // Otherwise we create a deployment and a commit status. + else { + + $params->target_url = $environment->url; + $params->description = t('Visit !url', array('!url' => $task->ref->environment->url)); + $post_url = "/repos/$owner/$repo/deployments/{$deployment_id}/statuses"; + + drush_log('Attempting to create github deployment status: ' . $post_url, 'success'); + + $deployment_status = $client->getHttpClient()->post($post_url, json_encode($params)); + drush_log('Deployment status created!', 'success'); + + + // Update Status API + + // Create a status + $sha = $environment->github_pull_request->pull_request_object->head->sha; + + $params = new stdClass(); + $params->state = $state; + $params->target_url = url("node/{$task->nid}", array('absolute' => TRUE));; + + if ($status == HOSTING_TASK_WARNING) { + $params->description = t('DevShop: Deploy success with warnings. [!url]', array( + '!url' => $environment->url, + )); + } + else { + $params->description = t('DevShop: Deploy !status [!url]', array( + '!status' => $state, + '!url' => $environment->url, + )); + } + $params->context = "devshop/{$project->name}/deploy"; + + $deployment_status = $client->getHttpClient()->post("/repos/$owner/$repo/statuses/$sha", json_encode($params)); + drush_log('Commit status created!', 'success'); + + // If deploy task fails, tests won't run. + if ($environment->settings->deploy['test'] && $status == HOSTING_TASK_ERROR) { + + $params = new stdClass(); + $params->state = $state; + $params->description = t('DevShop: Tests not run due to Deploy Fail'); + $params->context = "devshop/{$project->name}/tests"; + + $deployment_status = $client->getHttpClient()->post("/repos/$owner/$repo/statuses/$sha", json_encode($params)); + drush_log('Commit status created for devshop/tests', 'success'); + } + } + } catch (Github\Exception\RuntimeException $e) { + drush_log('GitHub API Error: ' . $e->getMessage(), 'error'); + } + } +} diff --git a/modules/devshop/devshop_github/devshop_github.info b/modules/devshop/devshop_github/devshop_github.info new file mode 100644 index 000000000..00126595b --- /dev/null +++ b/modules/devshop/devshop_github/devshop_github.info @@ -0,0 +1,8 @@ +name = DevShop GitHub +description = Integration with GitHub +core = 7.x +package = DevShop +files[] = includes/add-key.inc +files[] = includes/admin.inc +dependencies[] = devshop_projects +dependencies[] = composer_manager diff --git a/modules/devshop/devshop_github/devshop_github.install b/modules/devshop/devshop_github/devshop_github.install new file mode 100644 index 000000000..23b3482ec --- /dev/null +++ b/modules/devshop/devshop_github/devshop_github.install @@ -0,0 +1,94 @@ + array( + 'id' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'description' => 'Pull Request ID', + ), + 'number' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'description' => 'Pull Request Number', + ), + 'project_nid' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'description' => "The project's Node ID.", + ), + 'environment_name' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => 64, + 'default' => '', + 'description' => 'Environment name for this pull request environment.', + ), + 'pull_request_object' => array( + 'type' => 'text', + 'not null' => FALSE, + 'size' => 'big', + 'description' => 'A serialized array of settings for this environment.', + ), + ), + 'primary key' => array('id'), + ); + return $schema; +} + +/** + * Implements hook_install(). + */ +function devshop_github_install() { + + // Push devshop_github's system weight to 1. + db_update('system') + ->fields(array( + 'weight' => 2 + )) + ->condition('name', 'devshop_github') + ->execute(); + + // Display a message about setting a github personal token. + drupal_set_message(t('DevShop GitHub module has been enabled. You must add an access token to enable full functionality at !link.', array( + '!link' => l(t('the settings page'), 'admin/devshop/github'), + ))); +} + +/** + * Set a weight higher than devshop_project so our form doesn't get obliterated by + * devshop_projects_form_project_node_form_alter() + */ +function devshop_github_update_7000() { + db_update('system') + ->fields(array( + 'weight' => 1 + )) + ->condition('name', 'devshop_github') + ->execute(); +} + +/** + * Set a weight higher than devshop_project module. + */ +function devshop_github_update_7001() { + db_update('system') + ->fields(array( + 'weight' => 2 + )) + ->condition('name', 'devshop_github') + ->execute(); +} \ No newline at end of file diff --git a/modules/devshop/devshop_github/devshop_github.module b/modules/devshop/devshop_github/devshop_github.module new file mode 100644 index 000000000..d176cc7fc --- /dev/null +++ b/modules/devshop/devshop_github/devshop_github.module @@ -0,0 +1,1326 @@ + 'GitHub', + 'description' => 'DevShop GitHub Integration Settings', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('devshop_github_settings_form'), + 'access arguments' => array('administer projects'), + 'file' => 'admin.inc', + 'file path' => drupal_get_path('module', 'devshop_github') . '/includes', + 'type' => MENU_LOCAL_TASK, + ); + $items['admin/devshop/github/add-key'] = array( + 'title' => 'Add public key to GitHub Account', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('devshop_github_add_key_to_account'), + 'access arguments' => array('administer projects'), + 'file' => 'add-key.inc', + 'file path' => drupal_get_path('module', 'devshop_github') . '/includes', + 'type' => MENU_CALLBACK, + ); + $items['admin/devshop/github/load-repos'] = array( + 'title' => 'Load all repos that the user has access to.', + 'page callback' => 'devshop_github_get_repositories_page', + 'access arguments' => array('administer projects'), + 'file' => 'admin.inc', + 'file path' => drupal_get_path('module', 'devshop_github') . '/includes', + 'type' => MENU_CALLBACK, + ); + return $items; +} + +/** + * Implements hook_form_FORM_ID_alter() for project_node_form(). + */ +function devshop_github_form_project_node_form_alter(&$form, &$form_state, $form_id) { + $node = $form['#node']; + + if ($node->project->git_provider != 'github') { + return; + } + + //All settings git pull in project page + $form['project']['settings']['github'] = array( + '#type' => 'fieldset', + '#group' => 'project_settings', + '#collapsible' => TRUE, + '#collapsed' => arg(1) != $node->nid, + '#title' => t('GitHub Integration'), + ); + + // Pull Requests create environments? + // $form['github']['pull_request_environments'] = array( + $form['project']['settings']['github']['pull_request_environments'] = array( + '#type' => 'checkbox', + '#title' => t('Create Environments for Pull Requests'), + '#default_value' => isset($node->project->settings->github) ? $node->project->settings->github['pull_request_environments'] : FALSE, + '#description' => t('If using GitHub, create a new environment when a new Pull Request is created.'), + ); + + // Delete Pull Request environments? + // $form['github']['pull_request_environments_delete'] = array( + $form['project']['settings']['github']['pull_request_environments_delete'] = array( + '#type' => 'checkbox', + '#title' => t('Delete Pull Request Environments'), + '#default_value' => isset($node->project->settings->github) ? $node->project->settings->github['pull_request_environments_delete'] : FALSE, + '#description' => t('When Pull Requests are closed, delete the environment.'), + '#states' => array( + 'visible' => array( + ':input[name="project[settings][github][pull_request_environments]"]' => array('checked' => TRUE), + ), + ), + ); + + // Pull Request Environment method. + // $form['github']['pull_request_environments_method'] = array( + + $environments = array_keys($node->project->environments); + $options = array( + t('Install Drupal') => array( + 'devshop__github__install' => empty($node->project->install_profile)? t('Default install profile.'): $node->project->install_profile, + ), + t('Clone another environment') => array(), + ); + + if (empty($environments)) { + $options[t('Clone another environment')][] = t('No environments available. Check Project settings when you have one.'); + } + else { + $options[t('Clone another environment')] = array_combine($environments, $environments); + } + $form['project']['settings']['github']['pull_request_environments_method'] = array( + '#type' => 'select', + '#title' => t('Pull Request Environment Creation Method'), + '#default_value' => isset($node->project->settings->github) ? + $node->project->settings->github['pull_request_environments_method'] : 'devshop__github__install', + '#description' => t('Select the method for creating the pull request environments.'), + '#options' => $options, + '#states' => array( + 'visible' => array( + ':input[name="project[settings][github][pull_request_environments]"]' => array('checked' => TRUE), + ), + ), + ); + $form['project']['settings']['github']['pull_request_reinstall'] = array( + '#type' => 'checkbox', + '#title' => t('Reinstall Pull Request Environments on every git push.'), + '#default_value' => isset($node->project->settings->github) ? + $node->project->settings->github['pull_request_reinstall'] : 0, + '#description' => t('Destroy and reinstall Pull Request environments on every code push. All data in environments created via Pull Request will be destroyed.'), + '#states' => array( + 'visible' => array( + ':input[name="project[settings][github][pull_request_environments]"]' => array('checked' => TRUE), + ), + ), + ); +} + +/** + * Implements hook_form_alter(). + */ +function devshop_github_form_devshop_project_create_step_git_alter(&$form, &$form_state, $form_id) { + + module_load_include('inc', 'devshop_github', 'includes/admin'); + + if (!devshop_github_check_key(TRUE)) { + $form['connect'] = array( + '#type' => 'container', + '#markup' => t('DevShop Git Integration is not set up. Check settings and try again.'), + '#prefix' => "
", + '#suffix' => '
', + '#weight' => -1000, + ); + + $form['connect']['message'] = array( + '#markup' => ' ' . t('DevShop GitHub Integration is not set up. You should complete setup on the Settings page before continuing.'), + ); + $form['connect']['button'] = array( + '#markup' => l(t('DevShop GitHub Settings'),'admin/devshop/github', array('attributes' => array( + 'class' => array('btn btn-link'), + )))); + } + else { + $form['connect'] = array(); + } + + $repos = variable_get('devshop_github_available_repositories', array()); + foreach ($repos as $repo_name => $repo) { + $options[$repo['org']][$repo['url']] = $repo_name; + } + + if (count($options) && empty($form['source']['git_url']['#default_value'])) { + $form['source']['git_source']['#default_value'] = 'github_existing'; + } + + unset($form['source']['git_source']['#options']['custom']); + $form['source']['git_source']['#options']['custom'] = t('Custom Git repository URL'); + + if (empty($options)) { + $form['source']['github_repos'] = array( + '#title' => t('Use an existing GitHub repository'), + '#description' => t('No GitHub repositories found. !link', array( + '!link' => l(t('Refresh GitHub Repos'), 'admin/devshop/github/load-repos', array( + 'query' => drupal_get_destination(), + 'attributes' => array( + 'class' => array('btn btn-primary'), + ), + )))), + '#type' => 'item', + '#states' => array( + 'visible' => array( + ':input[name="git_source"]' => array( + 'value' => 'github_existing' + ), + ), + ), + ); + } + else { + + $form['source']['github_repos'] = array( + '#title' => t('Use an existing GitHub repository'), + '#type' => 'select', + '#options' => $options, + '#default_value' => $form['source']['git_url']['#default_value'], + '#description' => t('Select the GitHub repository to use for this project. Remember, it must contain a Drupal site.') . ' ' . l(t('Refresh GitHub Repos List'), 'admin/devshop/github/load-repos', array('query' => drupal_get_destination())), + '#bootstrap_ignore_pre_render' => TRUE, + '#states' => array( + 'visible' => array( + ':input[name="git_source"]' => array( + 'value' => 'github_existing' + ), + ), + ), + ); + + } + $form['source']['github_create'] = array( + '#type' => 'container', + '#tree' => TRUE, + '#attributes' => array( + 'class' => array( + 'form-group' + ), + ), + '#states' => array( + 'visible' => array( + ':input[name="git_source"]' => array( + 'value' => 'github_create' + ), + ), + ), + '#parents' => array( + 'github_create', + ), + + ); + + try { + $client = devshop_github_client(); + $account = $client->currentUser()->show(); + $repo_owners = variable_get('devshop_github_available_organizations', array('' => t('You do not have repository create permissions for any organizations.'))); + $repo_owners[$account['login']] = $account['login']; + + asort($repo_owners); + $form['source']['github_create']['github_organization'] = array( + '#type' => 'select', + '#title' => t('Create a new GitHub repository'), + '#options' => $repo_owners, + '#bootstrap_ignore_pre_render' => TRUE, + '#attributes' => array( + 'class' => array( + 'form-control form-inline' + ), + ), + ); + $form['source']['github_create']['github_repository_name'] = array( + '#type' => 'textfield', + '#attributes' => array( + 'placeholder' => t('repo_name'), + 'class' => array('form-inline'), + ), + '#element_validate' => array( + 'devshop_github_create_repository' + ), + ); + $form['source']['github_create']['github_repository_public'] = array( + '#type' => 'radios', + '#options' => array( + 1 => t('Public: Anyone can see this repository. You choose who can commit.'), + 0 => t('Private: You choose who can see and commit to this repository.') + ), + '#default_value' => 1, + ); + } + catch (\Exception $e) { + $form['source']['github_create'] = array( + '#markup' => t('GitHub token not found. Unable to create new repositories.'), + ); + drupal_set_message($e->getMessage(), 'warning'); + } + + $form['source']['settings']['#weight'] = 1; + $form['source']['settings']['#tree'] = 1; + $form['github_repository_source'] = array( + '#type' => 'container', + '#attributes' => array( + 'class' => array( + 'well' + ), + ), + '#states' => array( + 'visible' => array( + ':input[name="git_source"]' => array( + 'value' => 'github_create' + ), + ), + ), + '#tree' => 1, + '#parents' => array( + 'settings', + 'github_repository_source', + ), + ); + + + $form['github_repository_source']['populate_choice'] = array( +// '#prefix' => '
', + '#type' => 'radios', + '#default_value' => 'create', + '#title' => t('Choose what to put into your new Git repository'), + '#options' => array( + 'create' => t('Create project using composer'), + 'import' => t('Import from another Git URL'), + ), + ); + + $suggested_repos = variable_get('devshop_git_repo_suggestions', array( + 'git@github.com:opendevshop/devshop-composer-template.git', + )); + $options = array_combine(array_values($suggested_repos),array_values($suggested_repos)); + $options['custom'] = t('Enter a custom Git URL'); + + $form['github_repository_source']['import'] = array( + '#type' => 'textfield', + '#attributes' => array( + 'placeholder' => t('git@githost.com:example/repo.git'), + ), + '#description' => t('Your new repo will be a clone of this repository on the default branch.') .' ' . l(t('Suggested Repos'), '', array( + 'attributes' => array( + 'class' => array('put-back-suggested-repos'), + ), + )), + '#states' => array( + 'visible' => array( + ':input[name="settings[github_repository_source][populate_choice]"]' => array( + 'value' => 'import' + ), + 'select[name="settings[github_repository_source][import_suggestions]"]' => array( + 'value' => 'custom' + ), + ), + ), + '#element_validate' => array( + 'devshop_github_set_import_url' + ), + ); + + $form['github_repository_source']['import_suggestions'] = array( + '#type' => 'select', + '#options' => $options, + '#description' => t('Your new repo will be a direct clone of this repository. All commits will be loaded into the new repository.'), + '#states' => array( + 'visible' => array( + ':input[name="settings[github_repository_source][populate_choice]"]' => array( + 'value' => 'import' + ), + 'select[name="settings[github_repository_source][import_suggestions]"]' => array( + '!value' => 'custom' + ), + ), + ), + ); + + + $suggested_projects = variable_get('devshop_composer_project_suggestions', array( + 'devshop/composer-template:8.x-dev', + 'devshop/composer-template:7.x-dev', +// 'acquia/lightning-project', + )); + + $options = array_combine(array_values($suggested_projects),array_values($suggested_projects)); + $options['custom'] = t('Enter custom project'); + + $form['github_repository_source']['composer_project_suggestions'] = array( + '#type' => 'select', + '#options' => $options, + '#description' => t('The command above will be run and all the resulting files will be committed to your repository.'), + '#field_prefix' => '
composer create-project ', + '#field_suffix' => '' . t('projectname') .' +
', + '#states' => array( + 'visible' => array( + ':input[name="settings[github_repository_source][populate_choice]"]' => array( + 'value' => 'create' + ), + 'select[name="settings[github_repository_source][composer_project_suggestions]"]' => array( + '!value' => 'custom' + ), + ), + ), + ); + + $form['github_repository_source']['composer_project'] = array( + '#type' => 'textfield', + '#description' => t('The command above will be run and all the resulting files will be committed to your repository.') . ' ' . l(t('Suggested Projects'), '', array( + 'attributes' => array( + 'class' => array('put-back-suggested-projects'), + ), + )), + '#field_prefix' => '
composer create-project ', + '#field_suffix' => ' ' . t('projectname') .' +
', +// '#default_value' => $suggested_projects[0], + '#attributes' => array( + 'placeholder' => $suggested_projects[0], + ), + '#states' => array( + 'visible' => array( + ':input[name="settings[github_repository_source][populate_choice]"]' => array( + 'value' => 'create' + ), + 'select[name="settings[github_repository_source][composer_project_suggestions]"]' => array( + 'value' => 'custom' + ), + ), + ), + '#element_validate' => array( + 'devshop_github_set_composer_project' + ), + ); + + $form['github_repository_source']['wrapper'] = array( + '#markup' => drupal_valid_path('admin/devshop')? l(t("Modify suggested projects in DevShop Settings"), 'admin/devshop'): '', +// '#suffix' => '
', + ); + + $form['source']['git_url']['#required'] = FALSE; + $form['source']['git_url']['#element_validate'] = array( + 'devshop_github_create_project_form_set_values' + ); + + // We have to tell the front-end what type of field so we can assign the right event. + drupal_add_js(array( + 'devshop' => array( + 'projectNameSourceElements' => array( + '#edit-github-repos', + '#edit-github-create-github-repository-name', + ), + ), + ), 'setting'); + +} + +/** + * Implements hook_devshop_project_git_repo_options(). + */ +function devshop_github_devshop_project_git_repo_options() { + return array( + 'github_existing' => t('Use an existing GitHub repository'), + 'github_create' => t('Create a new GitHub repository'), + ); +} + +/** + * @param $element + * @param $form_state + * @param $form + */ +function devshop_github_create_project_form_set_values($element, &$form_state, &$form) { + + $form_state['values']['settings']['git_source'] = $form_state['values']['git_source']; + + if ($form_state['values']['git_source'] == 'github_existing') { + form_set_value($element, $form_state['values']['github_repos'] , $form_state); + } +} + +/** + * @param $element + * @param $form_state + * @param $form + */ +function devshop_github_set_import_url($element, &$form_state, &$form) { + if ($form_state['values']['settings']['github_repository_source']['import_suggestions'] != 'custom') { + form_set_value($element, $form_state['values']['settings']['github_repository_source']['import_suggestions'], $form_state); + } +} + +/** + * @param $element + * @param $form_state + * @param $form + */ +function devshop_github_set_composer_project($element, &$form_state, &$form) { + if ($form_state['values']['settings']['github_repository_source']['composer_project_suggestions'] != 'custom') { + form_set_value($element, $form_state['values']['settings']['github_repository_source']['composer_project_suggestions'], $form_state); + } +} + +/** + * Element validation for License Key. Pings devshop.support + * + * @param $element + * @param $form_state + * @param $form + */ +function devshop_github_create_repository($element, &$form_state, &$form) { + + if ($form_state['values']['git_source'] == 'github_create') { + + if (empty($element['#value'])) { + form_error($element, t('You must enter a repository name.')); + } + else { + $client = devshop_github_client(); + $name = $element['#value']; + // Must be NULL if org is username. + $organization = $form_state['values']['github_create']['github_organization'] == $client->currentUser()->show()['login']? NULL: $form_state['values']['github_create']['github_organization']; + $public = $form_state['values']['github_create']['github_repository_public']; + $description = t('Created by DevShop on @date', array('@date' => format_date(time()))); + + try { + $repo = $client->repos()->create($name, + $description, + '', + $public, + $organization + ); + form_set_value($form['source']['git_url'], $repo['ssh_url'],$form_state); + drupal_set_message(t('New GitHub Repository was created! See !link.', array( + '!link' => l($repo['html_url'], $repo['html_url']), + ))); + } + catch (\Exception $e) { + form_error($element, t('Unable to create repo. Error from GitHub: ') . $e->getMessage()); + } + } + } +} + +/** + * Implements hook_form_FORM_ID_alter() for project_node_form(). + */ +function devshop_github_form_devshop_project_create_step_sites_alter(&$form, &$form_state) { + + // Return if project isn't ready. + if (empty($form['install_profile'])) { + return; + } + + // Return if there is no github token + $github_token = variable_get('devshop_github_token', ''); + if (empty($github_token)) { + return; + } + + // Load project and github + $project_node = node_load($form['nid']['#value']); + $project = $project_node->project; + + // Return if provider is not github. + if ($project->git_provider != 'github') { + return; + } + + // Return if deploy method is not webhook. + if ($project->settings->deploy['method'] != 'webhook') { + return; + } + + $repo = $project->github_owner . '/' . $project->github_repo; + $repo_url = "http://github.com/" . $repo; + + $form['github_webhook'] = array( + '#title' => t('Setup GitHub Webhook'), + '#description' => t('Leave this box checked to automatically add a webhook to your GitHub Repository !link.', array( + '!link' => l($repo, $repo_url, array( + 'attributes' => array( + 'target' => '_blank', + ), + )) + )), + '#type' => 'checkbox', + '#default_value' => 1, + ); + + $form['#validate'][] = 'devshop_github_project_create_webhook'; + $form['#submit'][] = 'devshop_github_project_create_webhook'; +} + +/** + * Extra submit hook for last step of project create form. + */ +function devshop_github_project_create_webhook($form, $form_state) { + + // Return if box is not checked. + if (empty($form_state['values']['github_webhook'])) { + return; + } + + // Get Project + $project_node = node_load($form['nid']['#value']); + $project = $project_node->project; + + // Get GitHub client + $client = new Github\Client(); + $github_token = variable_get('devshop_github_token', ''); + $client->authenticate($github_token, Github\Client::AUTH_HTTP_TOKEN); + + // Create the webhook. + try { + $hook = $client->repo()->hooks()->create($project->github_owner, $project->github_repo, array( + 'name' => 'web', + 'active' => true, + 'events' => array( + 'push', + 'pull_request', + 'delete', + 'release', + ), + 'config' => array( + 'url' => $project->webhook_url, + 'content_type' => 'json', + 'insecure_ssl' => '1', + ), + )); + } + catch (Github\Exception\ValidationFailedException $e) { + if ($e->getMessage() == 'Validation Failed: Hook already exists on this repository') { + // For some reason, github always throws this exception on hooks()->create, but the webook still gets created!! + + // drupal_set_message(t("GitHub webhook added, but there is already an existing webhook. Please check your repository's !link.", array( +// '!link' => l(t('Webhook Settings'), $project->git_repo_url . '/settings/hooks'), +// )), 'warning'); + } + else { + drupal_set_message(t('GitHub Validation Exception: !exception', array('!exception' => $e->getMessage())), 'error'); + } + } + catch (Github\Exception\RuntimeException $e) { + drupal_set_message(t('GitHub Runtime Exception: !exception', array('!exception' => $e->getMessage())), 'error'); + } +} + +/** + * Implements hook_node_load(). + */ +function devshop_github_node_load($nodes, $types) { + if (count(array_intersect(array('project'), $types))) { + foreach ($nodes as $nid => $node) { + + // Look for a pull request object. + $pull_requests = db_query('SELECT * FROM {hosting_devshop_github_pull_requests} WHERE project_nid = :project_nid', array(':project_nid' => $nid)); + foreach ($pull_requests as $pull_request) { + if (!empty($pull_request->pull_request_object)) { + $pull_request->pull_request_object = unserialize($pull_request->pull_request_object); + if (isset($node->project->environments[$pull_request->environment_name])) { + $node->project->environments[$pull_request->environment_name]->github_pull_request = $pull_request; + } + } + } + + // Parse github owner and repo. + if (isset($node->project) && $node->project->git_provider == 'github') { + $node->project->github_owner = + $parts = explode('/', parse_url($node->project->git_repo_url, PHP_URL_PATH)); + $node->project->github_owner = $parts[1]; + $node->project->github_repo = $parts[2]; + } + } + } + +} + +/** + * Implements hook_node_update() for task insert. + * + * If task is a test run, send a "pending" commit status. + */ +function devshop_github_node_update($node) { + + // Only act on test triggers. + if ($node->type != 'task' || $node->type == 'task' && ($node->task_type != 'test' || $node->task_type != 'deploy')) { + return; + } + + // Load the site and check for environment. + $site = node_load($node->rid); + if (empty($site->environment) || empty($site->environment->github_pull_request) || empty($site->environment->settings->deploy['test'])) { + return; + } + + try { + $token = variable_get('devshop_github_token', ''); + $client = new \Github\Client(); + $client->authenticate($token, Github\Client::AUTH_HTTP_TOKEN); + + // Create a deployment status + $project = $site->project; + $owner = $project->github_owner; + $repo = $project->github_repo; + $sha = $site->environment->github_pull_request->pull_request_object->head->sha; + + $params = new stdClass(); + $params->state = 'pending'; + + if ($node->task_type != 'test') { + $params->target_url = url( + "node/{$node->nid}", + array('absolute' => true) + ); + $params->description = t('DevShop: Run Tests'); + $params->context = "devshop/{$project->name}/tests"; + } + elseif ($node->task_type != 'deploy') { + $params->target_url = $site->environment->url; + $params->description = t('DevShop: Deploy'); + $params->context = "devshop/{$project->name}/deploy"; + } + + // Post status to github + $status = $client->getHttpClient()->post("/repos/$owner/$repo/statuses/$sha", json_encode($params)); + + watchdog('devshop_github','Test run initiated. GitHub has been notified.'); + } catch (Github\Exception\RuntimeException $e) { + watchdog('devshop_github', 'GitHub Runtime Error in devshop_github_node_update(): ' . $e->getMessage()); + } +} + + +/** +* Implements hook_hosting_task_update_status() +*/ +// function devshop_github_hosting_task_update_status($task, $status) { +// +// $task_types = array( +// 'test', +// 'import', +// 'devshop-deploy', +// ); +// +// if (in_array($task->task_type, $task_types) && $task->ref->type == 'site' && isset($task->ref->environment->github_pull_request)) { +// +// // If autoloader is not available, return. +// if (!file_exists(__DIR__ . '/vendor/autoload.php')) { +// return; +// } +// +// // If project is not from github, return. +// if ($task->ref->project->git_provider != 'github') { +// return; +// } +// +// // Include vendors +// require_once 'vendor/autoload.php'; +// +// drush_log('===========================================', 'ok'); +// drush_log('Notifying github...', 'ok'); +// +// // Extract username and repo +// list($s, $owner, $repo) = explode('/', parse_url($task->ref->project->git_repo_url, PHP_URL_PATH)); +// +// try { +// $token = variable_get('devshop_github_token', ''); +// $client = new \Github\Client(); +// $client->authenticate($token, Github\Client::AUTH_HTTP_TOKEN); +// +// // Create a status +// $sha = $task->ref->environment->github_pull_request->pull_request_object->head->sha; +// +// if ($task->task_type == 'devshop-deploy'){ +// $description = t('Deployed to Environment: ') . _hosting_parse_error_code($status); +// $url = $task->ref->environment->url; +// } +// elseif ($task->task_type == 'test') { +// $description = t('Tests: ') . _hosting_parse_error_code($status); +// $url = url("devshop_tests/{$task->nid}/{$task->vid}", array('absolute' => TRUE)); +// } +// else { +// $description = 'Something happened...'; +// $url = $task->ref->environment->url; +// } +// +// if ($status == HOSTING_TASK_ERROR) { +// $state = 'error'; +// } +// elseif ($status == HOSTING_TASK_PROCESSING) { +// $state = 'pending'; +// } +// elseif ($status == HOSTING_TASK_SUCCESS || $status == HOSTING_TASK_WARNING) { +// $state = 'success'; +// } +// else { +// $state = 'error'; +// } +// +// $params = new stdClass(); +// $params->state = $state; +// $params->target_url = $url; +// $params->description = $description; +// $params->context = 'devshop/' . $task->task_type; +// +// $status = $client->getHttpClient()->post("/repos/$owner/$repo/statuses/$sha", json_encode($params)); +// +// drush_log('Status posted! ', 'ok'); +// } catch (Github\Exception\RuntimeException $e) { +// drush_log('GitHub API Error: ' . $e->getMessage(), 'error'); +// drush_log(l(t('Configure GitHub API'), 'admin/devshop/github'), 'error'); +// } catch (Github\Exception\ValidationFailedException $e) { +// drush_log('GitHub API Error: ' . $e->getMessage(), 'error'); +// } +// +// drush_log('done trying... ', 'ok'); +// +// } +// } + +/** + * + */ +function devshop_github_comment($task, $status) { + + $output = array(); + $output[] = '> **DEVSHOP**'; + $output[] = '> ' . ucfirst($task->task_type) . ": " . _hosting_parse_error_code($status); + $output[] = '> Site: ' . $task->ref->environment->url; + $output[] = '> Project: ' . url("node/{$task->ref->project->nid}", array('absolute' => TRUE)); + + if ($task->task_type == 'test') { + $output[] = 'Results: ' . url("node/{$task->nid}", array('absolute' => TRUE)); + } + + if ($task->task_type == 'import') { + $output[] = t('Your environment is now available.'); + } + + return implode("\n", $output); +} + + + +/** + * GitHub action to take on webhook init + */ +function devshop_github_webhook($project_node) { + $headers = getallheaders(); + $project = $project_node->project; + + // @TODO: Handle form content from github as well. + if ($headers['Content-Type'] == 'application/json' || $headers['content-type'] == 'application/json') { + $data = json_decode(file_get_contents('php://input')); + + $args = array(); + $args['cache'] = 1; + + if (isset($headers['X-Github-Event'])) { + $github_event = $headers['X-Github-Event']; + } + elseif (isset($headers['X-GitHub-Event'])) { + $github_event = $headers['X-GitHub-Event']; + } + else { + $github_event = ''; + } + + switch ($github_event) { + case 'ping': + $message = 'Pong!'; + break; + case 'push': + + // If push is for a deleted branch, don't do anything. + if ($data->deleted && $data->after == "0000000000000000000000000000000000000000") { + $message = 'Deleted ref detected.'; + break; + } + + // Limit "Deploy" tasks to only run for the branches we have new code for.. + $git_ref = strtr($data->ref, array('refs/tags/' => '', 'refs/heads/' => '')); + + // Check for environments set to pull + $environments_to_pull = array(); + $message = "Push Received for git ref $git_ref."; + + foreach ($project->environments as $environment_name => $environment) { + + // Only pull if deploy is not disabled or if environment is tracking a tag. + if ( +// @TODO: Improve this. If a site install failed, it shows up as "disabled", so we should still do a git pull. + $git_ref == $environment->git_ref && + !$environment->settings->pull_disabled && + !in_array($environment->git_ref, $project->settings->git['tags']) + ) { + $environments_to_pull[] = $environment->name; + + if (isset($environment->site) && $node = node_load($environment->site)) { + + // If project is set to reinstall pull request environments, and this is a pull request environment, run a forced install task. + if (isset($project->settings->github['pull_request_reinstall']) && $project->settings->github['pull_request_reinstall'] && isset($environment->github_pull_request)) { + + // Trigger deploy task to get latest code. + // Pass no args to skip DB updates, cache clearing, etc. + hosting_add_task($environment->site, 'devshop-deploy', array( + 'git_ref' => $environment->git_ref, + )); + + // Trigger "install" with force-reinstall + hosting_add_task($environment->site, 'install', array('force-reinstall' => 1)); + } + + // Otherwise, just run a "deploy" task. + else { + + // Default args to the environments deploy settings. + $args = $environment->settings->deploy; + $args['git_ref'] = $environment->git_ref; + hosting_add_task($environment->site, 'devshop-deploy', $args); + } + } + } + } + + if (empty($environments_to_pull)) { + $message .= "No environments tracking this branch. "; + } + else { + $message .= "Deploying code to environments: " . implode(', ', $environments_to_pull); + } + break; + + case 'pull_request': + // If pull request environments is enabled... + if ($project->settings->github['pull_request_environments']) { + $message = 'Pull Request Received.'; + + // @TODO: Handle forks? + $branch = $data->pull_request->head->ref; + + // Determine environment branch. + // @TODO: Make Configurable, allow branch names to be env name + $environment_name = "pr" . $data->pull_request->number; +// $environment_name = 'branch_' . str_replace('-', '_', $branch); + $already_have_pr_info = FALSE; + + // When PR is opened... create new environment. + if ($data->action == 'opened' || $data->action == 'reopened') { + $message = "Detected Pull Request creation for $branch \n"; + if (isset($project->environments[$environment_name])) { + $message = "Environment $environment_name already exists! Not creating one... \n"; + + // @TODO: Check for environments that are being deleted. + if (isset($project->environments[$environment_name]->github_pull_request)) { + $message .= "Already have a PR for $environment_name ... not inserting."; + $already_have_pr_info = TRUE; + } + } + else { + // If method is "install"... + if ($project->settings->github['pull_request_environments_method'] == 'devshop__github__install') { + hosting_create_environment($project, $environment_name, $branch); + $message .= "Environment $environment_name created for $project_node->title via installation profile.\n"; + } + // Otherwise, it is a clone from live. + elseif (isset($project->environments[$project->settings->github['pull_request_environments_method']] )) { + $source_env = $project->settings->github['pull_request_environments_method']; + hosting_create_environment($project, $environment_name, $branch, $source_env); + $message .= "Environment $environment_name created for $project_node->title via cloning $source_env \n"; + } + else { + $message .= 'Unable to determine what to do! Check "Pull Request Environment Method" setting.'; + } + } + + $project_node = node_load($project->nid); + $project = $project_node->project; + $environment = $project->environments[$environment_name]; + + $owner = $project->github_owner; + $repo = $project->github_repo; + $message .= "\n About to try to create a deployment for $owner/$repo... \n"; + + // Send a "deployment" to github. + try { + $token = variable_get('devshop_github_token', ''); + $client = new \Github\Client(); + $client->authenticate($token, Github\Client::AUTH_HTTP_TOKEN); + + $sha = $data->pull_request->head->sha; + + $params = new stdClass(); + $params->ref = $sha; + + // In GitHub's API, "environment" is just a small string it displays on the pull request: + $params->environment = $project->name . '.' . $environment_name; + devshop_environment_url($project, $environment_name) + ; + $params->required_contexts = array(); + + // $message .= "\nEnvironment: " . print_r($environment, 1); + $message .= "\nDeployment Params: " . print_r($params, 1); + + $post_url = "/repos/$owner/$repo/deployments"; + $deployment = json_decode($client->getHttpClient()->post($post_url, json_encode($params))->getBody(TRUE)); + + // Save deployment to pull request data for later access. + $data->pull_request->deployment = $deployment; + + $message .= " Deployment Created! \n"; + + // Create deployment status + $params = new stdClass(); + $params->state = 'pending'; + $params->target_url = $environment->url;; + $params->description = t('New environment is being created. Please stand by.'); + $deployment_status = $client->getHttpClient()->post("/repos/$owner/$repo/deployments/{$deployment->id}/statuses", json_encode($params)); + + $message .= " Deployment Status Created! \n"; + + // Set a commit status for this REF for devshop/deploy context + $sha = $data->pull_request->head->sha; + + $params = new stdClass(); + $params->state = 'pending'; + $params->target_url = url("node/$environment->site", array('absolute' => TRUE)); + $params->description = t('DevShop: Deploy'); + $params->context = "devshop/{$project->name}/deploy"; + + // Post status to github + $status = $client->getHttpClient()->post("/repos/$owner/$repo/statuses/$sha", json_encode($params)); + $message .= " Commit Status Created! \n"; + + // Determine if we are going to run tests. + // For now it is using the "live environment" setting. + // @TODO: Once we add "deploy hooks" to "Project Settings: Environment Defaults" we will have to change this. + +// // If live environment is set to run tests on deploy... +// $live_environment = $project->settings->live['live_environment']; +// if ($project->environments[$live_environment]->settings->deploy['test']) { +// $params = new stdClass(); +// $params->state = 'pending'; +// $params->target_url = url( +// "node/$environment->site", +// array('absolute' => true) +// ); +// $params->description = t('DevShop: Run Tests'); +// $params->context = 'devshop/tests'; +// +// // Post status to github +// $status = $client->getHttpClient()->post( +// "/repos/$owner/$repo/statuses/$sha", +// json_encode($params) +// ); +// $message .= " Commit Status for pending test run Created! \n"; +// } + + } catch (Github\Exception\RuntimeException $e) { + watchdog('devshop_github', 'GitHub Runtime Error: ' . $e->getMessage()); + $message .= 'GitHub RunTimeException during PR Create: ' . $e->getMessage() . $e->getCode(); + + if ($e->getCode() == '409') { + $message .= "\n Branch is out of date! alert the developer!"; + + // Send a failed commit status to alert to developer + $params = new stdClass(); + $params->state = 'failure'; + $params->target_url = $project->git_repo_url; + $params->description = t('Branch is out of date! Merge from default branch.'); + $params->context = "devshop/{$project->name}/merge"; + + // Post status to github + $status = $client->getHttpClient()->post("/repos/$owner/$repo/statuses/$sha", json_encode($params)); + } + else { + http_response_code(500); + } + + } catch (Github\Exception\ValidationFailedException $e) { + watchdog('devshop_github', 'GitHub Validation Failed Error: ' . $e->getMessage()); + $message .= 'GitHub ValidationFailedException Error: ' . $e->getMessage(); + } + + // Insert PR record + if (!$already_have_pr_info) { + $info = new stdClass(); + $info->id = $data->pull_request->id; + $info->number = $data->pull_request->number; + $info->project_nid = $project->nid; + $info->environment_name = $environment_name; + $info->pull_request_object = serialize($data->pull_request); + + // Last minute check for existing PR info + $results = db_select('hosting_devshop_github_pull_requests', 'pr') + ->condition('id', $data->pull_request->id) + ->fields('pr', array('id')) + ->execute() + ->fetchAssoc(); + + // If there are results, save $update param for drupal_write_record(). + if (empty($results)) { + $update = array(); + } + else { + $update = array('id'); + } + + // Save environment record. + try { + drupal_write_record('hosting_devshop_github_pull_requests', $info, $update); + $message .= ' ' . t('Pull Request info saved to DevShop.'); + } + catch (\PDOException $e) { + $message .= 'Saving PR record failed: ' . $e->getMessage(); + } + } + } + + // When PR is updated, send a new deployment status environment. + elseif ($data->action == 'synchronize') { + + // Create a new deployment + $owner = $project->github_owner; + $repo = $project->github_repo; + $environment = $project->environments[$environment_name]; + + // Environment might have been deleted. + if (empty($environment)) { + $message .= "Environment $environment_name not found in project $project->name."; + continue; + } + + $message .= "About to set deployment status for repo $owner/$repo using environment $environment_name \n"; + $message .= "Environment: " . print_r($environment, 1); + + try { + $token = variable_get('devshop_github_token', ''); + $client = new \Github\Client(); + $client->authenticate($token, Github\Client::AUTH_HTTP_TOKEN); + + $sha = $data->pull_request->head->sha; + + $params = new stdClass(); + $params->ref = $sha; + $params->environment = $environment->url; + $params->required_contexts = array(); + $post_url = "/repos/$owner/$repo/deployments"; + $deployment = json_decode($client->getHttpClient()->post($post_url, json_encode($params))->getBody(TRUE)); + + // Save deployment to pull request data for later access. + $data->pull_request->deployment = $deployment; + + $message .= " Deployment Created! \n"; + + // Create deployment status + $deployment_id = $deployment->id; + + $params = new stdClass(); + $params->state = 'pending'; + $params->target_url = $environment->url; + $params->description = t('Code is being deployed. Please stand by.'); + + $post_url = "/repos/$owner/$repo/deployments/{$deployment_id}/statuses"; + $message .= "Attempting to create deployment status: $post_url \n"; + + $deployment_status = $client->getHttpClient()->post($post_url, json_encode($params)); + + $message .= " Deployment Status Created! \n"; + + // Set a commit status for this REF for devshop/deploy context + $sha = $data->pull_request->head->sha; + + $params = new stdClass(); + $params->state = 'pending'; + $params->target_url = url("node/$project->nid", array('absolute' => TRUE)); + $params->description = t('DevShop: Deploy'); + $params->context = "devshop/{$project->name}/deploy"; + + // Post status to github + $status = $client->getHttpClient()->post("/repos/$owner/$repo/statuses/$sha", json_encode($params)); + + $message .= " Commit Status Created! \n"; + + // If environment is configured to run tests, add another status. + if (!empty($environment->settings->deploy['test'])) { + $params = new stdClass(); + $params->state = 'pending'; + + // @TODO: Add the link to the last "test" task here instead of the project. + $params->target_url = url("node/$project->nid", array('absolute' => TRUE)); + $params->description = t('DevShop: Run Tests'); + $params->context = "devshop/{$project->name}/tests"; + + // Post status to github + $status = $client->getHttpClient()->post("/repos/$owner/$repo/statuses/$sha", json_encode($params)); + + $message .= " Commit Status Created for test runs! \n"; + } + + } catch (Github\Exception\RuntimeException $e) { + $log = 'GitHub API Error during PR Syncronize: ' . $e->getMessage() . $e->getCode(); + watchdog('devshop_github', $log); + + if ($e->getCode() == '409') { + $log .= "\n Out of date! alert the developer!"; + + // Send a failed commit status to alert to developer + $params = new stdClass(); + $params->state = 'failure'; + $params->target_url = $project->git_repo_url; + $params->description = t('Branch is out of date! Merge from default branch.'); + $params->context = "devshop/{$project->name}/merge"; + + // Post status to github + $status = $client->getHttpClient()->post("/repos/$owner/$repo/statuses/$sha", json_encode($params)); + } + else { + http_response_code(500); + } + $message .= $log . "\n"; + } + + + // Update the PR record + $info = new stdClass(); + $info->id = $data->pull_request->id; + $info->number = $data->pull_request->number; + $info->project_nid = $project->nid; + $info->environment_name = $environment_name; + $info->pull_request_object = serialize($data->pull_request); + + // Save environment record. + $results = db_select('hosting_devshop_github_pull_requests', 'pr') + ->condition('id', $data->pull_request->id) + ->fields('pr', array('id')) + ->execute() + ->fetchAssoc(); + + // If there are results, save $update param for drupal_write_record(). + if (empty($results)) { + $update = array(); + } + else { + $update = array('id'); + } + + // Save environment record. + try { + drupal_write_record('hosting_devshop_github_pull_requests', $info, $update); + $message .= ' ' . t('Pull Request info saved to DevShop.'); + } + catch (\PDOException $e) { + $message .= 'Saving PR record failed: ' . $e->getMessage(); + } + } + // When PR is closed, delete environment. + elseif ($data->action == 'closed') { + $message .= "Pull Request Closed \n"; + if ($project->settings->github['pull_request_environments_delete']) { + + // If environment has a site... trigger it's deletion. + // Platform deletion triggers after site deletion completes. + if ($project->environments[$environment_name]->site) { + hosting_add_task($project->environments[$environment_name]->site, 'delete', array('force' => 1)); + $message .= "Environment $environment_name (Site Node: {$project->environments[$environment_name]->site}) scheduled for deletion."; + } + // If environment has a platform... trigger it's deletion. + elseif ($project->environments[$environment_name]->platform) { + hosting_add_task($project->environments[$environment_name]->platform, 'delete'); + $message .= "Environment $environment_name (Platform Node: {$project->environments[$environment_name]->platform}) scheduled for deletion."; + } + } + } + } + break; + } + + } + else { + $message = 'GitHub Request Received, but not in JSON. Please make sure to configure the webhook to use Payload version: application/vnd.github.v3+json'; + } + return $message; +} + +/** + * Check the GitHub account for an SSH key. + * + * @return bool + */ +function devshop_github_check_key($silent = false) { + + if (!devshop_public_key_valid_fingerprint()) { + return FALSE; + } + + $token = variable_get('devshop_github_token', ''); + + $client = new \Github\Client(); + $client->authenticate($token, Github\Client::AUTH_HTTP_TOKEN); + + try { + $keys = $client->currentUser()->keys()->all(); + } + catch (Exception $e) { + if (!$silent) { + drupal_set_message($e->getMessage(), 'error'); + } + return FALSE; + } + foreach ($keys as $key) { + $ssh_key = sshkey_parse($key['key']); + if (!empty($ssh_key['fingerprint']) && $ssh_key['fingerprint'] == devshop_public_key_valid_fingerprint()) { + return TRUE; + } + } + return FALSE; +} + +/** + * Check that we are getting a fingerprint at all first. + * @return bool|string + * @throws \SSHKeyParseException + */ +function devshop_public_key_valid_fingerprint() { + + $devshop_key = variable_get('devshop_public_key', ''); + $command = 'drush @hostmaster vset devshop_public_key "$(cat ~/.ssh/id_rsa.pub)" --yes'; + + if (empty($devshop_key)) { + drupal_set_message(t("DevShop does not know what the server's public SSH Key is. Run the command: !command", array( + '!command' => $command, + )), 'error'); + return FALSE; + } + + try { + $devshop_sshkey = sshkey_parse($devshop_key); + } + catch (Exception $e) { + drupal_set_message(t("The ssh key saved in DevShop was invalid. Run the command to re-import it: !command", array( + '!command' => $command, + )), 'error'); + return FALSE; + } + + return $devshop_sshkey['fingerprint']; +} diff --git a/modules/devshop/devshop_github/includes/add-key.inc b/modules/devshop/devshop_github/includes/add-key.inc new file mode 100644 index 000000000..89da4fdda --- /dev/null +++ b/modules/devshop/devshop_github/includes/add-key.inc @@ -0,0 +1,56 @@ + $devshop_key, + 'title' => t('Added by DevShop from !url', array( + '!url' => $_SERVER['HTTP_HOST'], + )), + ); + + // Create public key + $client = new \Github\Client(); + $client->authenticate($token, Github\Client::AUTH_HTTP_TOKEN); + try { + $return = $client->currentUser()->keys()->create($params); + } + catch (\Github\Exception\RuntimeException $e) { + drupal_set_message($e->getMessage(), 'error'); + drupal_set_message('Code ' . $e->getCode(), 'error'); + } + catch (\GitHub\Exception\ValidationFailedException $e) { + drupal_set_message($e->getMessage(), 'error'); + drupal_set_message('Code ' . $e->getCode(), 'error'); + } + + if ($return['verified']) { + drupal_set_message('SSH Key added to your account.'); + drupal_goto('admin/devshop/github'); + } else { + drupal_set_message('Something went wrong. SSH Key was not added to your account.', 'error'); + drupal_goto('admin/devshop/github'); + } +} diff --git a/modules/devshop/devshop_github/includes/admin.inc b/modules/devshop/devshop_github/includes/admin.inc new file mode 100644 index 000000000..28ef50126 --- /dev/null +++ b/modules/devshop/devshop_github/includes/admin.inc @@ -0,0 +1,289 @@ +'; + $form['#suffix'] = ''; + + $form['title'] = array( + '#markup' => t('GitHub Settings'), + '#prefix' => '

', + '#suffix' => '

', + ); + $form['description'] = array( + '#prefix' => '

', + '#markup' => $token_exists? + '': + t('There is no GitHub token stored in this DevShop. Follow the steps below.'), + '#suffix' => '

', + ); + $form['devshop_github_token_button'] = array( + '#type' => 'item', + '#prefix' => '
', + '#suffix' => '
', + '#title' => t('Step 1: Create a GitHub Token'), + '#markup' => '
' . l(t('Create a new token on GitHub.com'), 'https://github.com/settings/tokens/new?scopes=repo,admin:public_key,admin:repo_hook&description=' . $_SERVER['HTTP_HOST'], array('attributes' => array( + 'target' => '_blank', + 'class' => array('btn btn-info form-submit'), + ))) . '
', + '#description' => t('Click this button to take you to GitHub.com and pre-set the required scopes for a new Personal Access Token.'), + '#weight' => 1, + ); + + $form['devshop_github_token_step'] = array( + '#title' => t('Step 2: Enter your GitHub Token'), + '#type' => 'item', + '#weight' => 2, + ); + + $form['devshop_github_token_step']['message'] = array( + '#markup' => $token_exists? ' ' . t('The stored GitHub Token is valid.'): '', + '#prefix' => '
', + '#suffix' => '
', + '#access' => $token_exists + ); + $form['devshop_github_token'] = array( + '#type' => 'password', + '#description' => $token_exists? + t('If you wish to change it, enter a new one and press "Save Token".'): + t('Copy the token you created on GitHub.com and paste it here.'), + '#default_value' => variable_get('devshop_github_token', ''), + '#element_validate' => array( + 'devshop_github_settings_form_validate_token', + ), + '#attributes' => array( + 'placeholder' => $token_exists? + t('Token is saved.'): + t('Enter a new GitHub Token') + ), + '#weight' => 2, + ); + + $form['devshop_github_ssh_key'] = array( + '#title' => t('Step 3: Add the DevShop SSH Key to your account'), + '#type' => 'item', + '#weight' => 4, + ); + + $form['devshop_github_ssh_key']['ssh_key_status'] = array( + + ); + + if (!$token_exists) { + $form['devshop_github_ssh_key']['ssh_key_status'] ['#prefix'] = '
'; + $form['devshop_github_ssh_key']['ssh_key_status'] ['#suffix'] = '
'; + $form['devshop_github_ssh_key']['ssh_key_status'] ['#markup'] = t('You must enter a GitHub token before SSH access can be setup and confirmed.'); + } + elseif (!$ssh_key_exists) { + $form['devshop_github_ssh_key']['ssh_key_status'] ['#prefix'] = '
'; + $form['devshop_github_ssh_key']['ssh_key_status'] ['#suffix'] = '
'; + $form['devshop_github_ssh_key']['ssh_key_status'] ['#markup'] = t('DevShop does not know what this server\'s public SSH Key is. Run the following command on the server to import the aegir user\'s public key '); + } + elseif (devshop_github_check_key()) { + $form['devshop_github_ssh_key']['ssh_key_status'] ['#prefix'] = '
'; + $form['devshop_github_ssh_key']['ssh_key_status'] ['#suffix'] = '
'; + $form['devshop_github_ssh_key']['ssh_key_status'] ['#markup'] = t("This DevShop's public SSH Key is in your GitHub account."); + } + else { + $form['devshop_github_ssh_key']['ssh_key_status'] ['#markup'] = t("This DevShop's public SSH Key was not found in your GitHub account."); + $form['devshop_github_ssh_key']['ssh_key_status_buttons'] ['#markup'] .= '
' . l(t('Add public key to your GitHub account'), 'admin/devshop/github/add-key', array( + 'query' => array( + 'destination' => $_GET['q'], + ), + 'attributes' => array( + 'class' => 'btn btn-info' + ), + )); + $form['devshop_github_ssh_key']['ssh_key_status_buttons'] ['#markup'] .= l(t('Configure Public Key'), 'admin/devshop', array( + 'query' => array( + 'destination' => $_GET['q'], + ), + 'attributes' => array( + 'class' => 'btn btn-default' + ), + )) . '
'; + $form['devshop_github_ssh_key']['ssh_key_status'] ['#prefix'] = '
'; + $form['devshop_github_ssh_key']['ssh_key_status'] ['#suffix'] = '
'; + } + + $repos = variable_get('devshop_github_available_repositories', array()); + $count = count($repos); + $form['repos'] = array( + '#type' => 'container', + '#weight' => 5, + '#access' => !empty(variable_get('devshop_github_token')), + ); + + $form['repos']['message'] = array( + '#prefix' => "
", + '#suffix' => '
', + '#markup' => t('@count_string associated with this GitHub token.', array( + '@count_string' => format_plural($count, t('1 repository found'), t('@count repositories found')) + ))); + + $orgs = variable_get('devshop_github_available_organizations', array()); + $count = count($orgs); + $form['repos']['message_orgs'] = array( + '#prefix' => "
", + '#suffix' => '
', + '#markup' => t('@count_string with create repo privileges.', array( + '@count_string' => format_plural($count, t('1 GitHub organization'), t('@count GitHub organizations')) + ))); + $form['repos']['button'] = array( + '#markup' => l(t('Refresh GitHub Data'),'admin/devshop/github/load-repos', array('attributes' => array( + 'class' => array('btn btn-info'), + )))); + + $form = system_settings_form($form); + $form['actions']['submit']['#value'] = t('Save Token'); + $form['actions']['#weight'] = 3; + return $form; +} + +/** + * Element validation for License Key. Pings devshop.support + * + * @param $element + * @param $form_state + * @param $form + */ +function devshop_github_settings_form_validate_token($element, &$form_state, $form) { + + $token = $form_state['values']['devshop_github_token']; + $e = devshop_github_token_is_valid($token); + if (is_subclass_of($e, 'Exception')) { + form_error($element, $e->getMessage()); + } + else { + variable_set('devshop_github_token', $token); + devshop_github_refresh_repositories(); + } +} + +function devshop_github_token_is_valid($token = NULL) { + + if (empty($token)) { + $token = variable_get('devshop_github_token'); + } + + $client = new \Github\Client(); + $client->authenticate($token, Github\Client::AUTH_HTTP_TOKEN); + + try { + $show = $client->currentUser()->show(); + return TRUE; + } + // Happens when user has no public keys. + catch (Exception $e) { + return $e; + } +} + +/** + * Grab an authenticated GitHub client. + * + * @return \Exception|\Github\Client + */ +function devshop_github_client() { + + $client = new \Github\Client(); + $client->authenticate(variable_get('devshop_github_token'), Github\Client::AUTH_HTTP_TOKEN); + + try { + $show = $client->currentUser()->show(); + return $client; + } + catch (Exception $e) { + drupal_set_message(t('Error validating GitHub token: ') . $e->getMessage(), 'error'); + throw $e; + } +} + +/** + * Page callback for "get all repos" link. + */ +function devshop_github_get_repositories_page() { + $token = variable_get('devshop_github_token', ''); + if (empty($token)) { + drupal_set_message('GitHub API Token is not set.', 'error'); + drupal_goto('admin/devshop/github'); + return; + } + + devshop_github_refresh_repositories(); + + drupal_goto('admin/devshop/github'); + return; +} + +/** + * Get a list of all repos a user can access. + */ +function devshop_github_refresh_repositories() { + + try { + $client = devshop_github_client(); + $userApi = $client->currentUser(); + $orgsApi = $client->organizations(); + $paginator = new Github\ResultPager($client); + $params = array( + 'all' + ); + $repos = $paginator->fetchAll($userApi, 'repositories', $params); + + foreach ($repos as $repo) { + $available_repos[$repo['full_name']] = array( + 'url' => $repo['ssh_url'], + 'org' => $repo['owner']['login'], + ); + } + $count = count($repos); + $r = array( + '@count' => $count, + ); + + variable_set('devshop_github_available_repositories', $available_repos); + drupal_set_message(t('Found @count_string associated with that GitHub token.', array( + '@count_string' => format_plural($count, '1 repository', t('@count repositories', $r)), + ))); + + + ; + $params = array( + 'all' + ); + $orgs = $paginator->fetchAll($userApi, 'organizations', $params); + foreach ($orgs as $org) { + + // If membership role is admin, or organization '''' + $membership = $userApi->memberships()->organization($org['login']); + if ($membership['role'] == 'admin' || $orgsApi->show($org['login'])['members_can_create_repositories']) { + $organizations[$org['login']] = $org['login']; + } + } + $count = count($organizations); + $r = array( + '@count' => $count, + ); + + variable_set('devshop_github_available_organizations', $organizations); + drupal_set_message(t('Found @count_string with create repo privileges.', array( + '@count_string' => format_plural($count, '1 organization', t('@count organizations', $r)), + ))); + + + } + catch (\Exception $e) { + drupal_set_message($e->getMessage(), 'error'); + drupal_set_message('Code ' . $e->getCode(), 'error'); + } +} \ No newline at end of file diff --git a/modules/devshop/devshop_hosting.drush.inc b/modules/devshop/devshop_hosting.drush.inc new file mode 100644 index 000000000..ee5934cd7 --- /dev/null +++ b/modules/devshop/devshop_hosting.drush.inc @@ -0,0 +1,85 @@ + 'Trigger deletion of a platform. This is mainly used by the devshop upgrade task to delete the old hostmaster platform.', + 'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_SITE, + 'arguments' => array( + 'path' => 'The root path of the platform', + ), + ); + return $items; +} + +/** + * Implementation of hook_post_hosting_TASK_TYPE_task() + * for Verify tasks. + * + * Installs the "composer" plugin if needed, and then runs composer-manager install command. + */ +function devshop_hosting_post_hosting_verify_task($task, $data) { + if ($task->ref->type == 'site' && $task->ref->hosting_name == 'hostmaster') { + + // Import SSH key, + $stored_devshop_ssh_key = variable_get('devshop_public_key'); + if (empty(trim($stored_devshop_ssh_key))) { + $server_public_key = file_get_contents($_SERVER['HOME'] . '/.ssh/id_rsa.pub'); + if (!empty($server_public_key)) { + variable_set('devshop_public_key', $server_public_key); + drush_log(dt('Loaded public key into DevShop.'), 'p_log'); + } + } + + // Rebuild composer.json and run composer install + $working_dir = drupal_realpath(variable_get('composer_manager_file_dir', 'public://composer')); + provision_process('drush @hostmaster cc drush'); + provision_process('drush @hostmaster composer-json-rebuild -y'); + provision_process('composer update', $working_dir, t('Installing Devmaster dependencies')); + } +} + +/** + * Command function for platform-delete. + */ +function drush_devshop_hosting_platform_delete($platform_path = NULL) { + if (empty($platform_path)) { + return drush_set_error(DRUSH_APPLICATION_ERROR, 'You must specify a platform path.'); + } + drush_log('[DEVSHOP] Looking up platform at ' . $platform_path, 'ok'); + + $nid = db_query('SELECT nid FROM {hosting_platform} WHERE publish_path = :publish_path AND status = :status' , array( + ':publish_path' => $platform_path, + ':status' => HOSTING_PLATFORM_ENABLED, + ))->fetchField(); + + if (empty($nid)) { + return drush_set_error(DRUSH_APPLICATION_ERROR, 'No platform with that path was found.'); + } + else { + drush_log('[DEVSHOP] Found platform ' . $nid, 'ok'); + + // Look for existing enabled sites. + $site_count = db_select('hosting_site') + ->condition('platform', $nid) + ->condition('status', HOSTING_SITE_ENABLED) + ->fields('nid') + ->countQuery() + ->execute() + ->fetchField(); + + if (!empty($site_count)) { + return drush_set_error(DRUSH_APPLICATION_ERROR, "The platform at path $platform_path still has sites present. Please delete them first."); + } + + if (drush_confirm(dt("Schedule the platform at $platform_path for deletion?"))){ + return hosting_add_task($nid, 'delete'); + } + } +} diff --git a/modules/devshop/devshop_hosting.info b/modules/devshop/devshop_hosting.info new file mode 100644 index 000000000..bc94b4c4f --- /dev/null +++ b/modules/devshop/devshop_hosting.info @@ -0,0 +1,6 @@ +name = DevShop Hosting +description = Provides the streamlined DevShop user interface and experience. NOTE: This module alters some of of the UI of Aegir Hostmaster. +core = 7.x +package = DevShop + +dependencies[] = hosting diff --git a/modules/devshop/devshop_hosting.install b/modules/devshop/devshop_hosting.install new file mode 100644 index 000000000..ad151ee68 --- /dev/null +++ b/modules/devshop/devshop_hosting.install @@ -0,0 +1,289 @@ +devshop.')); + + db_update('system') + ->fields(array( + 'weight' => 11, + )) + ->condition('name', "devshop_hosting") + ->execute(); + + devshop_hosting_install_footer_menu(); +} + +/** + * Implements hook_requirements(). + * @param $phase + * @return array + */ +function devshop_hosting_requirements($phase) { + $requirements = array(); + + $devshop_version = file_get_contents(drupal_get_path('profile', 'devmaster') . '/VERSION.txt'); + $t = get_t(); + + // Report DevShop version + if ($phase == 'runtime') { + $requirements['devshop'] = array( + 'title' => $t('DevShop Version'), + 'value' => $devshop_version, + 'severity' => REQUIREMENT_OK, + ); + } + + return $requirements; +} + +/** + * Install the footer menu block and links. + */ +function devshop_hosting_install_footer_menu() { + $t = get_t(); + $menu = array( + 'menu_name' => 'devshop-footer-links', + 'title' => $t('DevShop Footer Links'), + ); + menu_save($menu); + + // Lookup hostmaster node. + $nid = db_select('hosting_context', 'hc') + ->fields('hc', ['nid']) + ->condition('name', 'hostmaster') + ->execute() + ->fetchField() + ; + + // Add links. + $links["admin/devshop"] = $t('DevShop Settings'); + $links["node/$nid"] = $t('Hostmaster Dashboard'); + $links['https://docs.opendevshop.com'] = $t('Documentation'); + $links['https://gitter.im/opendevshop/devshop'] = $t('Chat'); + $links['https://github.com/opendevshop/devshop/issues'] = $t('Issue Queue'); + $links['https://devshop.support'] = $t('Get DevShop.Support'); + + $number = 0; + foreach ($links as $url => $title) { + $menu_item = [ + 'link_title' => $title, + 'link_path' => $url, + 'menu_name' => 'devshop-footer-links', + 'description' => $title, + 'weight' => $number, + ]; + menu_link_save($menu_item); + $number++; + } + menu_rebuild(); + + // Add alter to block to add CSS or classes. +} + +/** + * Set weight of this module higher than views. + */ +function devshop_hosting_update_7001() { + $ret = array(); + $ret[] = update_sql('UPDATE {system} SET weight = 11 WHERE name = "devshop_hosting"'); + return $ret; +} + +/** + * Disable aegir's "Welcome" page. + */ +function devshop_hosting_update_7002() { + variable_set('hosting_welcome_page', 0); +} + +/** + * Enable devshop_github module. + */ +function devshop_hosting_update_7003() { + module_enable(array('devshop_hosting')); +} + +/** + * Enable devshop_dothooks module. + */ +function devshop_hosting_update_7004() { + module_enable(array('devshop_dothooks')); +} + +/** + * Save 'aegir_hostmaster_site_nid' variable for devshop. + */ +function devshop_hosting_update_7005() { + $nid = db_query('SELECT site.nid + FROM hosting_site site + JOIN hosting_package_instance pkgi + ON pkgi.rid = site.nid + JOIN hosting_package pkg + ON pkg.nid = pkgi.package_id + WHERE pkg.short_name = \'devmaster\'')->fetchField(); + variable_set('aegir_hostmaster_site_nid', $nid); +} + +/** + * Enable devshop_stats module. + * + * Removed now that we had a "successful failure". https://travis-ci.org/opendevshop/devmaster/jobs/189200584#L2682 + * +function devshop_hosting_update_7006() { + module_enable(array('devshop_stats')); +} + */ + +/** + * Disable and uninstall distro_update module. + */ +function devshop_hosting_update_7006() { + module_disable(array('distro_update')); + drupal_uninstall_modules(array('distro_update')); +} + +/** + * Set the "Force removal of deleted sites, platforms, and servers" setting. + */ +function devshop_hosting_update_7007() { + variable_set('hosting_delete_force', 1); +} + +/** + * Enable Aegir Download, Commit, Update, and Config modules. + */ +function devshop_hosting_update_7009() { + module_enable(array('aegir_download', 'aegir_commit', 'aegir_update', 'aegir_config', 'aegir_features')); +} + +/** + * Enable Hosting Git Tag module. + */ +function devshop_hosting_update_7010() { + module_enable(array('hosting_git_tag')); +} + +//drupal_set_installed_schema_version('devshop_hosting', 7010); + +/** + * Enable the footer menu. + */ +function devshop_hosting_update_7011() { + devshop_hosting_install_footer_menu(); +} + +/** + * Enable Devshop Remotes. + */ +function devshop_hosting_update_7012() { + module_enable(array('devshop_remotes')); +} + +/** + * Enable Hosting Git Checkout module for creating branches. + */ +function devshop_hosting_update_7013() { + module_enable(array('hosting_git_checkout')); +} + +/** + * Enable Aegir features module to allow one click feature updates and reverts. + */ +function devshop_hosting_update_7014() { + module_enable(array('aegir_features')); +} + +/** + * Remove aegir_commit and aegir_features from the system. + */ +function devshop_hosting_update_7015 (){ + $modules = array( + 'aegir_commit', + 'aegir_features', + ); + db_delete('system') + ->condition('name', $modules, 'IN') + ->condition('type', 'module') + ->execute(); +} + +/** + * Enable LetsEncrypt.org support for automatic free HTTPS certificates. + */ +function devshop_hosting_update_7016() { + module_enable(array('hosting_letsencrypt')); +} + +/** + * Enable DevShop.Support Client! + */ +function devshop_hosting_update_7100() { + module_enable(array('devshop_support_network_client')); +} + +/** + * Disable client requirement. + */ +function devshop_hosting_update_7101() { + variable_set('hosting_client_require_client_to_create_site', FALSE); +} + +/** + * Enable composer_manager module. + */ +function devshop_hosting_update_7102() { + module_enable(array('composer_manager')); +} + +/** + * Enable navbar, disable and uninstall admin_menu modules. + */ +function devshop_hosting_update_7103() { + module_disable(array('admin_menu')); + drupal_uninstall_modules(array('admin_menu', 'admin_menu_toolbar')); + module_enable(array('navbar')); +} + +/** + * Don't automatically import sites on platform verify. + */ +function devshop_hosting_update_7104() { + variable_set('hosting_platform_automatic_site_import', FALSE); +} + +/** + * Add a DevShop Settings link to the footer menu. + */ +function devshop_hosting_update_7105() { + $menu_item = array( + 'link_title' => t('DevShop Settings'), + 'link_path' => 'admin/devshop', + 'menu_name' => 'devshop-footer-links', + 'description' => t("Manage this DevShop's Settings"), + 'weight' => -1, + ); + menu_link_save($menu_item); +} + +/** + * Enable "chosen" module and make sure "chosen" widget allows "contains" string searching. + */ +function devshop_hosting_update_7106() { + module_enable(array('chosen')); + variable_set('chosen_search_contains', 1); + variable_set('chosen_jquery_selector', 'select:visible'); + variable_set('chosen_minimum_single', 0); + variable_set('chosen_minimum_multiple', 0); +} diff --git a/modules/devshop/devshop_hosting.module b/modules/devshop/devshop_hosting.module new file mode 100644 index 000000000..1395c80e6 --- /dev/null +++ b/modules/devshop/devshop_hosting.module @@ -0,0 +1,438 @@ +$welcome +

$note

+ + + $link +
+ $url +
+ +HTML; + + } +} + +/** + * Implements hook_init(). + * + * Adds CSS and sets the logo. + */ +function devshop_hosting_init() { + drupal_add_css(drupal_get_path('module', 'devshop_hosting') . '/devshop.css'); + $path = drupal_get_path('module', 'devshop_hosting') . '/icon.png'; + + drupal_add_html_head(''); + + +} + +/** + * Implements hook_menu(). + * + * Defines a new menu callback: login page for new users, redirect to projects + * for authenticated users. + */ +function devshop_hosting_menu() { + $items['devshop'] = array( + 'title' => 'Welcome to DevShop', + 'description' => '', + 'page callback' => 'devshop_hosting_home', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + $items['admin/reports/devshop_sites'] = array( + 'title' => 'DevShop Environments', + 'description' => "List all active sites and their IP addresses.", + 'page callback' => 'devshop_hosting_sites_status', + 'weight' => -60, + 'access arguments' => array('administer site configuration'), + ); + $items['devshop/login/reset/%'] = array( + 'title' => 'Reset Login', + 'description' => "List all active sites and their IP addresses.", + 'page callback' => 'devshop_hosting_login_reset', + 'page arguments' => array(3), + 'weight' => -60, + 'access arguments' => array('create login-reset task'), + ); + return $items; +} + +/** + * @param $blocks + * @param $theme + * @param $code_blocks + */ +function devshop_hosting_block_info_alter(&$blocks, $theme, $code_blocks) { + + // Enable the links block and put it in the footer.. + $blocks['menu']['devshop-footer-links']['status'] = 1; + $blocks['menu']['devshop-footer-links']['region'] = 'footer'; +} + +/** + * Menu callback for path "devshop" + * + * Provides a login page or redirects to proejcts + */ +function devshop_hosting_home() { + if (user_is_logged_in()) { + drupal_goto(variable_get('devshop_frontpage', 'projects')); + } + else { + drupal_goto(variable_get('devshop_frontpage_anonymous', 'user/login')); + } +} + +/** + * Implements hook_menu_alter(). + * + * Streamlines login and servers pages. + */ +function devshop_hosting_menu_alter(&$items) { + + // Commented out for the upgrade. Remove when 1.0.0 is released. +// $items['user/password']['type'] = MENU_CALLBACK; +// unset($items['hosting/sites']); +// unset($items['hosting/platforms']); +// unset($items['hosting/clients']); +// +// $items['hosting/servers/add'] = $items['node/add/server']; +// $items['hosting/servers/add']['title'] = t('Add new Server'); +// $items['hosting/servers/add']['type'] = MENU_LOCAL_TASK; +// $items['hosting/servers/add']['page arguments'] = array('server'); +// +// $items['hosting/servers/list'] = array( +// 'title' => t('All Servers'), +// 'weight' => -1, +// 'type' => MENU_DEFAULT_LOCAL_TASK, +// ); + + + $items['hosting/sites']['type'] = MENU_LOCAL_TASK; + $items['hosting/platforms']['type'] = MENU_LOCAL_TASK; + $items['hosting/clients']['type'] = MENU_LOCAL_TASK; + + $items['node/%node/goto_site']['page callback'] = 'devshop_hosting_site_goto'; + +} + +/** + * Override for hosting_site_goto(). + * @param $node + */ +function devshop_hosting_site_goto($node) { + $cid = "hosting:site:" . $node->nid . ":login_link"; + $cache = cache_get($cid); + if (user_access('create login-reset task') && !is_null($cache) && (REQUEST_TIME < $cache->data['expire'])) { + $theurl = $cache->data['link']; + cache_clear_all($cid, 'cache'); + } + else { + $theurl = devshop_hosting_site_url($node); + } + + drupal_goto($theurl); + exit(); +} + +/** + * Override for _hosting_site_url(). + * @param $node + * + * Allows us to check for project live domain alias. + */ +function devshop_hosting_site_url($node) { + $schema = 'http'; + $port = null; + + if (isset($node->project_nid)) { + $project = node_load($node->project_nid); + $url = "{$node->environment}.{$project->base_url}"; + } + else { + $url = strtolower(trim($node->title)); + } + + $platform = node_load($node->platform); + $server = node_load($platform->web_server); + + + if ($server->services['http']->has_port()) { + $port = $server->services['http']->port; + if ($port == 80) { + $port = null; + } + } + + /** + * This is part of the ssl feature, but is better to implement here. + */ + if (isset($node->ssl_enabled) && ($node->ssl_enabled == 2)) { + // this is a bit of a magic number, because we cant rely on the constant being available yet. + // 2 == Only SSL is enabled. + $schema = 'https'; + + if ($server->services['http']->has_port()) { + $port = $server->services['http']->ssl_port; + if ($port == 443) { + $port = null; + } + } + } + + if (is_numeric($port)) { + return "{$schema}://{$url}:{$port}"; + } + + return "{$schema}://{$url}"; +} + +/** + * Implements hook_form_alter() for user_login(). + * Provides some UI enhancements. + */ +function devshop_hosting_form_user_login_alter(&$form) { + $form['pass']['#description'] = l(t('Forgot your Password?'), 'user/password'); + + // Add user register link + if (user_register_access()) { + $form['submit']['#suffix'] = t('or') . ' ' . l(t('Create an Account'), 'user/register'); + } +} + +/** + * Implements hook_form_alter() for user_login_block(). + * Hides the login block form on "devshop" page. + */ +function devshop_hosting_form_user_login_block_alter(&$form) { + if (arg(0) == 'devshop') { + $form = array(); + } +} + +/** + * Page callback for devshop site status. + * @return string + */ +function devshop_hosting_sites_status() { + + $query = db_query("SELECT nid FROM {node} WHERE type = :type AND status = :status", array(':type' => 'project', ':status' => 1)); + while ($result = db_fetch_object($query)) { + $node = node_load($result->nid); + + foreach ($node->project->environments as $environment) { + if ($environment->site_status == HOSTING_SITE_ENABLED && count($environment->ip_addresses)) { + $ip = array_shift($environment->ip_addresses); + $items[] = "$ip $environment->uri"; + } + } + } + return theme('item_list', array('items' => $items, 'title' => t('Site Hosts'))); +} + +/** + * Special callback that waits for the login task to become available. + */ +function devshop_hosting_login_reset($site_nid) { + + $site = node_load($site_nid); + + // Set time limit to 3 minutes. + drupal_set_time_limit(180); + + // Create new reset-login task, if needed + $cache = cache_get("hosting:site:" . $site_nid . ":login_link"); + if (!($cache && (REQUEST_TIME < $cache->data['expire']))) { + $task = hosting_add_task($site_nid, 'login-reset'); + drupal_get_messages(); + } + else { + $tasks = hosting_get_tasks('rid', $site_nid, 'login-reset'); + } + + // Wait for it to complete + while ($task->task_status == HOSTING_TASK_QUEUED || $task->task_status == HOSTING_TASK_PROCESSING) { + sleep(1); + $cache = cache_get("hosting:site:" . $site_nid . ":login_link"); + if ($cache && (REQUEST_TIME < $cache->data['expire'])) { + break; + } + } + + // Output link to login. + $title = t('Log in'); + $close = t('Close'); + $login_text = t('Log in to !site', array('!site' => $site->environment->uri)); + $url = url("node/{$site_nid}/goto_site"); + + print << + + + + +HTML; + +} + +/** + * Replacement for hosting_get_tasks() that will load tasks for the platform and the site, as well as clone targets. + * @param $environment + * The environment object. + * + * @param null $type + * The task type you wish to retrieve. + * + * @param null $key + * + * If $key is specified, the return array will be re-ordered, with the key of + * the array being set to $task_node->$key. + * + * For example, if you want all tasks for an environment, sorted by date, use + * 'vid' for the $key parameter: + * + * $tasks = devshop_get_tasks($environment, NULL, 'vid'); + * + * @return array + * + * By default, an array is returned with Task types as the keys. Each element + * in the array is another array, keyed by VID. + * + */ +function devshop_get_tasks($environment, $type = NULL, $key = NULL, $limit = 20) { + + // Load tasks from hosting_task table for both site and platform. + $args = array(); + if ($environment->site) { + if ($type) { + $where = "(t.rid = :site OR t.rid = :platform) AND t.task_type = :type"; + $args = array( + ':site' => $environment->site, + ':platform' => $environment->platform, + ':type' => $type + ); + } + else { + $where = "(t.rid = :site OR t.rid = :platform)"; + $args = array( + ':site' => $environment->site, + ':platform' => $environment->platform, + ); + } + } + else { + if ($type) { + $where = "(t.rid = :platform AND t.task_type = :type)"; + $args = array( + ':platform' => $environment->platform, + ':type' => $type + ); + } + else { + $where = "(t.rid = :platform)"; + $args = array( + ':platform' => $environment->platform, + ); + } + } + + // Ensure limit is safe. + if (!is_int($limit) || $limit <= 0) { + $limit = 20; + } + $results = db_query(" + SELECT t.nid, t.task_type + FROM {node} tn + INNER JOIN {hosting_task} t ON tn.nid = t.nid + WHERE + $where + ORDER BY t.vid DESC + LIMIT $limit + ", + $args + ); + $tasks = array(); + foreach ($results as $result) { + $tasks[$result->task_type][$result->nid] = node_load($result->nid); + } + + // Load "Clone" tasks + if ($type == NULL || $type == 'clone') { + $results = db_query( + " + SELECT ta.nid + FROM {hosting_task_arguments} ta + INNER JOIN {hosting_task} t ON ta.nid = t.nid + WHERE + name = :name AND value = :platform + LIMIT 1 + ", + array( + ':platform' => $environment->platform, + ':name' => 'target_platform', + ) + ); + foreach ($results as $result) { + $task_node = node_load($result->nid); + $tasks[$task_node->task_type][$task_node->nid] = $task_node; + } + } + + // If key is requested... + if ($key) { + foreach ($tasks as $task_type => $task_list) { + foreach ($task_list as $task) { + $new_task_list[$task->{$key}] = $task; + } + } + if ($key == 'nid') { + krsort($new_task_list); + } + else { + ksort($new_task_list); + } + return $new_task_list; + } + else { + ksort($tasks); + return $tasks; + } +} diff --git a/modules/devshop/devshop_permissions/devshop_permissions.features.user_permission.inc b/modules/devshop/devshop_permissions/devshop_permissions.features.user_permission.inc new file mode 100644 index 000000000..67985b79f --- /dev/null +++ b/modules/devshop/devshop_permissions/devshop_permissions.features.user_permission.inc @@ -0,0 +1,1254 @@ + 'access content', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir account manager' => 'aegir account manager', + 'aegir administrator' => 'aegir administrator', + 'aegir client' => 'aegir client', + 'aegir platform manager' => 'aegir platform manager', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'node', + ); + + // Exported permission: 'access content overview'. + $permissions['access content overview'] = array( + 'name' => 'access content overview', + 'roles' => array( + 'administrator' => 'administrator', + ), + 'module' => 'node', + ); + + // Exported permission: 'access disabled sites'. + $permissions['access disabled sites'] = array( + 'name' => 'access disabled sites', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'anonymous user' => 'anonymous user', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting', + ); + + // Exported permission: 'access filemanager'. + $permissions['access filemanager'] = array( + 'name' => 'access filemanager', + 'roles' => array( + 'administrator' => 'administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_filemanager', + ); + + // Exported permission: 'access hosting logs'. + $permissions['access hosting logs'] = array( + 'name' => 'access hosting logs', + 'roles' => array( + 'administrator' => 'administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_logs', + ); + + // Exported permission: 'access hosting wizard'. + $permissions['access hosting wizard'] = array( + 'name' => 'access hosting wizard', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting', + ); + + // Exported permission: 'access task logs'. + $permissions['access task logs'] = array( + 'name' => 'access task logs', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'aegir client' => 'aegir client', + 'aegir platform manager' => 'aegir platform manager', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_task', + ); + + // Exported permission: 'access test results'. + $permissions['access test results'] = array( + 'name' => 'access test results', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'devshop_testing', + ); + + // Exported permission: 'add remote aliases to projects'. + $permissions['add remote aliases to projects'] = array( + 'name' => 'add remote aliases to projects', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'devshop_remotes', + ); + + // Exported permission: 'administer SSH public keys'. + $permissions['administer SSH public keys'] = array( + 'name' => 'administer SSH public keys', + 'roles' => array( + 'administrator' => 'administrator', + ), + 'module' => 'sshkey', + ); + + // Exported permission: 'administer clients'. + $permissions['administer clients'] = array( + 'name' => 'administer clients', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir account manager' => 'aegir account manager', + 'aegir administrator' => 'aegir administrator', + 'aegir platform manager' => 'aegir platform manager', + ), + 'module' => 'hosting_client', + ); + + // Exported permission: 'administer content types'. + $permissions['administer content types'] = array( + 'name' => 'administer content types', + 'roles' => array( + 'administrator' => 'administrator', + ), + 'module' => 'node', + ); + + // Exported permission: 'administer hosting'. + $permissions['administer hosting'] = array( + 'name' => 'administer hosting', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting', + ); + + // Exported permission: 'administer hosting aliases'. + $permissions['administer hosting aliases'] = array( + 'name' => 'administer hosting aliases', + 'roles' => array( + 'administrator' => 'administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_alias', + ); + + // Exported permission: 'administer hosting backup queue'. + $permissions['administer hosting backup queue'] = array( + 'name' => 'administer hosting backup queue', + 'roles' => array( + 'administrator' => 'administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_backup_queue', + ); + + // Exported permission: 'administer hosting features'. + $permissions['administer hosting features'] = array( + 'name' => 'administer hosting features', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting', + ); + + // Exported permission: 'administer hosting queues'. + $permissions['administer hosting queues'] = array( + 'name' => 'administer hosting queues', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting', + ); + + // Exported permission: 'administer hosting settings'. + $permissions['administer hosting settings'] = array( + 'name' => 'administer hosting settings', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting', + ); + + // Exported permission: 'administer hosting site backup manager'. + $permissions['administer hosting site backup manager'] = array( + 'name' => 'administer hosting site backup manager', + 'roles' => array( + 'administrator' => 'administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_site_backup_manager', + ); + + // Exported permission: 'administer nodes'. + $permissions['administer nodes'] = array( + 'name' => 'administer nodes', + 'roles' => array( + 'administrator' => 'administrator', + ), + 'module' => 'node', + ); + + // Exported permission: 'administer platforms'. + $permissions['administer platforms'] = array( + 'name' => 'administer platforms', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'aegir platform manager' => 'aegir platform manager', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_platform', + ); + + // Exported permission: 'administer projects'. + $permissions['administer projects'] = array( + 'name' => 'administer projects', + 'roles' => array( + 'administrator' => 'administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'devshop_projects', + ); + + // Exported permission: 'administer sites'. + $permissions['administer sites'] = array( + 'name' => 'administer sites', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'aegir platform manager' => 'aegir platform manager', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_site', + ); + + // Exported permission: 'administer tasks'. + $permissions['administer tasks'] = array( + 'name' => 'administer tasks', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'aegir platform manager' => 'aegir platform manager', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_task', + ); + + // Exported permission: 'bypass node access'. + $permissions['bypass node access'] = array( + 'name' => 'bypass node access', + 'roles' => array( + 'administrator' => 'administrator', + ), + 'module' => 'node', + ); + + // Exported permission: 'cancel own tasks'. + $permissions['cancel own tasks'] = array( + 'name' => 'cancel own tasks', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'aegir client' => 'aegir client', + 'aegir platform manager' => 'aegir platform manager', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_task', + ); + + // Exported permission: 'change site domain name'. + $permissions['change site domain name'] = array( + 'name' => 'change site domain name', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'devshop_projects', + ); + + // Exported permission: 'configure devshop pull'. + $permissions['configure devshop pull'] = array( + 'name' => 'configure devshop pull', + 'roles' => array( + 'administrator' => 'administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'devshop_pull', + ); + + // Exported permission: 'create backup schedules'. + $permissions['create backup schedules'] = array( + 'name' => 'create backup schedules', + 'roles' => array( + 'administrator' => 'administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_backup_queue', + ); + + // Exported permission: 'create backup task'. + $permissions['create backup task'] = array( + 'name' => 'create backup task', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'aegir client' => 'aegir client', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_task', + ); + + // Exported permission: 'create backup-delete task'. + $permissions['create backup-delete task'] = array( + 'name' => 'create backup-delete task', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_task', + ); + + // Exported permission: 'create client'. + $permissions['create client'] = array( + 'name' => 'create client', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir account manager' => 'aegir account manager', + 'aegir administrator' => 'aegir administrator', + ), + 'module' => 'hosting_client', + ); + + // Exported permission: 'create clone task'. + $permissions['create clone task'] = array( + 'name' => 'create clone task', + 'roles' => array( + 'administrator' => 'administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_clone', + ); + + // Exported permission: 'create config_export task'. + $permissions['create config_export task'] = array( + 'name' => 'create config_export task', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + ), + 'module' => 'aegir_config', + ); + + // Exported permission: 'create config_import task'. + $permissions['create config_import task'] = array( + 'name' => 'create config_import task', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + ), + 'module' => 'aegir_config', + ); + + // Exported permission: 'create delete task'. + $permissions['create delete task'] = array( + 'name' => 'create delete task', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'aegir client' => 'aegir client', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_task', + ); + + // Exported permission: 'create devshop-delete task'. + $permissions['create devshop-delete task'] = array( + 'name' => 'create devshop-delete task', + 'roles' => array( + 'administrator' => 'administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'devshop_projects', + ); + + // Exported permission: 'create devshop-deploy task'. + $permissions['create devshop-deploy task'] = array( + 'name' => 'create devshop-deploy task', + 'roles' => array( + 'administrator' => 'administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'devshop_projects', + ); + + // Exported permission: 'create disable task'. + $permissions['create disable task'] = array( + 'name' => 'create disable task', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'aegir client' => 'aegir client', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_task', + ); + + // Exported permission: 'create download task'. + $permissions['create download task'] = array( + 'name' => 'create download task', + 'roles' => array( + 'administrator' => 'administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'aegir_download', + ); + + // Exported permission: 'create enable task'. + $permissions['create enable task'] = array( + 'name' => 'create enable task', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'aegir client' => 'aegir client', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_task', + ); + + // Exported permission: 'create features_revert_all task'. + $permissions['create features_revert_all task'] = array( + 'name' => 'create features_revert_all task', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_tasks_extra', + ); + + // Exported permission: 'create features_update_all task'. + $permissions['create features_update_all task'] = array( + 'name' => 'create features_update_all task', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_tasks_extra', + ); + + // Exported permission: 'create flush_cache task'. + $permissions['create flush_cache task'] = array( + 'name' => 'create flush_cache task', + 'roles' => array( + 'administrator' => 'administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_tasks_extra', + ); + + // Exported permission: 'create flush_drush_cache task'. + $permissions['create flush_drush_cache task'] = array( + 'name' => 'create flush_drush_cache task', + 'roles' => array( + 'administrator' => 'administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_tasks_extra', + ); + + // Exported permission: 'create git-pull task'. + $permissions['create git-pull task'] = array( + 'name' => 'create git-pull task', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir account manager' => 'aegir account manager', + 'aegir administrator' => 'aegir administrator', + 'aegir client' => 'aegir client', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_git_pull', + ); + + // Exported permission: 'create lock task'. + $permissions['create lock task'] = array( + 'name' => 'create lock task', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'aegir platform manager' => 'aegir platform manager', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_task', + ); + + // Exported permission: 'create login-reset task'. + $permissions['create login-reset task'] = array( + 'name' => 'create login-reset task', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'aegir client' => 'aegir client', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_task', + ); + + // Exported permission: 'create migrate task'. + $permissions['create migrate task'] = array( + 'name' => 'create migrate task', + 'roles' => array( + 'administrator' => 'administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_migrate', + ); + + // Exported permission: 'create package'. + $permissions['create package'] = array( + 'name' => 'create package', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + ), + 'module' => 'hosting_package', + ); + + // Exported permission: 'create platform'. + $permissions['create platform'] = array( + 'name' => 'create platform', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'aegir platform manager' => 'aegir platform manager', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_platform', + ); + + // Exported permission: 'create platform git-checkout task'. + $permissions['create platform git-checkout task'] = array( + 'name' => 'create platform git-checkout task', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'aegir platform manager' => 'aegir platform manager', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_git_checkout', + ); + + // Exported permission: 'create platform git-commit task'. + $permissions['create platform git-commit task'] = array( + 'name' => 'create platform git-commit task', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'aegir platform manager' => 'aegir platform manager', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_git_commit', + ); + + // Exported permission: 'create platform git-pull task'. + $permissions['create platform git-pull task'] = array( + 'name' => 'create platform git-pull task', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'aegir platform manager' => 'aegir platform manager', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_git_pull', + ); + + // Exported permission: 'create platform git-tag task'. + $permissions['create platform git-tag task'] = array( + 'name' => 'create platform git-tag task', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'aegir platform manager' => 'aegir platform manager', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_git_tag', + ); + + // Exported permission: 'create project'. + $permissions['create project'] = array( + 'name' => 'create project', + 'roles' => array( + 'administrator' => 'administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'devshop_projects', + ); + + // Exported permission: 'create project content'. + $permissions['create project content'] = array( + 'name' => 'create project content', + 'roles' => array( + 'authenticated user' => 'authenticated user', + ), + 'module' => 'node', + ); + + // Exported permission: 'create rebuild_registry task'. + $permissions['create rebuild_registry task'] = array( + 'name' => 'create rebuild_registry task', + 'roles' => array( + 'administrator' => 'administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_tasks_extra', + ); + + // Exported permission: 'create restore task'. + $permissions['create restore task'] = array( + 'name' => 'create restore task', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'aegir client' => 'aegir client', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_task', + ); + + // Exported permission: 'create run_cron task'. + $permissions['create run_cron task'] = array( + 'name' => 'create run_cron task', + 'roles' => array( + 'administrator' => 'administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_tasks_extra', + ); + + // Exported permission: 'create server'. + $permissions['create server'] = array( + 'name' => 'create server', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + ), + 'module' => 'hosting_server', + ); + + // Exported permission: 'create site'. + $permissions['create site'] = array( + 'name' => 'create site', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'aegir client' => 'aegir client', + 'aegir platform manager' => 'aegir platform manager', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_site', + ); + + // Exported permission: 'create site aliases'. + $permissions['create site aliases'] = array( + 'name' => 'create site aliases', + 'roles' => array( + 'administrator' => 'administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_alias', + ); + + // Exported permission: 'create site git-checkout task'. + $permissions['create site git-checkout task'] = array( + 'name' => 'create site git-checkout task', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir account manager' => 'aegir account manager', + 'aegir administrator' => 'aegir administrator', + 'aegir client' => 'aegir client', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_git_checkout', + ); + + // Exported permission: 'create site git-commit task'. + $permissions['create site git-commit task'] = array( + 'name' => 'create site git-commit task', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir account manager' => 'aegir account manager', + 'aegir administrator' => 'aegir administrator', + 'aegir client' => 'aegir client', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_git_commit', + ); + + // Exported permission: 'create site git-tag task'. + $permissions['create site git-tag task'] = array( + 'name' => 'create site git-tag task', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir account manager' => 'aegir account manager', + 'aegir administrator' => 'aegir administrator', + 'aegir client' => 'aegir client', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_git_tag', + ); + + // Exported permission: 'create sites on locked platforms'. + $permissions['create sites on locked platforms'] = array( + 'name' => 'create sites on locked platforms', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'aegir platform manager' => 'aegir platform manager', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_platform', + ); + + // Exported permission: 'create sync task'. + $permissions['create sync task'] = array( + 'name' => 'create sync task', + 'roles' => array( + 'administrator' => 'administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_sync', + ); + + // Exported permission: 'create test task'. + $permissions['create test task'] = array( + 'name' => 'create test task', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'devshop_testing', + ); + + // Exported permission: 'create unlock task'. + $permissions['create unlock task'] = array( + 'name' => 'create unlock task', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'aegir platform manager' => 'aegir platform manager', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_task', + ); + + // Exported permission: 'create update task'. + $permissions['create update task'] = array( + 'name' => 'create update task', + 'roles' => array( + 'administrator' => 'administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_tasks_extra', + ); + + // Exported permission: 'create update_drupal task'. + $permissions['create update_drupal task'] = array( + 'name' => 'create update_drupal task', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + ), + 'module' => 'aegir_update', + ); + + // Exported permission: 'create update_translations task'. + $permissions['create update_translations task'] = array( + 'name' => 'create update_translations task', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_tasks_extra', + ); + + // Exported permission: 'create verify task'. + $permissions['create verify task'] = array( + 'name' => 'create verify task', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'aegir client' => 'aegir client', + 'aegir platform manager' => 'aegir platform manager', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_task', + ); + + // Exported permission: 'delete any project content'. + $permissions['delete any project content'] = array( + 'name' => 'delete any project content', + 'roles' => array(), + 'module' => 'node', + ); + + // Exported permission: 'delete own client'. + $permissions['delete own client'] = array( + 'name' => 'delete own client', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + ), + 'module' => 'hosting_client', + ); + + // Exported permission: 'delete own project content'. + $permissions['delete own project content'] = array( + 'name' => 'delete own project content', + 'roles' => array(), + 'module' => 'node', + ); + + // Exported permission: 'delete package'. + $permissions['delete package'] = array( + 'name' => 'delete package', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + ), + 'module' => 'hosting_package', + ); + + // Exported permission: 'delete platform'. + $permissions['delete platform'] = array( + 'name' => 'delete platform', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'aegir platform manager' => 'aegir platform manager', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_platform', + ); + + // Exported permission: 'delete projects'. + $permissions['delete projects'] = array( + 'name' => 'delete projects', + 'roles' => array( + 'administrator' => 'administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'devshop_projects', + ); + + // Exported permission: 'delete revisions'. + $permissions['delete revisions'] = array( + 'name' => 'delete revisions', + 'roles' => array( + 'administrator' => 'administrator', + ), + 'module' => 'node', + ); + + // Exported permission: 'delete server'. + $permissions['delete server'] = array( + 'name' => 'delete server', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + ), + 'module' => 'hosting_server', + ); + + // Exported permission: 'delete site'. + $permissions['delete site'] = array( + 'name' => 'delete site', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'aegir client' => 'aegir client', + 'aegir platform manager' => 'aegir platform manager', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_site', + ); + + // Exported permission: 'edit any project content'. + $permissions['edit any project content'] = array( + 'name' => 'edit any project content', + 'roles' => array(), + 'module' => 'node', + ); + + // Exported permission: 'edit client uname'. + $permissions['edit client uname'] = array( + 'name' => 'edit client uname', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir account manager' => 'aegir account manager', + 'aegir administrator' => 'aegir administrator', + ), + 'module' => 'hosting_client', + ); + + // Exported permission: 'edit client users'. + $permissions['edit client users'] = array( + 'name' => 'edit client users', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir account manager' => 'aegir account manager', + 'aegir administrator' => 'aegir administrator', + ), + 'module' => 'hosting_client', + ); + + // Exported permission: 'edit own client'. + $permissions['edit own client'] = array( + 'name' => 'edit own client', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'aegir client' => 'aegir client', + ), + 'module' => 'hosting_client', + ); + + // Exported permission: 'edit own project content'. + $permissions['edit own project content'] = array( + 'name' => 'edit own project content', + 'roles' => array( + 'authenticated user' => 'authenticated user', + ), + 'module' => 'node', + ); + + // Exported permission: 'edit package'. + $permissions['edit package'] = array( + 'name' => 'edit package', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + ), + 'module' => 'hosting_package', + ); + + // Exported permission: 'edit platform'. + $permissions['edit platform'] = array( + 'name' => 'edit platform', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'aegir platform manager' => 'aegir platform manager', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_platform', + ); + + // Exported permission: 'edit project'. + $permissions['edit project'] = array( + 'name' => 'edit project', + 'roles' => array( + 'administrator' => 'administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'devshop_projects', + ); + + // Exported permission: 'edit server'. + $permissions['edit server'] = array( + 'name' => 'edit server', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + ), + 'module' => 'hosting_server', + ); + + // Exported permission: 'edit site'. + $permissions['edit site'] = array( + 'name' => 'edit site', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'aegir client' => 'aegir client', + 'aegir platform manager' => 'aegir platform manager', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_site', + ); + + // Exported permission: 'manage any SSH public keys'. + $permissions['manage any SSH public keys'] = array( + 'name' => 'manage any SSH public keys', + 'roles' => array( + 'administrator' => 'administrator', + ), + 'module' => 'sshkey', + ); + + // Exported permission: 'manage own SSH public keys'. + $permissions['manage own SSH public keys'] = array( + 'name' => 'manage own SSH public keys', + 'roles' => array( + 'administrator' => 'administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'sshkey', + ); + + // Exported permission: 'manage site HTTPS settings'. + $permissions['manage site HTTPS settings'] = array( + 'name' => 'manage site HTTPS settings', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'aegir client' => 'aegir client', + ), + 'module' => 'hosting_https', + ); + + // Exported permission: 'remove remote aliases from projects'. + $permissions['remove remote aliases from projects'] = array( + 'name' => 'remove remote aliases from projects', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'devshop_remotes', + ); + + // Exported permission: 'retry failed tasks'. + $permissions['retry failed tasks'] = array( + 'name' => 'retry failed tasks', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'aegir platform manager' => 'aegir platform manager', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_task', + ); + + // Exported permission: 'revert revisions'. + $permissions['revert revisions'] = array( + 'name' => 'revert revisions', + 'roles' => array( + 'administrator' => 'administrator', + ), + 'module' => 'node', + ); + + // Exported permission: 'update status of tasks'. + $permissions['update status of tasks'] = array( + 'name' => 'update status of tasks', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_task', + ); + + // Exported permission: 'view any SSH public keys'. + $permissions['view any SSH public keys'] = array( + 'name' => 'view any SSH public keys', + 'roles' => array( + 'administrator' => 'administrator', + ), + 'module' => 'sshkey', + ); + + // Exported permission: 'view client'. + $permissions['view client'] = array( + 'name' => 'view client', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir account manager' => 'aegir account manager', + 'aegir administrator' => 'aegir administrator', + 'aegir client' => 'aegir client', + ), + 'module' => 'hosting_client', + ); + + // Exported permission: 'view locked platforms'. + $permissions['view locked platforms'] = array( + 'name' => 'view locked platforms', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'aegir platform manager' => 'aegir platform manager', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_platform', + ); + + // Exported permission: 'view own SSH public keys'. + $permissions['view own SSH public keys'] = array( + 'name' => 'view own SSH public keys', + 'roles' => array( + 'administrator' => 'administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'sshkey', + ); + + // Exported permission: 'view own tasks'. + $permissions['view own tasks'] = array( + 'name' => 'view own tasks', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'aegir client' => 'aegir client', + 'aegir platform manager' => 'aegir platform manager', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_task', + ); + + // Exported permission: 'view own unpublished content'. + $permissions['view own unpublished content'] = array( + 'name' => 'view own unpublished content', + 'roles' => array( + 'administrator' => 'administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'node', + ); + + // Exported permission: 'view package'. + $permissions['view package'] = array( + 'name' => 'view package', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'aegir client' => 'aegir client', + 'aegir platform manager' => 'aegir platform manager', + ), + 'module' => 'hosting_package', + ); + + // Exported permission: 'view platform'. + $permissions['view platform'] = array( + 'name' => 'view platform', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'aegir client' => 'aegir client', + 'aegir platform manager' => 'aegir platform manager', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_platform', + ); + + // Exported permission: 'view project'. + $permissions['view project'] = array( + 'name' => 'view project', + 'roles' => array( + 'administrator' => 'administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'devshop_projects', + ); + + // Exported permission: 'view projects'. + $permissions['view projects'] = array( + 'name' => 'view projects', + 'roles' => array( + 'administrator' => 'administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'devshop_projects', + ); + + // Exported permission: 'view revisions'. + $permissions['view revisions'] = array( + 'name' => 'view revisions', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'aegir client' => 'aegir client', + 'aegir platform manager' => 'aegir platform manager', + ), + 'module' => 'node', + ); + + // Exported permission: 'view server'. + $permissions['view server'] = array( + 'name' => 'view server', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_server', + ); + + // Exported permission: 'view site'. + $permissions['view site'] = array( + 'name' => 'view site', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir account manager' => 'aegir account manager', + 'aegir administrator' => 'aegir administrator', + 'aegir client' => 'aegir client', + 'aegir platform manager' => 'aegir platform manager', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_site', + ); + + // Exported permission: 'view task'. + $permissions['view task'] = array( + 'name' => 'view task', + 'roles' => array( + 'administrator' => 'administrator', + 'aegir administrator' => 'aegir administrator', + 'aegir client' => 'aegir client', + 'aegir platform manager' => 'aegir platform manager', + 'authenticated user' => 'authenticated user', + ), + 'module' => 'hosting_task', + ); + + return $permissions; +} diff --git a/modules/devshop/devshop_permissions/devshop_permissions.info b/modules/devshop/devshop_permissions/devshop_permissions.info new file mode 100644 index 000000000..a3c0aba29 --- /dev/null +++ b/modules/devshop/devshop_permissions/devshop_permissions.info @@ -0,0 +1,150 @@ +name = DevShop Permissions +description = Default user permissions for devshop. +core = 7.x +package = Features +dependencies[] = aegir_config +dependencies[] = aegir_download +dependencies[] = aegir_update +dependencies[] = devshop_projects +dependencies[] = devshop_pull +dependencies[] = devshop_remotes +dependencies[] = devshop_testing +dependencies[] = features +dependencies[] = hosting +dependencies[] = hosting_alias +dependencies[] = hosting_backup_queue +dependencies[] = hosting_client +dependencies[] = hosting_clone +dependencies[] = hosting_filemanager +dependencies[] = hosting_git_checkout +dependencies[] = hosting_git_commit +dependencies[] = hosting_git_pull +dependencies[] = hosting_git_tag +dependencies[] = hosting_https +dependencies[] = hosting_logs +dependencies[] = hosting_migrate +dependencies[] = hosting_package +dependencies[] = hosting_platform +dependencies[] = hosting_server +dependencies[] = hosting_site +dependencies[] = hosting_site_backup_manager +dependencies[] = hosting_sync +dependencies[] = hosting_task +dependencies[] = hosting_tasks_extra +dependencies[] = node +dependencies[] = sshkey +features[features_api][] = api:2 +features[user_permission][] = access content +features[user_permission][] = access content overview +features[user_permission][] = access disabled sites +features[user_permission][] = access filemanager +features[user_permission][] = access hosting logs +features[user_permission][] = access hosting wizard +features[user_permission][] = access task logs +features[user_permission][] = access test results +features[user_permission][] = add remote aliases to projects +features[user_permission][] = administer SSH public keys +features[user_permission][] = administer clients +features[user_permission][] = administer content types +features[user_permission][] = administer hosting +features[user_permission][] = administer hosting aliases +features[user_permission][] = administer hosting backup queue +features[user_permission][] = administer hosting features +features[user_permission][] = administer hosting queues +features[user_permission][] = administer hosting settings +features[user_permission][] = administer hosting site backup manager +features[user_permission][] = administer nodes +features[user_permission][] = administer platforms +features[user_permission][] = administer projects +features[user_permission][] = administer sites +features[user_permission][] = administer tasks +features[user_permission][] = bypass node access +features[user_permission][] = cancel own tasks +features[user_permission][] = change site domain name +features[user_permission][] = configure devshop pull +features[user_permission][] = create backup schedules +features[user_permission][] = create backup task +features[user_permission][] = create backup-delete task +features[user_permission][] = create client +features[user_permission][] = create clone task +features[user_permission][] = create config_export task +features[user_permission][] = create config_import task +features[user_permission][] = create delete task +features[user_permission][] = create devshop-delete task +features[user_permission][] = create devshop-deploy task +features[user_permission][] = create disable task +features[user_permission][] = create download task +features[user_permission][] = create enable task +features[user_permission][] = create features_revert_all task +features[user_permission][] = create features_update_all task +features[user_permission][] = create flush_cache task +features[user_permission][] = create flush_drush_cache task +features[user_permission][] = create git-pull task +features[user_permission][] = create lock task +features[user_permission][] = create login-reset task +features[user_permission][] = create migrate task +features[user_permission][] = create package +features[user_permission][] = create platform +features[user_permission][] = create platform git-checkout task +features[user_permission][] = create platform git-commit task +features[user_permission][] = create platform git-pull task +features[user_permission][] = create platform git-tag task +features[user_permission][] = create project +features[user_permission][] = create project content +features[user_permission][] = create rebuild_registry task +features[user_permission][] = create restore task +features[user_permission][] = create run_cron task +features[user_permission][] = create server +features[user_permission][] = create site +features[user_permission][] = create site aliases +features[user_permission][] = create site git-checkout task +features[user_permission][] = create site git-commit task +features[user_permission][] = create site git-tag task +features[user_permission][] = create sites on locked platforms +features[user_permission][] = create sync task +features[user_permission][] = create test task +features[user_permission][] = create unlock task +features[user_permission][] = create update task +features[user_permission][] = create update_drupal task +features[user_permission][] = create update_translations task +features[user_permission][] = create verify task +features[user_permission][] = delete any project content +features[user_permission][] = delete own client +features[user_permission][] = delete own project content +features[user_permission][] = delete package +features[user_permission][] = delete platform +features[user_permission][] = delete projects +features[user_permission][] = delete revisions +features[user_permission][] = delete server +features[user_permission][] = delete site +features[user_permission][] = edit any project content +features[user_permission][] = edit client uname +features[user_permission][] = edit client users +features[user_permission][] = edit own client +features[user_permission][] = edit own project content +features[user_permission][] = edit package +features[user_permission][] = edit platform +features[user_permission][] = edit project +features[user_permission][] = edit server +features[user_permission][] = edit site +features[user_permission][] = manage any SSH public keys +features[user_permission][] = manage own SSH public keys +features[user_permission][] = manage site HTTPS settings +features[user_permission][] = remove remote aliases from projects +features[user_permission][] = retry failed tasks +features[user_permission][] = revert revisions +features[user_permission][] = update status of tasks +features[user_permission][] = view any SSH public keys +features[user_permission][] = view client +features[user_permission][] = view locked platforms +features[user_permission][] = view own SSH public keys +features[user_permission][] = view own tasks +features[user_permission][] = view own unpublished content +features[user_permission][] = view package +features[user_permission][] = view platform +features[user_permission][] = view project +features[user_permission][] = view projects +features[user_permission][] = view revisions +features[user_permission][] = view server +features[user_permission][] = view site +features[user_permission][] = view task diff --git a/modules/devshop/devshop_permissions/devshop_permissions.module b/modules/devshop/devshop_permissions/devshop_permissions.module new file mode 100644 index 000000000..5f0757208 --- /dev/null +++ b/modules/devshop/devshop_permissions/devshop_permissions.module @@ -0,0 +1,3 @@ +tagline = t('Hosted by DevShop'); +} \ No newline at end of file diff --git a/modules/devshop/devshop_projects/devshop_projects.drush.inc b/modules/devshop/devshop_projects/devshop_projects.drush.inc new file mode 100644 index 000000000..bcb2af4fd --- /dev/null +++ b/modules/devshop/devshop_projects/devshop_projects.drush.inc @@ -0,0 +1,218 @@ +ref; + if (($ref->type == 'site' || $ref->type == 'platform') && isset($ref->environment)) { + $ref->environment->last_task = $task->nid; + devshop_environment_save_last_task($ref->environment); + } +} + +/** + * Implements drush_HOOK_pre_COMMAND() + * + * This runs for each task during the command + * drush @hostmaster hosting-tasks + * + * NOTE: This ONLY runs when being called from a hostmaster task. + * This hook should ONLY be used to pass options from a hostmaster task form to + * the $task object, or if you don't need this functionality from the command + * line. + */ +function drush_devshop_projects_pre_hosting_task() +{ + + $task =& drush_get_context('HOSTING_TASK'); + + // For all tasks + if ($task->ref->type == 'site' || $task->ref->type == 'platform' || $task->ref->type == 'project') { + $drush_aliases = new Provision_Config_ProjectAliases($task->ref->project->name, $task->ref->project); + } + + // Only write drush aliases if project name was found. + if (!empty($drush_aliases->data['name'])) { + $drush_aliases->write(); + drush_log(dt('Drush aliases written for %name project.', [ + '%name' => $drush_aliases->data['name'], + ]), 'p_log'); + } + + + // Passing options for Download task. This should go in aegir_download.drush.inc but I can't get that hook working right now. + if ($task->task_type == 'download') { + drush_log('[AEGIR DEVSHOP_PROJECTS] Download package enabled...', 'ok'); + + $task->options['packages'] = $task->task_args['packages']; + $task->options['commit'] = $task->task_args['update']; + $task->options['message'] = $task->task_args['message']; + $task->options['update'] = $task->task_args['update']; + } + + // Verify Platform + // Here is where we hook in and clone the site + // @TODO: This can be removed once hosting_git migration is complete. +// if ($task->ref->type == 'platform' && $task->task_type == 'verify' && !empty($task->ref->project->git_url)) { +// drush_devshop_platform_verify(); +// } + + // Pull + if ($task->ref->type == 'project' && $task->task_type == 'devshop-pull') { + $task->args['environments'] = $task->task_args['environments']; + $task->options['update'] = $task->task_args['update']; + $task->options['revert'] = !empty($task->task_args['revert']); + $task->options['cache'] = $task->task_args['cache']; + $task->options['force'] = FALSE; + } + + // Deploy + if ($task->ref->type == 'site' && $task->task_type == 'devshop-deploy') { + + // The git_ref is the only argument + $task->args['git_ref'] = $task->task_args['git_ref']; + + // Just load in all of the task_args as options(they come from the task form.) + foreach ($task->task_args as $arg => $value) { + $task->options[$arg] = $value; + } + } + + // Run tests + if ($task->ref->type == 'site' && $task->task_type == 'test') { + + $task->options['tests-to-run'] = $task->task_args['tests_to_run']; + $task->options['test-type'] = $task->task_args['test_type']; + } + + // On every task triggered by devshop, set this option. + $task->options['is-devshop'] = TRUE; +} + +/** + * Implementation of hook_post_hosting_TASK_TYPE_task() + * for Verify tasks. + * + * Provides the "Fork environment" feature: Checks out a new branch and pushes it. + */ +function devshop_projects_post_hosting_verify_task($task, $data) { + if ($task->ref->type == 'platform') { + + // Fork Environment: + // If this is the first verification, and branch needs to be created (for "fork environment") + $platform = $task->ref; + if (!$platform->verified && !empty($platform->environment->settings->branch_to_fork)) { + provision_process("git checkout -b {$platform->environment->git_ref}", $platform->repo_path, dt('Creating a new branch.')); + + // Push the branch + provision_process("git push -u origin {$platform->environment->git_ref}", $platform->repo_path, dt('Pushing new branch.')); + } + } +} + +/** + * Implements hook_post_hosting_TASK_TYPE_task() for delete tasks(). + */ +function devshop_projects_post_hosting_delete_task($task, $data) { + + // When a project is deleted... + if ($task->ref->type == 'project') { + // Queue site deletion for each environment. + $project = $task->ref->project; + foreach ($project->environments as $environment) { + // If site exists, trigger deletion. + if ($environment->site) { + hosting_add_task($environment->site, 'delete'); + } + // If platform exists, trigger deletion. + elseif ($environment->platform) { + hosting_add_task($environment->platform, 'delete', array('force' => 1)); + } + } + + // @TODO: Should probably add our own status column + // Unpublish the project node. + $task->ref->status = 0; + $task->ref->no_verify = TRUE; + node_save($task->ref); + } + + // When a site is deleted (if it is in a project, delete the platform it is on. + // @TODO: Check for another site on this platform? + if ($task->ref->type == 'site' && !empty($task->ref->project)) { + // We trigger platform deletion here. + hosting_add_task($task->ref->platform, 'delete'); + } + + // When a platform is deleted (if it is in a project), delete the environment record. + if ($task->ref->type == 'platform' && !empty($task->ref->project)) { + db_delete('hosting_devshop_project_environment') + ->condition('project_nid', $task->ref->project->nid) + ->condition('platform', $task->ref->platform) + ->execute(); + + // If drupal root is not repo root, delete folder at code_path. + $repo_path = str_replace($task->ref->project->drupal_path, '', $task->ref->publish_path); + if (file_exists($repo_path) ) { + _provision_recursive_delete($repo_path); + } + } + + // When a platform is deleted, if it is the last in the project, + // and the project has been unpublished, delete the directory. + if ($task->ref->type == 'platform' && !empty($task->ref->project)) { + if ($task->ref->project->status == 0 && count($task->ref->project->environments) == 1) { + // Delete the project folder. + exec("rm -rf {$project->code_path}"); + } + } +} + +/** + * Implements hook_post_hosting_TASK_TYPE_task() for install. + */ +function devshop_projects_post_hosting_install_task($task, $data) { + + // Queue up a verification of the site. + hosting_add_task($task->ref->nid, 'verify'); + +} + + /** + * Implements hook_post_hosting_TASK_TYPE_task() for devshop deploy tasks(). + */ +function devshop_projects_post_hosting_devshop_deploy_task($task, $data) { + + // Save the deployed git ref to the environment record. + // Doing this post deploy ensures it was actually checked out. + drush_log('[DEVSHOP] Environment Deployed. Saving new git ref to project settings.'); + + $git_ref = trim(str_replace('refs/heads/', '', shell_exec("cd {$task->ref->environment->repo_path}; git describe --tags --exact-match || git symbolic-ref -q HEAD")));; + + $args = array( + ':git' => $git_ref, + ':site' => $task->ref->environment->site, + ); + + if (db_query('UPDATE {hosting_git} SET git_ref = :git WHERE nid = :site', $args)) { + drush_log("[DEVSHOP] Git reference {$git_ref} saved to {$task->ref->environment->name} environment record."); + } + else { + return drush_set_error('[DEVSHOP] Environment update failed! Unable to save git ref to hosting_devshop_project_environment table.'); + } + + // Queue up a verification of the platform. + hosting_add_task($task->ref->environment->platform, 'verify'); + + // Queue up a verification of the site. + hosting_add_task($task->ref->nid, 'verify'); +} diff --git a/modules/devshop/devshop_projects/devshop_projects.info b/modules/devshop/devshop_projects/devshop_projects.info new file mode 100644 index 000000000..38f30d85a --- /dev/null +++ b/modules/devshop/devshop_projects/devshop_projects.info @@ -0,0 +1,29 @@ +name = DevShop Projects +description = A DevShop module to sites/platforms together into projects. +core = 7.x +package = DevShop + +files[] = inc/create/create.inc +files[] = inc/create/step-1.inc +files[] = inc/create/step-2.inc +files[] = inc/create/step-3.inc +files[] = inc/create/step-4.inc +files[] = inc/admin.inc +files[] = inc/create-wizard.inc +files[] = inc/forms.inc +files[] = inc/logs.inc +files[] = inc/nodes.inc +files[] = inc/queue.inc +files[] = inc/tasks-ajax.inc +files[] = inc/ui.inc +files[] = tasks/commit.inc +files[] = tasks/create.inc +files[] = tasks/deploy.inc +files[] = tasks/pull.inc +files[] = tasks/tasks.inc +dependencies[] = hosting +dependencies[] = hosting_alias +dependencies[] = hosting_package +dependencies[] = hosting_git +dependencies[] = hosting_git_pull +dependencies[] = ctools diff --git a/modules/devshop/devshop_projects/devshop_projects.install b/modules/devshop/devshop_projects/devshop_projects.install new file mode 100644 index 000000000..cc605b868 --- /dev/null +++ b/modules/devshop/devshop_projects/devshop_projects.install @@ -0,0 +1,478 @@ + 7300, + ); + return $dependencies; +} + +/** + * Implements hook_install() + */ +function devshop_projects_install() { + + // Push devshop_projects's system weight to 1. + db_update('system') + ->fields(array( + 'weight' => 1 + )) + ->condition('name', 'devshop_projects') + ->execute(); +} + +/** + * Implements hook_schema(). + */ +function devshop_projects_schema() { + $schema['hosting_devshop_project'] = array( + 'fields' => array( + 'nid' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'description' => 'Project/Node ID.', + ), + 'git_url' => array( + 'type' => 'text', + 'size' => 'big', + 'not null' => FALSE, + ), + 'code_path' => array( + 'type' => 'text', + 'size' => 'big', + 'not null' => FALSE, + ), + 'drupal_path' => array( + 'type' => 'text', + 'size' => 'big', + 'not null' => FALSE, + ), + 'base_url' => array( + 'type' => 'text', + 'size' => 'big', + 'not null' => FALSE, + ), + 'install_profile' => array( + 'type' => 'text', + 'size' => 'big', + 'not null' => FALSE, + ), + 'settings' => array( + 'type' => 'text', + 'not null' => FALSE, + 'size' => 'big', + 'description' => 'A serialized array of settings for this project.', + ), + ), + 'primary key' => array('nid'), + ); + + // Table for tracking environments + $schema['hosting_devshop_project_environment'] = array( + 'fields' => array( + 'project_nid' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'description' => "The project's Node ID.", + ), + 'name' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => 64, + 'default' => '', + 'description' => 'Environment name.', + ), + 'site' => array( + 'type' => 'int', + 'not null' => FALSE, + 'default' => 0, + 'unsigned' => TRUE, + 'description' => 'The node ID of the site for this environment.', + ), + 'platform' => array( + 'type' => 'int', + 'not null' => FALSE, + 'default' => 0, + 'unsigned' => TRUE, + 'description' => 'The node ID of the platform for this environment.', + ), + 'settings' => array( + 'type' => 'text', + 'not null' => FALSE, + 'size' => 'big', + 'description' => 'A serialized array of settings for this environment.', + ), + 'last_task' => array( + 'type' => 'text', + 'not null' => FALSE, + 'size' => 'big', + 'description' => 'The NID of the last task that should be displayed on an environment.', + ), + ), + 'indexes' => array( + 'project_environment' => array('project_nid', 'name'), + ), + ); + return $schema; +} + +/** + * Add the "data" column to projects. + */ +function devshop_projects_update_1() { + $ret = array(); + $ret[] = update_sql("ALTER TABLE {hosting_devshop_project} " . + "ADD COLUMN data longtext NOT NULL default ''"); + return $ret; +} + +/* + * Update 2: Delete rows in the hosting_devshop_project_objects table + * that the node pointed to by object nid no longer exists. + */ +function devshop_projects_update_2() { + $ret = array(); + $query = db_query("SELECT object_nid " . + "FROM {hosting_devshop_project_object}"); + + while($proj = db_fetch_object($query)) { + $count = db_result(db_query("SELECT COUNT(*) FROM {node} " . + "WHERE nid = %d", $proj->object_nid)); + if ($count != 1) { + $ret[] = update_sql('DELETE FROM {hosting_devshop_project_object} ' . + 'WHERE object_nid = %d', $proj->object_nid); + } + } + + return $ret; +} + +/** + * Adds git_branch column to hosting_devshop_project_objects table. + */ +function devshop_projects_update_3() { + $ret = array(); + db_add_field($ret, 'hosting_devshop_project_object', 'git_branch', array('type' => 'varchar', 'length' => 16, 'not null' => FALSE)); + return $ret; +} + +/** + * Changes env_type to environment in {hosting_devshop_project_object}. + */ +function devshop_projects_update_4() { + $ret = array(); + $schema = devshop_projects_schema(); + $spec = $schema['hosting_devshop_project_object']['fields']['environment']; + db_change_field($ret, 'hosting_devshop_project_object', 'env_type', 'environment', $spec); + return $ret; +} + +/** + * Makes "git_branch" field larger. + */ +function devshop_projects_update_5() { + $ret = array(); + $ret[] = update_sql("ALTER TABLE {hosting_devshop_project_object} CHANGE git_branch git_branch VARCHAR(128) NULL"); + return $ret; +} + +/** + * Adds drupal_path column to hosting_devshop_project table. + */ +function devshop_projects_update_6() { + $ret = array(); + db_add_field($ret, 'hosting_devshop_project', 'drupal_path', array('type' => 'text', 'size' => 'big', 'not null' => FALSE)); + return $ret; +} + +/** + * Adds drupal_path column to hosting_devshop_project table. + */ +function devshop_projects_update_7() { + $ret = array(); + db_add_field($ret, 'hosting_devshop_project_object', 'drupal_path', array('type' => 'text', 'size' => 'big', 'not null' => FALSE)); + return $ret; +} + +/** + * Makes "environment" field larger. + */ +function devshop_projects_update_8() { + $ret = array(); + $ret[] = update_sql("ALTER TABLE {hosting_devshop_project_object} CHANGE environment environment VARCHAR(64) NOT NULL"); + + return $ret; +} + +/** + * Add clone_nid column to hosting_devshop_project_object. + */ +function devshop_projects_update_9() { + $ret = array(); + $ret[] = db_add_field($ret, 'hosting_devshop_project_object', 'clone_nid', array('type' => 'int', 'not null' => FALSE, 'default' => 0, 'unsigned' => TRUE)); + + return $ret; +} + +/** + * 6.x-1.9-beta4 -^ + * + * 6.x-1.9-rc1 -v (FUTURE RELEASE) + */ + +/** + * Remove live_domain from hosting_devshop_project schema. + */ +function devshop_projects_update_6000() { + $ret = array(); + if (db_column_exists('hosting_devshop_project', 'live_domain')){ + $ret[] = db_drop_field($ret, 'hosting_devshop_project', 'live_domain'); + } + return $ret; +} + +/** + * Create new hosting_devshop_environment table. + */ +function devshop_projects_update_6001() { + // Create only the new table + // Clone of drupal_install_schema('devshop_projects'); + $module = 'devshop_projects'; + $name = 'hosting_devshop_project_environment'; + + $schema = drupal_get_schema_unprocessed($module); + _drupal_initialize_schema($module, $schema); + + $ret = array(); + db_create_table($ret, $name, $schema['hosting_devshop_project_environment']); + return $ret; +} + +/** + * Enable hosting_alias module to allow custom domains on sites. + */ +function devshop_projects_update_6002() { + module_enable(array('hosting_alias')); + return array(); +} + +/** + * Remove legacy `hosting_devshop_project_object` table. + */ +function devshop_projects_update_6003(){ + $ret = array(); + db_drop_table($ret, 'hosting_devshop_project_object'); + return $ret; +} + +/** + * Change "data" column to "settings" column. + */ +function devshop_projects_update_6004(){ + $ret = array(); + db_change_column($ret, 'hosting_devshop_project', 'data', 'settings', 'text', array( + 'not null' => FALSE, + 'size' => 'big', + 'description' => 'A serialized array of settings for this project.', + )); + return $ret; +} + +/** + * Turn on the new Boots theme. + * Uncomment once the theme is ready. +function devshop_projects_update_600X() { + variable_set('theme_default','boots'); + return array(); +} + */ + +/** + * Add "last_task" column for environments. + */ +function devshop_projects_update_6008(){ + $schema = devshop_projects_schema(); + $ret = array(); + $ret[] = db_add_field( + $ret, + 'hosting_devshop_project_environment', + 'last_task', $schema['hosting_devshop_project_environment']['fields']['last_task'] + ); + return $ret; +} + +/** + * Update all environments with their last_task(); + */ +function devshop_projects_update_6009 () { + + // Lookup all environments + $query = db_query('SELECT * FROM {hosting_devshop_project_environment}'); + while ($result = db_fetch_object($query)) { + $site = node_load($result->site); + $result = db_fetch_object(db_query("SELECT t.nid, t.vid FROM hosting_task t INNER JOIN node n ON n.vid = t.vid WHERE rid = %d GROUP BY t.nid ORDER BY vid DESC", $site->nid)); + + $task = node_load($result->nid); + $site->environment->last_task = array( + 'status' => $task->task_status, + 'type' => $task->task_type, + 'nid' => $task->nid, + ); + devshop_environment_save_last_task($site->environment); + } +} + + +/** + * Reset all environment's "last task" to the NID. + */ +function devshop_projects_update_6010 () { + + // Lookup all environments + $query = db_query('SELECT * FROM {hosting_devshop_project_environment}'); + while ($result = db_fetch_object($query)) { + $site = node_load($result->site); + $result = db_fetch_object(db_query("SELECT t.nid, t.vid FROM hosting_task t INNER JOIN node n ON n.vid = t.vid WHERE rid = %d GROUP BY t.nid ORDER BY vid DESC", $site->nid)); + $site->environment->last_task = $result->nid; + devshop_environment_save_last_task($site->environment); + } +} + +/** + * Reset all environment's "last task" to the NID, again. + */ +function devshop_projects_update_6011 () { + devshop_reset_last_tasks(); +} + +/** + * Remove the menu form from the project node form. + */ +function devshop_projects_update_7000 () { + variable_set('menu_options_project', array()); +} + +/** + * Remove the menu form from the site node form. + */ +function devshop_projects_update_7001 () { + variable_set('menu_options_site', array()); +} + +// @TODO: This breaks upgrades. Figure out why and in include it in an upcoming release. +///** +// * Enable devshop_testing. +// */ +//function devshop_projects_update_7002 () { +// module_enable(array( +// 'devshop_testing', +// )); +//} + +/** + * Fully resetting project and site node menu settings. + */ +function devshop_projects_update_7003 () { + variable_set('menu_options_site', ''); + variable_set('menu_options_project', ''); +} + +/** + * Enable Hosting Git and Hosting Git Checkout. + */ +function devshop_projects_update_7004 () { + module_enable(array( + 'hosting_git', + 'hosting_git_pull', + )); +} + +/** + * Save git_ref data to hosting_git table and drop the devshop environment's git_ref field. + */ +function devshop_projects_update_7005 () { + $environments = db_query('SELECT * FROM {hosting_devshop_project_environment} e LEFT JOIN {hosting_devshop_project} p ON p.nid = e.project_nid'); + foreach ($environments as $environment) { + if ($environment->platform) { + db_merge('hosting_git') + ->key(array('nid' => $environment->platform)) + ->fields(array( + 'repo_url' => $environment->git_url, + 'git_ref' => $environment->git_ref, + )) + ->execute(); + drupal_set_message(t("Updated Platform $environment->platform with $environment->git_url and $environment->git_ref")); + } + else { + drupal_set_message(t("No Platform found for environment $environment->name for site $environment->site."), 'warning'); + } + } + + if (db_drop_field('hosting_devshop_project_environment', 'git_ref')) { + drupal_set_message(t("Git ref information has been migrated and the field has been removed.")); + } + else { + drupal_set_message(t("Unable to remove git_ref field from hosting_devshop_project_environment table."), 'error'); + } +} + +/** + * Reduce the character limit for bootstrap to turn form element descriptions into tooltips. + */ +function devshop_projects_update_7007 () { + // Get the settings + $settings = variable_get('theme_boots_settings', array()); + + // Set the variable + $settings['bootstrap_forms_smart_descriptions_limit'] = 40; + + // Save our settings + variable_set('theme_boots_settings', $settings); +} + +/** + * Set weight of devshop_projects module to 1 so it loads alters after hosting_git. + */ +function devshop_projects_update_7008 () { + + // Push devshop_projects's system weight to 1. + db_update('system') + ->fields(array( + 'weight' => 1 + )) + ->condition('name', 'devshop_projects') + ->execute(); + +} + +/** + * Update hosting contexts for project node to ensure they all begin with "project_". + */ +function devshop_projects_update_7009 () { + $query = "UPDATE hosting_context c JOIN node n ON n.nid=c.nid SET c.name = CONCAT('project_', name) WHERE n.type='project' AND c.name NOT LIKE 'project_'"; + if (!db_query($query)) { + drupal_set_message(t('Something went wrong when running a query to update hosting_context table to ensure all projects are prefixed with "project_":') . $query); + } +} \ No newline at end of file diff --git a/modules/devshop/devshop_projects/devshop_projects.module b/modules/devshop/devshop_projects/devshop_projects.module new file mode 100644 index 000000000..886ccb991 --- /dev/null +++ b/modules/devshop/devshop_projects/devshop_projects.module @@ -0,0 +1,903 @@ +step)) { + + // Make sure we don't show this message twice. + $message = t('You have an unfinished project. You should !link.', array( + '!link' => l(t('finish creating your project'), 'projects/add'), + )); + + $show_message = TRUE; + $messages = drupal_get_messages('status'); + + if (isset($messages['status'])) { + foreach ($messages['status'] as $m) { + if (strpos($m, $message) !== FALSE) { + $show_message = FALSE; + } + else { + drupal_set_message($m); + } + } + } + + if ($show_message) { + drupal_set_message($message); + } + } + } + + // If on the projects page, or a project or site node... + if (arg(0) == 'node' && is_numeric(arg(1))) { + $node = node_load(arg(1)); + if ($node->type == 'project' || $node->type == 'site' || $node->type == 'task') { + drupal_add_js(drupal_get_path('module', 'devshop_projects') . '/inc/task-ajax.js'); + + if ($node->type == 'project') { + drupal_add_js(array('devshopProject' => arg(1)), 'setting'); + } + if ($node->type == 'task') { + drupal_add_js(array('devshopTask' => arg(1)), 'setting'); + } + } + } + // If on the projects list page... + else { + drupal_add_js(drupal_get_path('module', 'devshop_projects') . '/inc/task-ajax.js'); + } +} + +/** + * Implements hook_permission(). + * + * Since we have a node type, "create project content permission is + * automatically added by Drupal + */ +function devshop_projects_permission() { + return array( + 'view projects' => array( + 'title' => t('view projects'), + 'description' => t('Access the projects page.'), + ), + 'create devshop-delete task' => array( + 'title' => t('create devshop-delete task'), + 'description' => t('Create "devshop-delete" task.'), + ), + 'create devshop-deploy task' => array( + 'title' => t('create devshop-deploy task'), + 'description' => t('Create "devshop-deploy" task.'), + ), + 'create project' => array( + 'title' => t('create project'), + 'description' => t('Create new DevShop projects.'), + ), + 'view project' => array( + 'title' => t('view project'), + 'description' => t('Access DevShop project dashboards.'), + ), + 'edit project' => array( + 'title' => t('edit project'), + 'description' => t('Edit DevShop projects.'), + ), + 'delete projects' => array( + 'title' => t('delete projects'), + 'description' => t('Delete DevShop projects.'), + ), + 'administer projects' => array( + 'title' => t('administer projects'), + 'description' => t('Administer DevShop projects.'), + ), + 'change site domain name' => array( + 'title' => t("Change a site's primary domain name."), + 'description' => t('Alter the main domain name the site responds to.'), + ), + ); +} + +/** + * Implements hook_devshop_environment_menu(). + * + * Defines the list of tasks that appear under the gear icon. + */ +function devshop_environment_menu_first_items($environment, $tasks) { + $site = null; + $platform = null; + $items = array(); + $aegir_items = array(); + + global $user; + + // Show for every site, even disabled ones. + if ($environment->site) { + $site = node_load($environment->site); + if (node_access('update', $site)) { + $items[] = l(' '.t('Environment Settings'), + "node/{$environment->site}/edit", + array( + 'html' => TRUE, + )); + $items[] = '
'; + + } + + if (drupal_valid_path("hosting_confirm/{$environment->site}/site_migrate") && user_access('change site domain names')) { + $href = "hosting_confirm/{$environment->site}/site_migrate"; + $items[] = l(' '.t('Change Domain Name'), + $href, array( + 'query' => array( + 'rename' => 1, + 'token' => drupal_get_token($user->uid), + ), + 'html' => TRUE, + )); + } + if (node_access('view', $site)) { + $aegir_items[] = l(' '.t('Aegir Site'), + "node/{$environment->site}", + array( + 'html' => TRUE, + )); + } + } + + // If platform exists + if ($environment->platform) { + $platform = node_load($environment->platform); + if (node_access('view', $platform)) { + $aegir_items[] = l(' '.t('Aegir Platform'), + "node/{$environment->platform}", + array( + 'query' => array( + 'rename' => 1, + 'token' => drupal_get_token($user->uid), + ), + 'html' => TRUE, + )); + } + } + + // Load test task, if it exists + if (drupal_valid_path("hosting_confirm/{$environment->site}/site_test")) { + $href = "hosting_confirm/{$environment->site}/site_test"; + $url = url($href, array( + 'query' => array( + 'token' => drupal_get_token($user->uid), + 'redirect' => 'task-page', + ) + )); + $title = $tasks['test']['title']; + if (isset($tasks['test']['icon'])) { + $icon = ''; + } + else { + $icon = ''; + } + $items[] = << + $icon $title + +HTML; + $items[] = '
'; + } + + // Add aegir items to items + if (!empty($aegir_items)) { + $items = array_merge($items, $aegir_items); + + if (!empty($actions)) { + $items[] = '
'; + } + } + + // Site specific actions. + if ($environment->site && $environment->site_status == HOSTING_SITE_ENABLED) { + if (node_access('create', 'site')) { + $items[] = l(' '.t('Clone Environment'), + "node/add/site/{$environment->project_name}/clone/{$environment->name}", + array( + 'html' => TRUE, + ) + ); + +// Removing "Fork Environment" because it's a decent amount of work to re-do now +// now that the site node form is being used. It was confusing to users , and we will +// have a new beta shortly after this one anyway. +// @TODO: Un-deprecate Fork Environment. +// $items[] = l(' '.t('Fork Environment'), +// "node/add/site/{$environment->project_name}/fork/{$environment->name}", +// array( +// 'html' => TRUE, +// ) +// ); + } + + // Add disable or delete task based on hosting variable. + if (!variable_get('hosting_require_disable_before_delete', TRUE)) { + $items[] = 'disable'; + $items[] = 'delete'; + } else { + + if ($environment->site_status == HOSTING_SITE_DISABLED) { + $items[] = 'enable'; + $items[] = 'delete'; + } elseif (empty($environment->settings->locked)) { + $items[] = 'disable'; + } + } + + $items[] = '
'; + + // Add export and import config buttons to Drupal 8 sites. + if (module_exists('aegir_config') && strpos($environment->version, '8') === 0) { + $items[] = 'config_export'; + $items[] = 'config_import'; + $items[] = '
'; + } + elseif (module_exists('hosting_tasks_extra') && strpos($environment->version, '7') === 0) { + $items[] = 'features_update_all'; + $items[] = 'features_revert_all'; + $items[] = '
'; + } + } + elseif ($environment->site && $environment->site_status == HOSTING_SITE_DISABLED) { + $items[] = 'enable'; + $items[] = 'delete'; + } + + return $items; +} + +/** + * Implements hook_node_access(). + */ +function devshop_projects_node_access($node, $op, $account) { + switch ($op) { + case 'create': + return user_access('create project', $account); + break; + case 'update': + return user_access('edit project', $account); + break; + case 'delete': + return user_access('delete projects', $account); + break; + case 'view': + return user_access('view project', $account); + break; + } +} + +/** + * Implements hook_menu(). + */ +function devshop_projects_menu() { + //Settings page. + $items['admin/devshop'] = array( + 'title' => 'DevShop Settings', + 'description' => 'Default values for use in creation project', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('devshop_projects_settings_form'), + 'access arguments' => array('administer projects'), + 'file' => 'admin.inc', + 'file path' => drupal_get_path('module', 'devshop_projects') . '/inc', + 'type' => MENU_NORMAL_ITEM, + ); + $items['admin/devshop/settings'] = array( + 'title' => t('DevShop Settings'), + 'type' => MENU_DEFAULT_LOCAL_TASK, + ); + + $items['projects'] = array( + 'title' => t('Projects'), + 'description' => 'Display a list of all projects', + 'page callback' => 'devshop_projects_projects_page', + 'access arguments' => array('view projects'), + 'menu_name' => 'main-menu', + 'type' => MENU_NORMAL_ITEM, + 'weight' => 1, + ); + $items['projects/list'] = array( + 'title' => t('All Projects'), + 'description' => '', + 'type' => MENU_DEFAULT_LOCAL_TASK, + ); + $items['projects/add'] = array( + 'title' => 'Start New Project', + 'type' => MENU_LOCAL_TASK, + 'title' => t('Start a new Project'), + 'title callback' => 'check_plain', + 'page callback' => 'devshop_projects_add', + 'page arguments' => array(2), + 'access arguments' => array('create project'), + 'description' => 'Start a new Drupal website project.', + 'file' => 'create.inc', + 'file path' => drupal_get_path('module', 'devshop_projects') . '/inc/create', + ); + + // Ajax endpoint for reloads + $items['projects/add/status'] = array( + 'page callback' => 'devshop_projects_add_status', + 'access callback' => 'node_access', + 'access arguments' => array('create', 'project'), + 'file' => 'create.inc', + 'file path' => drupal_get_path('module', 'devshop_projects') . '/inc/create', + ); + + // hosting tasks ajax pages. + $tasks = hosting_available_tasks('project') ; + if (!empty($tasks)) { + foreach (hosting_available_tasks('project') as $task => $info) { + $path = 'hosting_confirm/%/project_' . $task; + $items[$path] = array( + 'title' => $info['title'], + 'description' => $info['description'], + 'page callback' => 'devshop_projects_hosting_task_confirm_form_page', + 'page arguments' => array(1, $task), + 'access callback' => 'hosting_task_menu_access_csrf', + 'access arguments' => array(1, $task), + 'type' => MENU_CALLBACK, + ); + $items[$path] = array_merge($items[$path], $info); + } + } + + // Drush aliases download. + $items['node/%node/aliases'] = array( + 'title' => 'Drush Aliases', + 'description' => '', + 'page callback' => 'devshop_project_drush_aliases_page', + 'page arguments' => array(1), + 'access callback' => 'node_access', + 'access arguments' => array('update', 1), + 'weight' => 1, + 'file' => 'inc/drush.inc', + 'type' => MENU_CALLBACK, + ); + +// // Add Environment page +// $items['project/%/add-environment'] = array( +// 'title' => 'Add Environment', +// 'type' => MENU_CALLBACK, +// 'page callback' => 'devshop_projects_environment_add_page', +// 'page arguments' => array(1), +// 'access callback' => 'devshop_projects_environment_add_access', +// 'access arguments' => array(1), +// ); + + // Ajax endpoint for reloads + $items['devshop/tasks'] = array( + 'page callback' => 'devshop_projects_tasks_status_json', + 'page arguments' => array(2), + 'description' => '', + 'access arguments' => array('access task logs'), + 'file' => 'tasks-ajax.inc', + 'file path' => drupal_get_path('module', 'devshop_projects') . '/inc', + ); + + return $items; +} + +/** + * Get the bootstrap class from hosting task status. + */ +function devshop_task_status_class($status) { + $codes = array( + HOSTING_TASK_SUCCESS => 'success', + HOSTING_TASK_QUEUED => 'queued', + HOSTING_TASK_ERROR => 'danger', + HOSTING_TASK_PROCESSING => 'processing', + HOSTING_TASK_WARNING => 'warning', + ); + return $codes[$status]; +} + +/** + * Get the fontawesome icon from hosting task status. + */ +function devshop_task_status_icon($status) { + $icons = array( + HOSTING_TASK_SUCCESS => 'check', + HOSTING_TASK_QUEUED => 'cog', + HOSTING_TASK_ERROR => 'exclamation-circle', + HOSTING_TASK_PROCESSING => 'cog fa-spin', + HOSTING_TASK_WARNING => 'warning', + ); + return $icons[$status]; +} + +function devshop_project_logs_access() { + return TRUE; +} + +/** + * Implements hook_block_info_alter() + * + * Disables Aegir's queue and "supporting aegir" block. + * + * @param $blocks + * @param $theme + * @param $code_blocks + */ +function devshop_projects_block_info_alter(&$blocks, $theme, $code_blocks) { + $blocks['hosting']['hosting_queues']['status'] = 0; + $blocks['hosting']['hosting_queues_summary']['status'] = 0; + $blocks['hosting']['hosting_supporting_aegir']['status'] = 0; +} + +/** + * Implements hook_block_info(). + */ +function devshop_projects_block_info() { + $blocks['project_nav'] = array( + 'info' => t('DevShop Project Nav'), + 'weight' => '-10', + 'cache' => DRUPAL_CACHE_GLOBAL, + 'status' => 1, + 'region' => 'header', + ); + $blocks['project_create'] = array( + 'info' => t('DevShop Create Project'), + 'weight' => '-10', + 'cache' => DRUPAL_CACHE_PER_USER, + 'status' => 1, + 'region' => 'sidebar_first', + ); + return $blocks; +} + +/** + * Implements hook_block_view(). + */ +function devshop_projects_block_view($delta) { + if ($delta == 'project_nav' && ( + arg(0) == 'node' || arg(0) == 'hosting_confirm' + )) { + + if (is_numeric(arg(1))) { + $node = node_load(arg(1)); + } + elseif (arg(1) == 'add') { + $node = devshop_projects_load_by_name(arg(3)); + } + + if (node_access('view', $node) && ($node->type == 'project' || !empty($node->project))) { + + // If viewed node is project, use that. + if (is_numeric($node->project)) { + if ($project_node = node_load($node->project)) { + $project = $project_node->project; + } + } + elseif (!empty($node->project)) { + $project = $node->project; + } + else { + return ''; + } + + $block['subject'] = ''; + $block['content'] = theme('devshop_project_nav', array('project' => $project)); + } + } + elseif ($delta == 'project_create') { + if (arg(0) == 'projects' && arg(1) == 'add') { + ctools_include('object-cache'); + $project = ctools_object_cache_get('project', 'devshop_project'); + $block['subject'] = ''; + $block['content'] = theme('devshop_project_add_status', array('project' => $project)); + } + } + if (isset($block)) { + return $block; + } +} + +/** + * Page Callback for projects/add + */ +function devshop_projects_add($step = NULL) { + if ($step == NULL) { + // Check to see if this project is still in the wizard + ctools_include('object-cache'); + $project_wizard_cache = ctools_object_cache_get('project', 'devshop_project'); + if (!empty($project_wizard_cache->step)) { + drupal_goto('projects/add/' . $project_wizard_cache->step); + } + } + return devshop_projects_create_wizard($step); +} + +/** + * Replacement for hosting_task_confirm_form() + * + * @TODO: Remove once http://drupal.org/node/1861898 is committed. + */ +function devshop_projects_hosting_task_confirm_form_page($nid, $task) { + $node = node_load($nid); + return drupal_get_form('hosting_task_confirm_form', $node, $task); +} + +/** + * Implements hook_menu_alter(). + * + * Replaces node/add/project with a ctools wizard. + */ +function devshop_projects_menu_alter(&$items) { + $items['node/add/project']['page callback'] = 'devshop_projects_create_wizard'; + $items['node/add/project']['page arguments'] = array(3); + $items['node/add/project']['file'] = 'create-wizard.inc'; + $items['node/add/project']['file path'] = drupal_get_path('module', 'devshop_projects') . '/inc'; + + // Make project node pages more user-friendly. + $items['node/%node/view']['title callback'] = 'devshop_hosting_project_tab_title'; + $items['node/%node/view']['title arguments'] = array('View', 1); + + $items['node/%node/edit']['title callback'] = 'devshop_hosting_project_tab_title'; + $items['node/%node/edit']['title arguments'] = array('Edit', 1); + + $items['node/%node/edit']['access callback'] = 'devshop_hosting_project_tab_access'; + + + + $items['node/%node/view']['weight'] = -10; + $items['node/%node/edit']['weight'] = -9; + + // Hosting now hides all but migrate and clone tasks. + $path = sprintf("hosting_confirm/%hosting_%s_wildcard/site_devshop-deploy", 'deploy', 'deploy', 'devshop-deploy'); + $items[$path]['access callback'] = 'devshop_hosting_task_menu_access_csrf'; + + // Redirect hosting/task/% to the node + $items['hosting/task/%'] = array( + 'page callback' => 'drupal_goto_nid', + 'page arguments' => array(2), + 'access callback' => TRUE, + ); +} + +/** + * Helper to redirect to a node page. + * @param $nid + */ +function drupal_goto_nid($nid) { + drupal_goto("node/{$nid}"); +} + +/** + * Access callback helper for hosting task menu items. + * A copy of hosting_task_menu_access() except we allow devshop-deploy tasks. + * @see hosting_task_menu_access() + */ +function devshop_hosting_task_menu_access_csrf($node, $task) { + global $user; + + $interactive_tasks = array('migrate', 'clone', 'devshop-deploy'); + + // To prevent CSRF attacks, a unique token based upon user is used. Deny + // access if the token is missing or invalid. We only do this on + // non-interactive tasks. + if (!in_array($task, $interactive_tasks) && (!isset($_GET['token']) || !drupal_valid_token($_GET['token'], $user->uid))) { + return FALSE; + } + // Call the main menu access handler. + return hosting_task_menu_access($node, $task); +} + +/** + * Tab title replacer + */ +function devshop_hosting_project_tab_title($default, $node) { + if ($default == 'View' && $node->type == 'project') { + return t('Project Dashboard'); + } + if ($default == 'Edit' && $node->type == 'project') { + return t('Project Settings'); + } + if ($default == 'View' && $node->type == 'site' && isset($node->project)) { + return t('Environment Dashboard'); + } + if ($default == 'Edit' && $node->type == 'site' && isset($node->project)) { + return t('Environment Settings'); + } + if ($node->type == 'task') { + if ($default == 'View') { + $tasks = hosting_available_tasks('site'); + $label = drupal_ucfirst($tasks[$node->task_type]['title']); + return t('Task: !type', array('!type' => $label)); + } + } + + // Otherwise, just return the page text + return t($default); +} + +/** + * replacement for node_access + */ +function devshop_hosting_project_tab_access($type, $node) { + if ($node->type == 'task') { + return FALSE; + } + return node_access($type, $node); +} + +/** + * Access Callback for Aegir Tasks + * + * This function defines access to the various aegir tasks. + * + * @arg $node object + * The node object is running the task. (Site, Platform, Server) + * + * @arg $task string + * The type of the task that is running. + * + * @see hosting_task_menu_access() + * + * @TODO: This NEVER runs for verify! Only for devshop-xyz tasks. + * project verify task is defined in devshop_projects_hosting_tasks() in + * inc/tasks.inc, and has this function as it's access callback. But this + * function seems to never run. + */ +function devshop_hosting_task_menu_access($node, $task) { + // If we are passed the nid by mistake + if (!isset($node->nid)) { + $node = node_load($node); + } + + if ($node->type != 'project' && $task != 'delete' && !$node->status) { + return FALSE; + } + + if (user_access("create " . $task . " task")) { + return TRUE; + } +} + + + +/** + * Implements hook_hosting_drush_aliases_name(). + * + * See http://drupal.org/project/hosting_drush_aliases + */ +function devshop_projects_hosting_drush_aliases_name($node) { + if (isset($node->project_name)) { + return $node->project_name . "." . $node->project_environment; + } +} + +/** + * Helper function to create a site in a project. + * Used by Wizard & "Create Platform" > Post Verify + */ +function devshop_projects_create_site($project, $platform_node, $environment_name, $db_server = NULL) { + global $user; + + // Create the site nodes + $node = new stdClass(); + $node->type = 'site'; + $node->status = 1; + $node->uid = $user->uid; + $node->title = devshop_environment_url($project, $environment_name); + + // Aegir properties + // @TODO: better client & DB support + $node->client = HOSTING_DEFAULT_CLIENT; + $servers = hosting_get_servers('db', FALSE); + $server = $db_server ? $db_server : key($servers); + $node->db_server = $server; + + $node->platform = $platform_node->nid; + + // Lookup this platforms install profile + $node->profile = db_query('SELECT nid FROM {hosting_package} WHERE short_name = :short_name', array(':short_name' => $project->install_profile))->fetchField(); + + // @TODO: Improve site language handling? + $node->site_language = !empty($user->language) ? $user->language : 'en'; + + // Subdomain alias, if configured. + if ($project->settings->live['environment_aliases'] && !empty($project->settings->live['live_domain'])) { + $node->aliases = array($environment_name . '.' . $project->settings->live['live_domain']); + } + + // Save the site node + try { + if ($node = node_submit($node)) { + node_save($node); + } + } catch (PDOException $e) { + drupal_set_message(t('Unable to save environment: %message', array( + '%message' => $e->getMessage(), + ))); + } + + // Lookup install task to use it for last task. + $tasks = hosting_get_tasks('rid', $node->nid); + + // Update environment with our site + db_update('hosting_devshop_project_environment') + ->fields(array( + 'site' => $node->nid, + 'last_task' => current($tasks)->nid, + )) + ->condition('project_nid', $project->nid) + ->condition('name', $environment_name) + ->execute(); + + return $node; +} + +/** + * Helper to get select #options for git ref. + */ +function devshop_projects_git_ref_options($project, $current_ref = '') { + + // Build branch options + if (is_array($project->settings->git['branches']) && !empty($project->settings->git['branches'])) { + $options = array( + 'Branches' => array_combine($project->settings->git['branches'], $project->settings->git['branches']), + ); + + // If there are tags... + if (is_array($project->settings->git['tags']) && !empty($project->settings->git['tags'])) { + $options['Tags'] = array_combine($project->settings->git['tags'], $project->settings->git['tags']); + } + } + + // If there are none... + if (!isset($options)) { + $options = array(t('No branches or tags! Re-validate the project.')); + } + + // Set "current" label. + if (isset($options['Branches'][$current_ref])) { + $options['Branches'][$current_ref] .= ' (' . t('current') . ')'; + } + elseif (isset($options['Tags'][$current_ref])) { + $options['Tags'][$current_ref] .= ' (' . t('current') . ')'; + } + + + return $options; +} + +/** + * Check if a site has features diff enabled. + */ +function _devshop_projects_site_has_module($nid, $module) { + if (is_object($nid) && isset($nid->nid)) { + $nid = $nid->nid; + } + + $query = db_select('hosting_package_instance', 'i') + ->fields('i', array('status')); + $query->leftJoin('hosting_package', 'p', 'i.package_id = p.nid'); + + $query->condition('p.short_name', $module); + $query->condition('rid', $nid); + + $result = $query->execute()->fetchField(); + return $result; +} + +/** + * Check if a site has features diff enabled. + */ +function _devshop_projects_project_has_module($node, $module) { + $environment = key($node->project->environments); + + if (isset($node->project->environments[$environment]->site)) { + return _devshop_projects_site_has_module($node->project->environments[$environment]->site, $module); + } +} + +/** + * Reset all environment's "last task" to the NID. + */ +function devshop_reset_last_tasks () { + + // Lookup all environments + $results = db_query('SELECT * FROM {hosting_devshop_project_environment}')->fetchAllAssoc('site'); + foreach ($results as $result) { + $nid = db_query("SELECT t.nid FROM hosting_task t WHERE rid = :platform OR rid = :site ORDER BY t.vid DESC", array( + ':site' => $result->site, + ':platform' => $result->platform, + ))->fetchField(); + + if (!empty($result->name)) { + $result->last_task = $nid; + devshop_environment_save_last_task($result); + } + } +} + +/** + * Helper to get a project node by name. + */ +function devshop_load_project_by_name($name) { + $nid = db_select('node', 'n') + ->fields('n', array('nid')) + ->condition('type', 'project') + ->condition('title', $name) + ->condition('status', 1) + ->execute() + ->fetchField() + ; + $node = node_load($nid); + return $node; +} + +/** + * Generate an environment's URL from project and environment name using the domain name pattern. + * + * @param $project + * @param $environment_name + * @return string + */ +function devshop_environment_url($project, $environment_name) { + + // Generate field prefix and suffix using domain name pattern. + if (variable_get('devshop_projects_allow_custom_base_url')) { + $pattern = $project->base_url; + } + else { + $pattern = variable_get('devshop_project_environment_url_pattern', '@project.@environment.@hostname'); + } + $domain = strtr($pattern, array( + '@environment' => $environment_name, + '@project' => $project->name, + '@hostname' => $_SERVER['SERVER_NAME'], + )); + return $domain; +} + +/** + * Retrieve a project node by name. + * + * @param $name + * @return bool|mixed + */ +function devshop_projects_load_by_name($name) { + $nid = db_select('node', 'n') + ->fields('n', array('nid')) + ->condition('title', $name) + ->condition('type', 'project') + ->condition('status', 1) + ->execute() + ->fetchField(); + + return node_load($nid); +} diff --git a/modules/devshop/devshop_projects/drush/Provision/Config/ProjectAliases.php b/modules/devshop/devshop_projects/drush/Provision/Config/ProjectAliases.php new file mode 100644 index 000000000..555c244b9 --- /dev/null +++ b/modules/devshop/devshop_projects/drush/Provision/Config/ProjectAliases.php @@ -0,0 +1,31 @@ +data = array( + 'name' => strtr($context, array('@project_' => '')), + 'project' => $project, + ); + } + + function filename() { + return drush_server_home() . '/.drush/project_aliases/' . $this->data['name'] . '.aliases.drushrc.php'; + } +} diff --git a/modules/devshop/devshop_projects/drush/Provision/Config/project_aliases.tpl.php b/modules/devshop/devshop_projects/drush/Provision/Config/project_aliases.tpl.php new file mode 100644 index 000000000..19202aae2 --- /dev/null +++ b/modules/devshop/devshop_projects/drush/Provision/Config/project_aliases.tpl.php @@ -0,0 +1,39 @@ +environments as $name => $environment) { + + if ($environment->site_status != 1) { + continue; + } + // Tell drush to inherit from the provision site alias record. + $alias = array( + 'parent' => '@' . $environment->uri + ); + + // If web server is not server master, add "remote host and user. + if (d($environment->drush_alias)->platform->web_server->name != '@server_master') { + $alias['remote-host'] = d($environment->drush_alias)->platform->web_server->remote_host; + $alias['remote-user'] = d($environment->drush_alias)->platform->web_server->script_user; + } + + $export = var_export($alias, TRUE); + ?> + +$aliases[''] = ; + + settings->aliases as $name => $remote_alias) { + $export = var_export($remote_alias, TRUE); + ?> + +$aliases[''] = ; + + diff --git a/modules/devshop/devshop_projects/drush/Provision/Context/project.php b/modules/devshop/devshop_projects/drush/Provision/Context/project.php new file mode 100644 index 000000000..abd16a6f1 --- /dev/null +++ b/modules/devshop/devshop_projects/drush/Provision/Context/project.php @@ -0,0 +1,46 @@ +project['environments'][$name])) { + return (object) $this->project['environments'][$name]; + } + else { + return drush_set_error('DEVSHOP_PROJECT_ERROR', dt('Environment %name not found.', array( + '%name' => $name, + ))); + } + } + + static function option_documentation() { + return array( + '--project_name' => 'Project: The codename for this project.', + '--project' => 'Project: JSON encoded data about the project.', + + //@TODO: Document this! +// '--code_path' => 'Project: The path to the project codebases. (NOT the Drupal root)', +// '--drupal_path' => 'Project: The path to the drupal root.', +// '--git_url' => 'Project: The Git URL for this project.', +// '--git_branches' => 'Project: The available Git branches in the remote repository for this project.', +// '--git_tags' => 'Project: The available Git tags in the remote repository for this project.', +// '--base_url' => 'Project: the base URL that the dev/test/live subdomains will be attached to.', +// '--server' => 'Project: The server hosting this project. (Default is @server_master)', +// '--install_profile' => 'Project: The desired installation profile for all sites.', + ); + } + + function init_project() { + $this->setProperty('project_name'); + $this->setProperty('project'); + } + + function verify() { + $this->type_invoke('verify'); + drush_log('[DEVSHOP] verify()', 'ok'); + } +} diff --git a/modules/devshop/devshop_projects/drush/Provision/Service/Process.php b/modules/devshop/devshop_projects/drush/Provision/Service/Process.php new file mode 100644 index 000000000..bb8e70e75 --- /dev/null +++ b/modules/devshop/devshop_projects/drush/Provision/Service/Process.php @@ -0,0 +1,26 @@ +server->remote_host)) { + return provision_process($command, $cwd, $label, $env, $log_output); + } + else { + return provision_process('ssh ' . drush_get_option('ssh-options', '-o PasswordAuthentication=no') . ' ' . $this->server->script_user . '@' . $this->server->remote_host . ' ' . $command, $cwd, $label, $env); + } + } +} diff --git a/modules/devshop/devshop_projects/drush/Provision/Service/project.php b/modules/devshop/devshop_projects/drush/Provision/Service/project.php new file mode 100644 index 000000000..b20d70be7 --- /dev/null +++ b/modules/devshop/devshop_projects/drush/Provision/Service/project.php @@ -0,0 +1,26 @@ +setProperty('environment'); + $context->setProperty('environment_settings'); + $context->setProperty('project'); + } + + /** + * Add environment, project, and git ref to platform aliases. + */ + static function subscribe_platform($context) { + $context->setProperty('environment'); + $context->setProperty('project'); + } + +} diff --git a/modules/devshop/devshop_projects/drush/acquia/README.txt b/modules/devshop/devshop_projects/drush/acquia/README.txt new file mode 100644 index 000000000..be1b71e65 --- /dev/null +++ b/modules/devshop/devshop_projects/drush/acquia/README.txt @@ -0,0 +1,9 @@ +Acquia Cloud Hooks Integration +============================== + +DevShop now supports firing Acquia Cloud Hooks when deploying to environments. + +The code in acquia.drush.inc will detect an acquia repo and trigger your hooks +to run. + +Currently, only the `post-code-update` hook is supported. More to come. \ No newline at end of file diff --git a/modules/devshop/devshop_projects/drush/acquia/acquia.drush.inc b/modules/devshop/devshop_projects/drush/acquia/acquia.drush.inc new file mode 100644 index 000000000..59346a401 --- /dev/null +++ b/modules/devshop/devshop_projects/drush/acquia/acquia.drush.inc @@ -0,0 +1,76 @@ +type == 'site'){ + + $project_name = d()->project; + $project = (object) d("@project_{$project_name}")->project; + $environment = (object) $project->environments[d()->environment]; + + // Respect environment settings. + if ($environment->settings['deploy']['acquia_hooks'] != 1) { + return; + } + + // If project has no path to drupal, we know it's not acquia. + if ($project->drupal_path != 'docroot') { + drush_log('[DEVSHOP] Acquia Cloud skipped.', 'ok'); + return; + } + + // Detect hook files. + $cloud_hooks_path = $environment->repo_path . '/hooks'; + if (file_exists($cloud_hooks_path)) { + drush_log('[DEVSHOP] Acquia Cloud Hooks detected...', 'ok'); + } + + // Collect scripts to run. + // Common scripts. + $files = scandir($cloud_hooks_path . '/common/post-code-update'); + if (empty($files)) $files = array(); + $scripts = array_diff($files, array('..', '.')); + foreach ($scripts as &$script) { + $script = realpath($cloud_hooks_path . '/common/post-code-update/' . $script); + } + + // Environment scripts: Post Code Update + if (file_exists($cloud_hooks_path . '/' . $environment->name)) { + $files = scandir($cloud_hooks_path . '/' . $environment->name . '/post-code-update'); + if (empty($files)) $files = array(); + $scripts += array_diff($files, array('..', '.')); + foreach ($scripts as &$script) { + $script = realpath($cloud_hooks_path . '/' . $environment->name . '/post-code-update/' . $script); + } + } + + // @TODO: Post Code Deploy & Post DB Copy + + drush_log(implode("\n", $scripts), 'ok'); + + // Run Scripts + // Usage: post-code-deploy site target-env source-branch deployed-tag repo-url repo-type + foreach ($scripts as $file) { + drush_log('[DEVSHOP] Running Acquia Cloud Hook: ' . $file, 'ok'); + + if (drush_shell_exec("sh $file {$project_name} {$environment->name} old_branch {$branch} repo_url repo_type devshop $environment->url") !== 0) { + $output = drush_shell_exec_output(); + drush_log(implode("\n", $output), 'ok'); + } + else { + return drush_set_error(DRUSH_FRAMEWORK_ERROR, 'The last cloud hook returned a non-zero exit code. Remaining hooks skipped.'); + } + } + } +} \ No newline at end of file diff --git a/modules/devshop/devshop_projects/drush/acquia/example.slack.sh b/modules/devshop/devshop_projects/drush/acquia/example.slack.sh new file mode 100644 index 000000000..3c4f848d1 --- /dev/null +++ b/modules/devshop/devshop_projects/drush/acquia/example.slack.sh @@ -0,0 +1,39 @@ +#!/bin/sh +# +# Altered from https://github.com/acquia/cloud-hooks/tree/master/samples/slack +# Requires a /var/aegir/slack_settings file that contains: +# +# SLACK_WEBHOOK_URL=https://example.slack.com/services/hooks/incoming-webhook?token=TOKEN +# +# Cloud Hook: post-code-deploy +# +# The post-code-deploy hook is run whenever you use the Workflow page to +# deploy new code to an environment, either via drag-drop or by selecting +# an existing branch or tag from the Code drop-down list. See +# ../README.md for details. +# +# Usage: post-code-deploy site target-env source-branch deployed-tag repo-url +# repo-type + +site="$1" +target_env="$2" +source_branch="$3" +deployed_tag="$4" +repo_url="$5" +repo_type="$6" +source="$7" +site_url="$8" + +# Load the Slack webhook URL (which is not stored in this repo). +. $HOME/slack_settings + +if [ $source = 'devshop' ]; then + source="DevShop" + image="https://www.drupal.org/files/project-images/devshop-icon.png" +else + source="Acquia Cloud" + image="https://pbs.twimg.com/profile_images/1901642489/cloud_icon_150.png" +fi + +# Post deployment notice to Slack +curl -X POST --data-urlencode "payload={\"channel\": \"#dev-engageny\", \"username\": \"$source\", \"text\": \"Git branch \`$deployed_tag\` updated on *$target_env*: $site_url.\", \"icon_url\": \"$image\"}" $SLACK_WEBHOOK_URL diff --git a/modules/devshop/devshop_projects/drush/aegir_extras/aegir_download.drush.inc b/modules/devshop/devshop_projects/drush/aegir_extras/aegir_download.drush.inc new file mode 100644 index 000000000..8840c3e04 --- /dev/null +++ b/modules/devshop/devshop_projects/drush/aegir_extras/aegir_download.drush.inc @@ -0,0 +1,157 @@ + 'Downloads drupal modules and themes, and optionally commits them to git.', + 'bootstrap' => DRUSH_BOOTSTRAP_DRUSH, + 'options' => array( + 'packages' => 'This list of modules and themes to download.', + 'commit' => 'Commit the downloaded code to git.', + 'message' => 'The message to use when committing.', + 'update' => 'Run update.php after the download.', + 'test' => 'Queue a test run after the download.', + ), + 'arguments' => array( + ), + 'examples' => array( + 'drush @env.project.domain.com provision-download views ctools --update --commit --message="the usuals"' => 'Downloads views & ctools, commits them to the repo with a message, and run drush updb.', + ), + 'aliases' => array('pdl'), + ); + return $items; +} + +/** + * Implements the provision-download command. + */ +function drush_aegir_download_provision_download() { + $packages = drush_get_option('packages'); + $composer_build = false; + + // Use composer require if there is a composer.json file and packages.drupal.org is a repo... + if (file_exists(d()->platform->repo_path . '/composer.json')) { + $composer_json = json_decode(file_get_contents(d()->platform->repo_path . '/composer.json')); + $cmd = ''; + + if (is_string($packages)) { + $packages = explode(' ', $packages); + } + + if (isset($composer_json->repositories)) { + foreach ($composer_json->repositories as $repo) { + if (strpos($repo->url, 'https://packages.drupal.org') === 0) { + $cmd = "composer require "; + foreach ($packages as $package) { + $cmd .= "drupal/$package "; + $composer_build = true; + } + continue; + } + } + } + + if (empty($cmd)) { + drush_log(dt('The Drupal packagist repository was not found: unable to use `composer require` to add modules. Please see https://www.drupal.org/docs/develop/using-composer/using-composer-to-manage-drupal-site-dependencies#drupal-packagist'), 'warning'); + } + } + else { + + if (is_array($packages)) { + $packages = implode($packages); + } + + // No modules, no more. + if (empty($packages)) { + return drush_set_error('No packages defined.'); + } + + // Run `drush dl $modules` + $target = d()->name; + $cmd = "drush $target dl $packages --yes --strict=0"; + + // Pass through options to the drush dl command. + if (drush_get_option('commit', FALSE)) { + $cmd .= ' --commit '; + } + if (drush_get_option('message', '')) { + $message = escapeshellarg(drush_get_option('message', dt('Committed by DevShop: ') . $packages)); + $cmd .= " --message='$message' "; + } + if (drush_get_option('update', FALSE)) { + $cmd .= " --update"; + } + } + + provision_process($cmd, d()->platform->repo_path); + + // Commit and push has to happen here when using composer, there are no post-composer hooks here. + if ($composer_build && drush_get_option('commit', FALSE)) { + $message = escapeshellarg(drush_get_option('message', dt('Committed by DevShop: ') . implode(" ", $packages))); + + $files = 'composer.json'; + if (file_exists(d()->platform->repo_path . '/composer.lock')) { + $files .= ' composer.lock'; + } + + $cmd = "git commit $files --message=$message"; + provision_process($cmd, d()->platform->repo_path); + + $process = drush_get_context('provision_process_result'); + if ($process->getExitCode() == 0) { + provision_process('git push', d()->platform->repo_path); + } + } +} + +/** + * Implements hook_drush_pm_post_download() + * + * Runs after a project has been downloaded. + * + * This is needed for devshop because we want to commit the new module we just + * downloaded, and only this hook knows about the path. + * + * @param $drupal_project + * @param $release + */ +function aegir_download_drush_pm_post_download($drupal_project, $release) { + + $project_name = d()->project; + $project = (object) d("@project_{$project_name}")->project; + $environment = (object) $project->environments[d()->environment]; + + if (d()->environment && drush_get_option('commit', FALSE)) { + drush_log(dt('[DEVSHOP] Committing new module...'), 'ok'); + + $message = drush_get_option('message', dt('Committed by DevShop: ') . $drupal_project['name']); + + $wrapper = new GitWrapper(); + $wrapper->streamOutput(); + + try { + $git = $wrapper->workingCopy($environment->repo_path); + + $download_path = $drupal_project['full_project_path']; + $git + ->add($download_path) + ->commit($message) + ->push(); + } + catch (GitException $e) { + return drush_set_error('DRUSH_ERROR', "Git Exception: " . $e->getMessage()); + } + } + else { + drush_log(dt('Skipping Commit. Make sure you commit or delete this module at some point.'), 'notice'); + } +} diff --git a/modules/devshop/devshop_projects/drush/contexts.inc b/modules/devshop/devshop_projects/drush/contexts.inc new file mode 100644 index 000000000..fd7fb8ef5 --- /dev/null +++ b/modules/devshop/devshop_projects/drush/contexts.inc @@ -0,0 +1,360 @@ +ref->project->git_url); + + $home = variable_get('aegir_home', '/var/aegir'); + $clone_dir = "$home/repos/{$task->ref->project->name}"; + + // If something went wrong connecting to the git repo, don't wipe out our branches. + if (!empty($branches['branches'])) { + $task->ref->project->settings->git['branches'] = $branches['branches']; + $task->ref->project->settings->git['tags'] = $branches['tags']; + $task->ref->project->settings->git['refs'] = $branches['refs']; + + // Save the project node now that we have branches and tags. + // Don't verify again, this is the verification process. + $task->ref->no_verify = TRUE; + node_save($task->ref); + } + + // If branches are empty, and "github_create" was selected... + elseif (!file_exists($clone_dir) && $task->ref->project->settings->create_project_settings['git_source'] == 'github_create') { + + $git_url = $task->ref->project->git_url; + + $account = user_load($task->uid); + $name = $account->name; + $mail = $account->mail; + $env = array(); + + // Make commit using environment variables for NAME and EMAIL, if specified. + if (!empty($name)) { + $env['GIT_AUTHOR_NAME'] = $name; + $env['GIT_COMMITTER_NAME'] = $name; + } + if (!empty($mail)) { + $env['GIT_AUTHOR_MAIL'] = $mail; + $env['GIT_COMMITTER_MAIL'] = $mail; + } + + $hostname = $_SERVER['HOSTNAME']; + + if (!file_exists("$home/repos")) { + provision_process("mkdir -p $home/repos", null, t('Creating repos directory')); + } + + if ($task->ref->project->settings->create_project_settings['github_repository_source']['populate_choice'] == 'import') { + $source_url = $task->ref->project->settings->create_project_settings['github_repository_source']['import']; + provision_process("git clone $source_url $clone_dir"); + provision_process("git remote set-url origin $git_url", $clone_dir); + } + else { + + $composer_project = $task->ref->project->settings->create_project_settings['github_repository_source']['composer_project']; + $composer_create_project_command = "composer create-project {$composer_project} {$task->ref->project->name} --no-interaction --ansi"; + + provision_process($composer_create_project_command,"$home/repos", t('Creating composer project...')); + $result = drush_get_context('provision_process_result'); + if (!$result->isSuccessful()) { + return drush_set_error('DEVSHOP_ERROR', 'Composer create-project failed.'); + } + + if (!file_exists($clone_dir)) { + return drush_set_error('DEVSHOP_ERROR', 'Unable to find codebase in ' . $clone_dir); + } + + provision_process("git init", $clone_dir); + + provision_process("git add .gitignore", $clone_dir); + provision_process("git add -A", $clone_dir); + provision_process("git status", $clone_dir); + provision_process("git commit -m 'Repo created by DevShop on $hostname using the command `$composer_create_project_command`'", $clone_dir, t('Committing codebase...'), $env); + provision_process("git remote add origin $git_url", $clone_dir); + } + + $branch = trim(str_replace('refs/heads/', '', shell_exec("cd {$clone_dir}; git describe --tags --exact-match 2> /dev/null || git symbolic-ref -q HEAD 2> /dev/null"))); + + provision_process("git push -u origin $branch", $clone_dir); + + if (file_exists($clone_dir)) { + provision_process("rm -rf $clone_dir", null, t('Removing temporary git clone...')); + } + + $branches = getBranchesAndTags($task->ref->project->git_url); + $task->ref->project->settings->git['branches'] = $branches['branches']; + $task->ref->project->settings->git['tags'] = $branches['tags']; + $task->ref->project->settings->git['refs'] = $branches['refs']; + + // Save the project node now that we have branches and tags. + // Don't verify again, this is the verification process. + $task->ref->no_verify = TRUE; + node_save($task->ref); + + } + + // Save project object to drush alias (aegir context). + if (isset($task->ref->project)) { + $task->context_options['server'] = '@server_master'; + $task->context_options['project_name'] = $task->ref->title; + + // Clean up project and environment objects. + $project = $task->ref->project; + unset($project->tasks); + unset($project->verify); + unset($project->messages); + + foreach ($project->environments as &$environment) { + unset($environment->tasks); + unset($environment->tasks_list); + unset($environment->last_task_node); + } + + $task->context_options['project'] = $project; + } +} + +/** + * Implements hook_hosting_site_context_options(). + * + * Runs on verify task. Passes data to the drush alias. + * Save environment name, project name, and git ref to site aliases. + */ +function devshop_projects_hosting_site_context_options(&$task) { + + if (isset($task->ref->environment)) { + $task->context_options['environment'] = $task->ref->environment->name; + $task->context_options['environment_settings'] = $task->ref->environment->settings; + $task->context_options['project'] = $task->ref->project->name; + $task->context_options['git_ref'] = $task->ref->environment->git_ref; + } + + // If install task is set to force-reinstall, pass to drush options. + if ($task->task_type == 'install' && $task->task_args['force-reinstall']) { + $task->options['force-reinstall'] = 1; + } + +} + +/** + * Implements hook_hosting_site_context_options(). + * + * Runs on verify task. Passes data to the drush alias. + * Save environment name, project name, and git ref to site aliases. + */ +function devshop_projects_hosting_platform_context_options(&$task) { + + if (isset($task->ref->environment)) { + $task->context_options['environment'] = $task->ref->environment->name; + $task->context_options['project'] = $task->ref->project->name; + $task->context_options['git_ref'] = $task->ref->environment->git_ref; + } +} + +/** + * Implements hook_drush_context_import(). + * + * This allows project nodes to be created from contexts (aliases) + */ +function devshop_projects_drush_context_import($context, &$node) { + if ($context->type == 'project') { + + $node->title = $context->project_name; + $node->type = 'project'; + + $project = (object) $context->project; + $node->project = new stdClass(); + $node->project->git_url = $project->git_url; + $node->project->code_path = $project->code_path; + $node->project->drupal_path = $project->drupal_path; + $node->project->base_url = $project->base_url; + $node->project->install_profile = $project->install_profile; + $node->project->settings = $project->settings; + + foreach ($project->environments as $name => $environment) { + $environment = (object) $environment; + + // Backwards compatibility: *_context properties were added, may be missing. + if (empty($environment->site_context)) { + $environment->site_context = $environment->uri; + } + if (empty($environment->platform_context)) { + $environment->platform_context = "platform_{$project->name}_{$environment->name}"; + } + + // Load site and platform nodes. + $site_node = hosting_context_load($environment->site_context); + $platform_node = hosting_context_load($environment->platform_context); + + // If both site and platform nodes were found, load them in. + if ($site_node && $platform_node) { + drush_log(dt('Site and Environment nodes found. Saving environment data for site "!site_context" (nid:!site) and platform "!platform_context" (nid:!platform)', array( + '!site' => $site_node->nid, + '!platform' => $platform_node->nid, + '!site_context' => $environment->site_context, + '!platform_context' => $environment->platform_context, + )), 'success'); + $environment_object = new stdClass(); + $environment_object->name = $name; + $environment_object->site = $site_node->nid; + $environment_object->platform = $platform_node->nid; + $environment_object->settings = $environment->settings; + + // Project environments only get automatically added on project insert. + if (!empty($node->nid)) { + $environment_object->project_nid = $node->nid; + $environment_object->settings = serialize($environment_object->settings); + devshop_environment_save($environment_object); + } + else { + // Project is being created, we can just append the environment. + $node->project->environments[$name] = $environment_object; + } + } + else { + // If site or platform is missing, look for drush context. If found, trigger import of that. + $t = array( + '!context' => $environment->site_context, + ); + if (empty($site_node) && $context = d($environment->site_context) && $context->type == 'site') { + drush_log(dt('Site node not found, using context "!context" instead.', $t), 'warning'); + hosting_drush_import($context); + } + else { + // @TODO: Should we create site context here? + drush_set_error(dt('No site node or context found for "!context". Create one with that name first if you want environments to be created.', $t)); + } + if (empty($platform_node) && $context = d($environment->platform_context) && $context->type == 'platform') { + drush_log(dt('Platform node not found, using context "!context" instead.', array('!context' => $environment->platform_context)), 'warning'); + hosting_drush_import($context); + } + else { + // @TODO: Should we create platform context here? + drush_set_error(dt('No platform node or context found for "!context". Create one with that name first if you want environments to be created.', array('!context' => $environment->platform_context))); + } + } + } + devshop_reset_last_tasks(); + } + + // If importing a site that has environment and project assigned, + elseif ($context->type == 'site' && !empty($context->environment) && !empty($context->project)) { + + // If site, platform, and project already exist, update environment info from alias. + $project_nid = hosting_context_nid("project_{$context->project}"); + $platform_nid = hosting_context_nid($context->platform); + + if (!empty($node->nid) && $project_nid && $platform_nid) { + + drush_log(dt('Found existing site environment (!environment), and project properties (!project). Saving environment.', array( + '!environment' => $context->environment, + '!project' => $context->project, + )), 'success'); + + $environment_object = new stdClass(); + $environment_object->name = $context->environment; + $environment_object->site = $node->nid; + $environment_object->platform = $platform_nid; + $environment_object->settings = $context->environment_settings; + $environment_object->project_nid = $project_nid; + + if (devshop_environment_save($environment_object)) { + drush_log(dt('Environment saved.'), 'success'); + } + else { + drush_set_error('DEVSHOP_ENVIRONMENT_NOT_SAVED', dt('Environment not saved.')); + } + } + + // Site node doesn't exist, will be created after this function. + elseif (empty($node->nid)) { + + // If project node not found, try to load the context and import that. + if (empty($project_nid) && $context = d("project_{$context->project}") && $context->type == 'project') { + drush_log(dt('Project node not found, importing context "!context".', array('!context' => "project_{$context->project}")), 'warning'); + hosting_drush_import($context); + + } + // If there is a project nid, apply it as a property of the node so the insert hook can handle it. + elseif (!empty($project_nid)) { + $node->project_nid = $project_nid; + } + // If there is no project nid, and no project context, throw a warning. + else { + drush_log(dt('Site property "project" is set to "!project", but there is no project node or context by that name.'), 'warning'); + } + } + + devshop_reset_last_tasks(); + } +} + +/** + * Helpfer for getting branches and tags from a git URL + */ +function getBranchesAndTags($git_url = NULL) { + if (is_null($git_url)) { + $git_url = drush_get_option('git_url'); + } + $command = "git ls-remote {$git_url}"; + $output = provision_process($command); + $lines = explode("\n", $output); + + // Build tag and branch list + $branches = array(); + $tags = array(); + $refs = array(); + + foreach ($lines as $line_string) { + + // "annotated" tags come with an extra row and these characters at the end. + // See http://stackoverflow.com/questions/15472107/when-listing-git-ls-remote-why-theres-after-the-tag-name + if (substr($line_string, -3, 3) == '^{}') { + continue; + } + + // Example remote line: + // 9fc5727c0823d8d3300ba5aae3328d5998033e45 refs/heads/master + // 9fc5727c0823d8d3300ba5aae3328d5998033e45 refs/tags/1.0 + $line = trim(substr($line_string, 40)); + if (empty($line) || $line == 'HEAD') { + continue; + } + + // If branch + if (strpos($line, 'refs/heads/') === 0) { + $git_ref = str_replace('refs/heads/', '', $line); + $branches[] = $git_ref; + $refs[$git_ref] = 'branch'; + } + // else if tag + elseif (strpos($line, 'refs/tags/') === 0) { + $git_ref = str_replace('refs/tags/', '', $line); + $tags[] = $git_ref; + $refs[$git_ref] = 'tag'; + } + // If not a tag or a head, continue. + // @TODO: Should we store alternative types? GitHub Pull Requests use this. + else { + continue; + } + } + drush_log(dt('Found !count branches: !list', array( + '!count' => count($branches), + '!list' => implode(', ', $branches), + )), 'p_log'); + drush_log(dt('Found !count tags: !list', array( + '!count' => count($tags), + '!list' => implode(', ', $tags), + )), 'p_log'); + + return array('branches' => $branches, 'tags' => $tags, 'refs' => $refs); +} diff --git a/modules/devshop/devshop_projects/drush/deploy.devshop.provision.inc b/modules/devshop/devshop_projects/drush/deploy.devshop.provision.inc new file mode 100644 index 000000000..bb0aa4ca9 --- /dev/null +++ b/modules/devshop/devshop_projects/drush/deploy.devshop.provision.inc @@ -0,0 +1,149 @@ +type == 'site') { + if (empty(d()->environment)) { + return drush_set_error('DEVSHOP_FRAMEWORK_ERROR', 'This site is not a part of a project. You cannot use this command.'); + } + } + else { + return drush_set_error('DEVSHOP_FRAMEWORK_ERROR', 'provision-devshop-deploy must be run on a site context.'); + } + + // Verify that the branch or tag exists + if (empty($git_ref)) { + return drush_set_error('DEVSHOP_FRAMEWORK_ERROR', 'You must specify a valid branch or tag.'); + } + + $project_alias = '@project_' . d()->project; + $project = (object) d($project_alias)->project; + + if (!isset($project->settings['git']['refs'][$git_ref])) { + $drush_command = "drush $project_alias provision-verify"; + return drush_set_error('DEVSHOP_FRAMEWORK_ERROR', "Branch or tag '$git_ref' not found. Try running '$drush_command' to fetch new remote branches or tags."); + } +} + +/** + * Implements the provision-devshop-deploy command. + */ +function drush_devshop_provision_provision_devshop_deploy($git_ref = '') +{ + $project_name = d()->project; + $project = (object) d("@project_{$project_name}")->project; + $environment = (object) $project->environments[d()->environment]; + $desired_ref_type = $project->settings['git']['refs'][$git_ref]; + + drush_log('[Current Working Directory]' . d()->platform->repo_path, 'p_log'); + + // Stash any changes? Not sure if we should do this anymore... + // $git->command('stash'); + + // Fetch + provision_process("git fetch --all", d()->platform->repo_path, dt('DevShop Deploy')); + + // Checkout the chosen ref + $git_checkout_output = provision_process("git checkout {$git_ref}", d()->platform->repo_path, dt('DevShop Deploy')); + $git_checkout_output_lines = explode("\n", $git_checkout_output); + + // Run Git Pull, if on a branch. + if ($desired_ref_type == 'branch') { + $git_pull_output = provision_process("git pull", d()->platform->repo_path, dt('DevShop Deploy')); + } + + // Run a submodule update and init. + provision_process("git submodule update --init --recursive --force ", d()->platform->repo_path, dt('DevShop Deploy')); + + provision_process("git status", d()->platform->repo_path, dt('DevShop Deploy')); +} + +/** + * Post provision-devshop-deploy + */ +function drush_devshop_provision_post_provision_devshop_deploy($git_ref = '') { + + // Get post deploy options + $revert = drush_get_option('revert'); + $update = drush_get_option('update'); + $cache = drush_get_option('cache'); + $composer = drush_get_option('composer', TRUE); + + $project_name = d()->project; + $project = (object) d("@project_{$project_name}")->project; + $environment = (object) $project->environments[d()->environment]; + $desired_ref_type = $project->settings['git']['refs'][$git_ref]; + + // Ensure drush_alias exists. Not sure why it was missing for me. + if (empty($environment->drush_alias)) { + $environment->drush_alias = d()->name; + } + + $commands = array(); + + drush_log("[{$project_name}] {$environment->name}: " . dt('Running deploy hooks.'), 'notice'); + + // Built in Hooks + if ($composer) { + if (file_exists(d()->platform->repo_path . '/composer.json')) { + $commands[] = "composer install"; + } + elseif (file_exists($environment->root . '/composer.json')) { + $commands[] = "cd {$environment->root} && composer install"; + } + } + else { + drush_log(dt('[DEVSHOP] Skipped running composer install...'), 'ok'); + } + + // Built in Hooks + if ($update) { + $commands[] = "drush {$environment->drush_alias} updatedb --yes"; + } + else { + drush_log(dt('[DEVSHOP] Skipped updating database...'), 'ok'); + } + + // Clear the whole cache, unless option is false + if ($cache) { + if (drush_drupal_major_version(d()->root) == 8) { + $commands[] = "drush {$environment->drush_alias} cache-rebuild"; + } + else { + $commands[] = "drush {$environment->drush_alias} cache-clear all"; + } + } + else { + drush_log(dt('[DEVSHOP] Skipped clearing all caches...'), 'ok'); + } + + // Revert All Features, unless option is false + if ($revert) { + $commands[] = "drush {$environment->drush_alias} features-revert-all --yes"; + } + else { + drush_log(dt('[DEVSHOP] Skipped reverting all features...'), 'ok'); + } + + foreach ($commands as $command) { + $output = provision_process($command, d()->platform->repo_path, 'DevShop Deploy Hook'); + + // Detect common errors and help the user. + // Composer install with incorrect PHP version. + if (strpos($command, 'composer install') !== FALSE && strpos($output, 'your PHP version') !== FALSE && strpos($output, 'does not satisfy that requirement') !== FALSE) { + drush_log(dt('Composer indicated that it could not install packages because the site codebase requires a higher version of PHP than what is running on the server.'), 'p_log'); + drush_log(dt('You can either upgrade your version of PHP on this server or set the maximum PHP version in your composer.json file. See !link for instructions for how to modify your composer.json file, then make sure to run `composer update` on your codebase to pin your packages to a lower PHP version.', [ + '!link' => 'https://getcomposer.org/doc/06-config.md#platform', + ]), 'p_log'); + } + + } +} + diff --git a/modules/devshop/devshop_projects/drush/deploy_hooks_example/devshop.drush.inc b/modules/devshop/devshop_projects/drush/deploy_hooks_example/devshop.drush.inc new file mode 100644 index 000000000..bb6fceb5e --- /dev/null +++ b/modules/devshop/devshop_projects/drush/deploy_hooks_example/devshop.drush.inc @@ -0,0 +1,39 @@ +type == 'site'){ + + $project_name = d()->project; + $project = (object) d("@project_{$project_name}")->project; + $environment = (object) $project->environments[d()->environment]; + + drush_log("[CUSTOM] Successfully checked out $branch to environment $environment->name in project $project_name.", 'ok'); + +// provision_backend_invoke(d()->name, 'status'); + + } +} diff --git a/modules/devshop/devshop_projects/drush/devshop-dev.make b/modules/devshop/devshop_projects/drush/devshop-dev.make new file mode 100644 index 000000000..b9808342b --- /dev/null +++ b/modules/devshop/devshop_projects/drush/devshop-dev.make @@ -0,0 +1,19 @@ +; +; This makefile is used by the DevShop standalone installer to build devmaster. +; + +core = 6.x +api = 2 + +projects[drupal][type] = "core" + +; DEVELOPMENT MODE: +; When in development, use this: +projects[devmaster][type] = "profile" +projects[devmaster][download][type] = "git" +projects[devmaster][download][url] = "git@git.drupal.org:project/devmaster.git" +projects[devmaster][download][branch] = "6.x-1.x" + +; RELEASE: +; When releasing, lock in the devmaster version. +;projects[devmaster][version] = "6.x-1.0" \ No newline at end of file diff --git a/modules/devshop/devshop_projects/drush/devshop_provision.drush.inc b/modules/devshop/devshop_projects/drush/devshop_provision.drush.inc new file mode 100644 index 000000000..664d637ab --- /dev/null +++ b/modules/devshop/devshop_projects/drush/devshop_provision.drush.inc @@ -0,0 +1,555 @@ + 'Deploys a tag or branch to an environment and (optionally) run update.php, clear cache, and revert features.', + 'bootstrap' => DRUSH_BOOTSTRAP_DRUSH, + 'options' => array( + 'update' => 'Run update.php after code pull.', + 'revert' => 'Revert all features after code pull.', + 'cache' => 'Clear all caches after code pull.', + 'reset' => 'Runs "git reset --hard" before pulling.', + 'force' => "Runs always update,revert and cache options, even if files don't change.", + 'test' => 'Queue a test run after the deploy. (Only works from Hostmaster)', + ), + 'arguments' => array( + 'git_ref' => 'The git branch or tag to deploy.', + ), + 'examples' => array( + 'drush @env.project.domain.com provision-devshop-deploy master --cache --update' => 'Triggers a git checkout & pull of branch master to the dev environment, clearing caches and running updates.', + ), + 'aliases' => array('deploy'), + ); + $items['provision-test'] = array( + 'description' => 'Run a set of tests.', + 'bootstrap' => DRUSH_BOOTSTRAP_DRUSH, + 'options' => array( + 'tests-to-run' => 'The list of tests to run, separated by comma.', + 'test-type' => 'The type of tests to run. simpletest, behat', + 'behat-folder-path' => 'The path to this sites behat tests.', + 'behat-bin-path' => 'The path to the behat executable within behat-folder-path', + 'output-path' => 'The path to a folder to store results in. Will be created if it doesn\'t exist', + ), + 'aliases' => array('test'), + ); + return $items; +} + +/** + * Function for checking if this is a project and we have a repo. + * + * Used in pre drush command hooks + */ +function devshop_provision_pre_flight($platform_name = NULL){ + if (d()->type != 'Project'){ + return drush_set_error(DEVSHOP_FRAMEWORK_ERROR, 'All provision-devshop-* commands must be run on a project alias.'); + } +} + +/** + * Append PHP code to Drupal's settings.php file. + * + * To use templating, return an include statement for the template. + * + * @param $uri + * URI for the site. + * @param $data + * Associative array of data from provisionConfig_drupal_settings::data. + * + * @return + * Lines to add to the site's settings.php file. + * + * @see provisionConfig_drupal_settings + */ +function devshop_provision_provision_drupal_config($uri, $data, $config = NULL) { + + $environment = d()->environment; + $project = d()->project; + + return << NULL, + 'Process' => 'Process', + ); +} + +/** + * Implements hook_provision_apache_vhost_config() + * + * Adds "devshop_project" and "devshop_environment" server variables. + * + * @param $uri + * URI for the site. + * @param $data + * Associative array of data from Provision_Config_Apache_Site::data. + * + * @return + * Lines to add to the configuration file. + * + * @see Provision_Config_Apache_Site + * provision_logs_provision_apache_vhost_config + */ +function devshop_provision_provision_apache_vhost_config($uri, $data) { + + $environment = d()->environment; + $project = d()->project; + + if (empty($environment) || empty($project)) { + return; + } + + return <<environment; + $project = d()->project; + + if (empty($environment) || empty($project)) { + return; + } + + return <<project) && empty(d()->environment)) { + drush_log('[DEVSHOP] New environment detected. Rebuilding Registry.', 'ok'); + drush_invoke('registry-rebuild'); + } +} + +/** + * Find the username of the current running procses + * + * This will return the username of the current running user (as seen + * from posix_geteuid()) and should be used instead of + * get_current_user() (which looks at the file owner instead). + * + * @see get_current_user() + * @see posix_geteuid() + * + * @return + * String. The username. + */ +function devshop_current_user() { + return devshop_posix_username(posix_geteuid()); +} + +/** + * Check whether a user is a member of a group. + * + * @param user + * username or user id of user. + * @param group + * groupname or group id of group. + * + * @return + * Boolean. True if user does belong to group, + * and FALSE if the user does not belong to the group, or either the user or group do not exist. + */ +function devshop_user_in_group($user, $group) { + // TODO: make these singletons with static variables for caching. + $user = devshop_posix_username($user); + $group = devshop_posix_groupname($group); + if ($user && $group) { + $info = posix_getgrnam($group); + if (in_array($user, $info['members'])) { + return TRUE; + } + } + return FALSE; +} + +/** + * Return the valid system username for $user. + * + * @return + * Returns the username if found, otherwise returns FALSE + */ +function devshop_posix_username($user) { + // TODO: make these singletons with static variables for caching. + // we do this both ways, so that the function returns NULL if no such user was found. + if (is_numeric($user)) { + $info = posix_getpwuid($user); + $user = $info['name']; + } + else { + $info = posix_getpwnam($user); + $user = $info['name']; + } + return $user; +} + +/** + * Return the valid system groupname for $group. + * + * @return + * Returns the groupname if found, otherwise returns FALSE + */ +function devshop_posix_groupname($group) { + // TODO: make these singletons with static variables for caching. + // we do this both ways, so that the function returns NULL if no such user was found. + if (is_numeric($group)) { + $info = posix_getgrgid($group); + $group = $info['name']; + } + else { + $info = posix_getgrnam($group); + $group = $info['name']; + } + return $group; +} + +/** + * Generate a random alphanumeric password. + * + * This is a copy of Drupal core's user_password() function. We keep it + * here in case we need this and don't have a bootstrapped Drupal + * around. + * + * @see user_password() + */ +function devshop_password($length = 10) { + // This variable contains the list of allowable characters for the + // password. Note that the number 0 and the letter 'O' have been + // removed to avoid confusion between the two. The same is true + // of 'I', 1, and 'l'. + $allowable_characters = 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789'; + + // Zero-based count of characters in the allowable list: + $len = strlen($allowable_characters) - 1; + + // Declare the password as a blank string. + $pass = ''; + + // Loop the number of times specified by $length. + for ($i = 0; $i < $length; $i++) { + + // Each iteration, pick a random character from the + // allowable string and append it to the password: + $pass .= $allowable_characters[mt_rand(0, $len)]; + } + + return $pass; +} + +function devshop_default_web_group() { + $info = posix_getgrgid(posix_getgid()); + $common_groups = array( + 'www-data', + 'apache', + 'nginx', + 'www', + '_www', + 'webservd', + 'httpd', + 'nogroup', + 'nobody', + $info['name']); + + foreach ($common_groups as $group) { + if (devshop_posix_groupname($group)) { + return $group; + break; + } + } + return NULL; +} + +/** + * return the FQDN of the machine or provided host + * + * this replicates hostname -f, which is not portable + */ +function devshop_fqdn($host = NULL) { + if (is_null($host)) { + $host = php_uname('n'); + } + return strtolower(gethostbyaddr(gethostbyname($host))); +} + +// +//if (!function_exists('provision_autoload_register_prefix')) { +// /** +// * Register a PECL style prefix with the provision autoloader. +// * +// * @param string $prefix +// * The class prefix to register. +// * @param string $dir +// * The directory to search for the classes in. +// * @param bool $prepend +// * If the directory should be searched first for the classes in the given +// * prefix, set this to TRUE, otherwise, the default, FALSE, is fine. +// */ +// function provision_autoload_register_prefix($prefix, $dir, $prepend = FALSE) { +// +// // Get any current directories set for this prefix. +// $current_prefixes = provision_autoload()->getPrefixes(); +// if (isset($current_prefixes[$prefix])) { +// $dirs = $current_prefixes[$prefix]; +// } +// else { +// $dirs = array(); +// } +// +// // Now add the new one. +// if ($prepend) { +// array_unshift($dirs, $dir); +// } +// else { +// array_push($dirs, $dir); +// } +// +// // Set the prefixes. +// provision_autoload()->addPrefix($prefix, $dirs); +// } +//} +// +//if (!function_exists('provision_autoload')) { +// /** +// * Return an instance of the provision autoloader. +// * +// * This will instiatate an instance if it needs to. +// */ +// function provision_autoload() { +// static $instance = NULL; +// +// if (is_null($instance)) { +// $instance = new ClassLoader(); +// // Activate the autoloader. +// $instance->register(); +// } +// +// return $instance; +// } +//} + +/** + * Implementation of drush_hook_provision_pre_COMMAND() + * Calls drush_devshop_provision_pre_provision_verify() + */ +function drush_devshop_provision_pre_provision_deploy() { + drush_devshop_provision_pre_provision_verify(); +} + +/** + * Implementation of drush_hook_provision_pre_COMMAND() + * for Verify tasks: Writes easier to use drush aliases for each project. + */ +function drush_devshop_provision_pre_provision_verify() { + if (d()->type === 'project' || (!empty(d()->project) && d()->type === 'site' || d()->type === 'platform')) { + + } +} + +/** + * Implementation of drush_hook_post_provision_install() + * + * This function handles all of the alternative environment install methods. + */ +function drush_devshop_provision_post_provision_install() { + + // Install Method: Clone + // Sync from a drush alias. + drush_log('Installing environment with method: ' . d()->install_method , 'p_log'); + if (d()->install_method == 'clone') { + drush_log(dt('Environment Clone initiated...'), 'p_log'); + $clone_source = d()->environment_settings['install_method']['clone_source']; + + // If "clone_source" is set to "_other", use the drush alias in "clone_source_drush". + if ($clone_source == '_other') { + $source = d()->environment_settings['install_method']['clone_source_drush']; + } + elseif (!empty($clone_source)) { + $source = d()->environment_settings['install_method']['clone_source']; + } + else { + return drush_set_error('DEVSHOP_ERROR', dt('Install_method"clone_source" was not set.')); + } + + $destination = d()->name; + $command = "drush provision-sync $source $destination --database --files --update-uri --disable-rollback-backup --registry-rebuild"; + + // Build command based on environment settings. + if (d()->environment_settings['deploy']['update']) { + $command .= " --updatedb"; + } + if (d()->environment_settings['deploy']['cache']) { + $command .= " --cache-clear"; + } + if (d()->environment_settings['deploy']['revert']) { + $command .= " --features-revert-all"; + } + + provision_process($command, NULL, 'Cloning site from ' . d($source)->uri); + provision_process('drush cc drush', NULL, 'Clearing drush caches'); +// devshop_drush_process_cache_clear($destination); + } + + // Install Method: import + elseif (d()->install_method == 'import') { + + + // If import source is a mysql URL: + $path_or_url = d()->environment_settings['install_method']['import']; + $url = parse_url($path_or_url); + if ($url['scheme'] == 'mysql') { + + // Dump the file + $temp_file_name = tempnam('/tmp', 'devshop_remote_db.sql.'); + $db_name = ltrim($url['path'], '/'); + $command = "mysqldump -u{$url['user']} -p{$url['pass']} -h{$url['host']} {$db_name} --result-file={$temp_file_name}"; + provision_process($command, NULL, dt('Saving database file...')); + + $file_to_import = $temp_file_name; + } + + // If file exists but is not readable, alert the user. + elseif (file_exists($path_or_url) && !is_readable($path_or_url)){ + return drush_set_error('DEVSHOP_IMPORT_ERROR', dt('Unable to import SQL file @path: not readable.')); + } + + // If file exists and is readable, set it as the file to import. + elseif (file_exists($path_or_url) && is_readable($path_or_url)){ + $file_to_import = $path_or_url; + } + + // Import the file + $alias = d()->name; + $command = "drush {$alias} sqlq 'SOURCE {$file_to_import}'"; + provision_process($command, NULL, dt('Importing database file...')); + provision_process('drush cc drush', NULL, 'Clearing drush caches'); + devshop_drush_process_cache_clear($alias); + provision_process("drush {$alias} sqlq 'SHOW TABLES'", NULL, dt('Show Tables')); + } +} + +/** + * Runs drush cache-clear or drush cache-rebuild on the chosen alias. + * + * @param $alias + * A drush alias. + */ +function devshop_drush_process_cache_clear($alias) { + + // Clear all caches. + if (drush_drupal_major_version(d()->root) == 8) { + provision_process("drush {$alias} cache-rebuild", NULL, dt('Clear all Caches')); + } + else { + provision_process("drush {$alias} cache-clear all", NULL, dt('Clear all Caches')); + } +} diff --git a/modules/devshop/devshop_projects/drush/install.devshop.inc b/modules/devshop/devshop_projects/drush/install.devshop.inc new file mode 100644 index 000000000..e1e09b454 --- /dev/null +++ b/modules/devshop/devshop_projects/drush/install.devshop.inc @@ -0,0 +1,289 @@ + join("\n", drush_shell_exec_output())))); + } +} + +function drush_provision_devshop_install_validate($site = NULL) { + // set defaults for this whole script + // those are settings that are not prompted to the user but still overridable + drush_set_default('version', provision_version()); + drush_set_default('profile', 'devmaster'); + + // Get values needed to set other defaults + $version = drush_get_option('version'); + $aegir_root = drush_set_default('aegir_root', drush_server_home()); + $profile = drush_get_option('profile'); + + drush_set_default('root', $aegir_root . '/' . $profile . '-' . $version); + drush_set_default('r', drush_get_option('root')); + drush_set_default('script_user', devshop_current_user()); + drush_set_default('web_group', _provision_default_web_group()); + drush_set_default('http_service_type', 'apache'); + drush_set_default('http_port', '80'); + drush_set_default('aegir_db_user', 'root'); + drush_set_default('aegir_db_port', '3306'); + drush_set_default('client_name', 'admin'); + $aegir_db_user = drush_get_option('aegir_db_user'); + + // Generate "makefile" message only if there is one set. + $root = drush_get_option(array('r', 'root')); + if (is_dir($root) && !drush_get_option('makefile', FALSE)) { + // Don't assume we know the makefile used to build an existing platform + $makefile_msg = ''; + } + else { + drush_set_default('makefile', dirname(__FILE__) . '/devshop-dev.make'); + $makefile_msg = dt("Aegir makefile: !makefile\n", array('!makefile' => drush_get_option('makefile'))); + } + + // Generate "profile" message only if there is one set. + if (!drush_get_option('profile', FALSE)) { + $profile_msg = ''; + } + else { + $profile_msg = dt("Aegir install profile: !profile\n", array('!profile' => $profile)); + } + + drush_print("Aegir $version automated install script"); + drush_print("=============================================================================="); + + if (!$site || !drush_get_option('aegir_host', NULL) || !drush_get_option('aegir_db_pass', NULL) || filter_var(drush_get_option('client_email'), FILTER_VALIDATE_EMAIL)) { + drush_print("Some settings have not been provided and will now be prompted. +Don't worry: you will get to review those settings after the final install"); + } + // now we prompt the user for settings if not provided or not sane + if (!$site) { + $site = drush_prompt(dt("Aegir frontend URL"), get_fqdn()); + } + drush_set_option('site', $site); + + drush_set_default('aegir_host', get_fqdn()); + drush_set_default('aegir_db_host', 'localhost'); + + if (is_null(drush_get_option('aegir_db_pass', NULL))) { + // XXX: may not be portable everywhere? + system('stty -echo'); + drush_set_option('aegir_db_pass', drush_prompt(dt('MySQL privileged user ("!root") password', array('!root' => $aegir_db_user)))); + system('stty echo'); + print "\n"; // add a newline since the user's didn't print + } + + if (drush_get_option('aegir_host') == 'localhost') { + $default_email = 'webmaster@example.com'; + } else { + $default_email = 'webmaster@' . drush_get_option('aegir_host'); + } + drush_set_default('client_email', $default_email); + while (!filter_var(drush_get_option('client_email'), FILTER_VALIDATE_EMAIL) && !drush_get_context('DRUSH_AFFIRMATIVE')) { + drush_set_option('client_email', drush_prompt(dt("Admin user e-mail"), $default_email)); + } + + drush_print(dt(' +This script will operate the following changes in your system: + +1. Create server-level configuration directories +2. Create the Hostmaster frontend platform +3. Install the frontend site +4. Setup the dispatcher (a user cron job) + +We are making the following assumptions: + * you have read and are following the install instructions at: + http://community.aegirproject.org/installing + * the FQDN of this machine is valid and resolves + * you are executing this script as your "aegir" user + +The following settings will be used: + Aegir frontend URL: !site + Master server FQDN: !fqdn + Aegir root: !home + Aegir user: !user + Web group: !web + Web server: !web_server + Web server port: !web_server_port + Aegir DB host: !db_host + Aegir DB user: !db_user + Aegir DB password: !db_pass + Aegir DB port: !db_port + Aegir version: !version + Aegir platform path: !root + Admin email: !email + !makefile !profile', + array( + '!site' => $site, + '!fqdn' => drush_get_option('aegir_host'), + '!home' => drush_get_option('aegir_root'), + '!user' => drush_get_option('script_user'), + '!web' => drush_get_option('web_group'), + '!web_server' => drush_get_option('http_service_type'), + '!web_server_port' => drush_get_option('http_port'), + '!db_host' => drush_get_option('aegir_db_host'), + '!db_user' => drush_get_option('aegir_db_user'), + '!db_pass' => is_null(drush_get_option('aegir_db_pass', NULL, 'process')) ? '' : '', + '!db_port' => drush_get_option('aegir_db_port'), + '!version' => drush_get_option('version'), + '!root' => $root, + '!makefile' => $makefile_msg, + '!profile' => $profile_msg, + '!email' => drush_get_option('client_email'), + ))); + + if (!drush_confirm(dt('Do you really want to proceed with the install'))) { + return drush_set_error('PROVISION_CANCEL_INSTALL', dt('Installation aborted')); + } + + return TRUE; +} + +/** + * Drush command to install hostmaster. + */ +function drush_provision_devshop_install($site = NULL) { + $version = drush_get_option('version'); + $site = drush_get_option('site', get_fqdn()); + $aegir_root = drush_get_option('aegir_root'); + $platform = drush_get_option(array('r', 'root')); + + $aegir_http_host = drush_get_option('aegir_host'); + $aegir_http_port = drush_get_option('http_port'); + $aegir_db_user = drush_get_option('aegir_db_user'); + $aegir_db_pass = drush_get_option('aegir_db_pass'); + $aegir_db_port = drush_get_option('aegir_db_port'); + $aegir_db_host = drush_get_option('aegir_db_host'); + + $server = '@server_master'; + $master_context = array( + 'context_type' => 'server', + // files + 'remote_host' => $aegir_http_host, + 'aegir_root' => $aegir_root, + 'script_user' => drush_get_option('script_user'), + // apache or nginx or.. + 'http_service_type' => drush_get_option('http_service_type'), + 'http_port' => $aegir_http_port, + 'web_group' => drush_get_option('web_group'), + 'master_url' => "http://" . $site, + 'db_port' => $aegir_db_port, + ); + + $master_db = sprintf("mysql://%s:%s@%s:%s", urlencode($aegir_db_user), urlencode($aegir_db_pass), $aegir_db_host, $aegir_db_port); + if ($aegir_http_host == $aegir_db_host) { + $master_context['db_service_type'] = 'mysql'; + $master_context['master_db'] = $master_db; + $dbserver = $server; + } else { + $dbserver = '@server_' . $aegir_db_host; + $dbserver_context = array( + 'remote_host' => $aegir_db_host, + 'context_type' => 'server', + 'db_service_type' => 'mysql', + 'master_db' => $master_db, + 'db_port' => $aegir_db_port, + ); + drush_invoke_process('@none', "provision-save", array($dbserver), $dbserver_context); + provision_backend_invoke($dbserver, 'provision-verify'); + } + drush_invoke_process('@none', "provision-save", array($server), $master_context); + provision_backend_invoke($server, 'provision-verify'); + + // exit if an error has occured. + if (drush_get_error()) { + return false; + } + + if (drush_get_option('backend-only')) { + return; + } + + $platform_name = '@platform_hostmaster'; + drush_invoke_process('@none', "provision-save", array($platform_name), array( + 'context_type' => 'platform', + 'server' => $server, + 'web_server' => $server, + 'root' => $platform, + 'makefile' => drush_get_option('makefile'), + )); + // propagate working-copy args downward + $options = array(); + if (drush_get_option('working-copy')) { + $options['working-copy'] = 1; + } + provision_backend_invoke($platform_name, 'provision-verify', array(), $options); + + // exit if an error has occured. + if (drush_get_error()) { + return false; + } + + drush_set_default('profile', 'devmaster'); + $profile = drush_get_option('profile'); + + $site_name = '@hostmaster'; + drush_invoke_process('@none', "provision-save", array($site_name), array( + 'context_type' => 'site', + 'platform' => $platform_name, + 'db_server' => $dbserver, + 'uri' => $site, + 'client_name' => drush_get_option('client_name'), + 'profile' => $profile, + )); + $data = provision_backend_invoke($site_name, 'provision-install', array(), array('client_email' => drush_get_option('client_email'))); + provision_backend_invoke($site_name, 'provision-verify'); + + // exit if an error has occured. + if (drush_get_error()) { + return false; + } + + + drush_print(dt("Initializing the hosting system")); + drush_invoke_process('@none', 'cache-clear', array('drush')); + provision_backend_invoke($site_name, 'hosting-setup'); + + drush_print(""); + drush_print("=============================================================================="); + drush_print(""); + drush_print(""); + drush_print(dt("Congratulations, Aegir has now been installed.")); + drush_print(""); + drush_print(dt("You should now log in to the Aegir frontend by opening the following link in your web browser:")); + drush_print(""); + drush_print($data['context']['login_link']); + drush_print(""); + drush_print(""); + drush_print("=============================================================================="); + drush_print(""); +} + +/** + * Implements drush_hook_post_hostmaster_install(). + */ +function drush_provision_post_devshop_install() { + $backend_only = drush_get_option('backend-only'); + if (empty($backend_only)) { + drush_invoke_process('@hostmaster', 'cache-clear', array('drush')); + } +} diff --git a/modules/devshop/devshop_projects/drush/pull.devshop.provision.inc b/modules/devshop/devshop_projects/drush/pull.devshop.provision.inc new file mode 100644 index 000000000..2d2e7fdc4 --- /dev/null +++ b/modules/devshop/devshop_projects/drush/pull.devshop.provision.inc @@ -0,0 +1,123 @@ +project_name; + $platform_alias = '@' . implode('_', array('platform', $project_name, $platform_name)); + $platform = d($platform_alias); + + $site_alias = '@' . $platform_name . '.' . d()->project['base_url']; + $site = d($site_alias); + + // Find repo path + $repo_path = $platform->root; + + // Ensure it's a git repo. + provision_git_is_repo($repo_path); + + // Determine revert setting from project settings (or if set as option, always) + $reset = drush_get_option('reset') ? drush_get_option('reset') : FALSE; + + // If reset is true, do a git reset --hard first. + if ($reset) { + drush_shell_cd_and_exec($repo_path, 'git reset --hard'); + drush_log(dt('[DEVSHOP] Git repository reset.'), 'ok'); + } + + // Pull latest version of site + // Execute git pull --rebase + if (drush_shell_cd_and_exec($repo_path, 'git pull --rebase')) { + drush_log(dt('[DEVSHOP] Git repository pulled.', array('!path' => $repo_path)), 'ok'); + $output = drush_shell_exec_output(); + drush_log(implode("\n", drush_shell_exec_output()), 'ok'); + } + else { + drush_set_error('DRUSH_PROVISION_GIT_PULL_FAILED', dt("Git pull failed in !path.\nThe specific errors are below:\n!errors", array('!path' => $repo_path, '!errors' => implode("\n", drush_shell_exec_output())))); + continue; + } + + // If no new files were detected... and force is false then skip out. + if (count($output) == 1 && !$force) { + drush_log('[DEVSHOP] No changes detected. Nothing else needs to be done', 'ok'); + continue; + } + + // Verify the platform. + provision_backend_invoke($platform_alias, 'provision-verify'); + + // update db, unless option is false. + if ($update){ + drush_log(dt('[DEVSHOP] Updating database...'), 'ok'); + provision_backend_invoke($site_alias, 'updb'); + } + else { + drush_log(dt('[DEVSHOP] Skipped updating database...'), 'ok'); + } + + // Revert All Features, unless option is false + if ($revert){ + drush_log(dt('[DEVSHOP] Reverting all features...'), 'ok'); + provision_backend_invoke($site_alias, 'features-revert-all'); + } + else { + drush_log(dt('[DEVSHOP] Skipped reverting all features...'), 'ok'); + } + + // Clear the whole cache, unless option is false + // Seriously, lets do this twice. Go Drupal! + if ($cache){ + drush_log(dt('[DEVSHOP] Clearing all caches...'), 'ok'); + provision_backend_invoke($site_alias, 'cc all'); + provision_backend_invoke($site_alias, 'cc all'); + } + else { + drush_log(dt('[DEVSHOP] Skipped clearing all caches...'), 'ok'); + } + } +} + diff --git a/modules/devshop/devshop_projects/drush/test.devshop.provision.inc b/modules/devshop/devshop_projects/drush/test.devshop.provision.inc new file mode 100644 index 000000000..e69de29bb diff --git a/modules/devshop/devshop_projects/drush/test.provision.inc b/modules/devshop/devshop_projects/drush/test.provision.inc new file mode 100644 index 000000000..e46580e3d --- /dev/null +++ b/modules/devshop/devshop_projects/drush/test.provision.inc @@ -0,0 +1,182 @@ +project; + $project = (object) d("@project_{$project_name}")->project; + $environment = (object) $project->environments[d()->environment]; + + drush_log(dt('Provision DevShop Run Tests started...'), 'status'); + + // Get tests to run + if (!empty($project->settings['testing']['test_type'])) { + $type = $project->settings['testing']['test_type']; + } + else { + $type = drush_get_option('test-type', NULL); + } + + if ($project->settings['testing']['tests_to_run'] === NULL) { + $tests = array(); + } + else { + $tests = array_filter($project->settings['testing']['tests_to_run']); + } + $tests_to_run = drush_get_option('tests-to-run', $tests); + + // Run Simpletest + if ($type == 'simpletest') { + drush_log(dt("Running $type tests $tests_to_run"), 'ok'); + provision_backend_invoke('@self', 'en simpletest'); + provision_backend_invoke('@self', 'test-run', array($tests_to_run)); + } + elseif ($type == 'behat') { + // Get paths from options or site context. + $repo_path = d()->platform->repo_path? d()->platform->repo_path: d()->platform->root; + $behat_folder_path = drush_get_option('behat-folder-path', $project->settings['testing']['behat_folder_path']); + + // If no repo at that path, error. + if (!file_exists($repo_path)){ + return drush_set_error('DEVSHOP_MISSING_FILE', dt('repo_path does not exist.')); + } + // If no file at behat bin path, error. + $tests_folder = $repo_path . '/' .$behat_folder_path; + $yml_path = $repo_path . '/' .$behat_folder_path . '/behat.yml'; + if (!file_exists($yml_path)){ + + $message = << Testing. + 2. Copy the files from https://github.com/opendevshop/devmaster/tree/1.x/modules/devshop/devshop_testing/tests_example + 3. Write more tests! + +TXT; + + + drush_log(dt($message, array('!path' => $tests_folder)), 'error'); + return drush_set_error('DEVSHOP_MISSING_FILE', dt('Your project is not yet ready to run Behat tests. Please follow the instructions and try again.')); + } + + // Prepare path and command + $full_behat_folder_path = $repo_path . '/' . $behat_folder_path; + $full_behat_bin_path = $repo_path . '/' . $behat_folder_path . '/bin/behat'; + $full_behat_yml_path = $full_behat_folder_path . '/behat.yml'; + + // Load the behat.yml from behat_folder_path. + if (file_exists($full_behat_yml_path)) { + $behat_yml = file_get_contents($full_behat_yml_path); + $behat_yml_data = \Symfony\Component\Yaml\Yaml::parse($behat_yml); + } + elseif (file_exists($full_behat_folder_path . '/config/behat.yml')) { + $full_behat_yml_path = $full_behat_folder_path . '/config/behat.yml'; + $behat_yml = file_get_contents($full_behat_yml_path); + $behat_yml_data = \Symfony\Component\Yaml\Yaml::parse($behat_yml); + } + else { + return drush_set_error('DEVSHOP_MISSING_FILE', dt('behat.yml file not found in behat_folder_path: ') . $behat_folder_path); + } + + // Run composer install. + provision_process('composer install', $full_behat_folder_path, 'DevShop Testing'); + + // Write custom behat.yml to temporary folder. + $environment_name = $environment->name; + $alias = d()->name; + $root = d()->root; + + $username = d()->http_basic_auth_username; + $password = d()->http_basic_auth_password; + $uri = d()->uri; + $url = d()->ssl_enabled? + "https://$uri": + "http://$uri"; + + if (!empty($username)) { + $url = d()->ssl_enabled? + "https://$username:$password@$uri": + "http://$username:$password@$uri"; + } + + $behat_params = array( + 'extensions' => array( + 'Behat\\MinkExtension' => array( + 'base_url' => $url, + ), + 'Drupal\\DrupalExtension' => array( + 'drupal' => array( + "drupal_root" => $root, + ), + 'drush' => array( + "alias" => $alias, + ) + ), + ), + ); + + // Check the behat.yml file for options that will break automation. + if ($behat_yml_data['default']['extensions']['Behat\MinkExtension']['base_url']) { + drush_log(dt("Your project's behat.yml file includes the entry default.extensions.Behat\MinkExtension.base_url, preventing devshop from automating this test run. Please remove this entry to continue."), 'error'); + } + + if ($behat_yml_data['default']['extensions']['Drupal\DrupalExtension']['drupal']['drupal_root']) { + drush_log(dt("Your project's behat.yml file includes the entry default.extensions.Drupal\DrupalExtension.drupal.drupal_root, preventing devshop from automating this test run. Please remove this entry to continue."), 'error'); + } + + if ($behat_yml_data['default']['extensions']['Drupal\DrupalExtension']['drush']['drupal_root']) { + drush_log(dt("Your project's behat.yml file includes the entry default.extensions.Drupal\DrupalExtension.drupal.drupal_root, preventing devshop from automating this test run. Please remove this entry to continue."), 'error'); + } + + // Run behat tests for each feature. + $no_errors = TRUE; + $test_result = ''; + + // Fill an empty item if empty so we run all the tests. + if (empty($tests_to_run)) { + $tests_to_run[] = ''; + } + + // Foreach test to run... + $i = 0; + foreach ($tests_to_run as $feature) { + $i++; + + // Check for path. + if (!empty($feature) && substr($feature, 0, 1) !== '/') { + $feature = '/'.$feature; + } + $feature_path = empty($feature) ? '' : "features{$feature}"; + + // Create Command + // The extra profile is used so we can dynamically set the URL and drush alias of the behat.yml. + // the "colors" option is to force it to output ANSI colors. "format-settings" "expand: true" is so + // Behat will output all of the steps when using "Scenario Outlines". + $cmd = "$full_behat_bin_path $feature_path --colors --strict --format-settings='{\"expand\": true}'"; + + // Run behat command + provision_process($cmd, $full_behat_folder_path, 'DevShop Testing', array( + 'BEHAT_PARAMS' => json_encode($behat_params, JSON_FORCE_OBJECT), + ), TRUE, 'Test Run Failed'); + } + } +} + diff --git a/modules/devshop/devshop_projects/images/loader.gif b/modules/devshop/devshop_projects/images/loader.gif new file mode 100644 index 000000000..5b33f7e54 Binary files /dev/null and b/modules/devshop/devshop_projects/images/loader.gif differ diff --git a/modules/devshop/devshop_projects/images/loading.gif b/modules/devshop/devshop_projects/images/loading.gif new file mode 100644 index 000000000..bd6dd9741 Binary files /dev/null and b/modules/devshop/devshop_projects/images/loading.gif differ diff --git a/modules/devshop/devshop_projects/images/spinner-white.gif b/modules/devshop/devshop_projects/images/spinner-white.gif new file mode 100644 index 000000000..f27d7cd48 Binary files /dev/null and b/modules/devshop/devshop_projects/images/spinner-white.gif differ diff --git a/modules/devshop/devshop_projects/inc/add-environment-options.tpl.php b/modules/devshop/devshop_projects/inc/add-environment-options.tpl.php new file mode 100644 index 000000000..e69de29bb diff --git a/modules/devshop/devshop_projects/inc/admin.inc b/modules/devshop/devshop_projects/inc/admin.inc new file mode 100644 index 000000000..4896a0ab2 --- /dev/null +++ b/modules/devshop/devshop_projects/inc/admin.inc @@ -0,0 +1,134 @@ + 1) { + $options[$line[0]] = $line[1]; + } + else { + $options[] = $line[0]; + } + } + form_set_value($element, array_filter($options),$form_state); +} + +/** + * Projects Settings Page + * + * All code for admin interface. + */ +function devshop_projects_settings_form($form, &$form_state) { + $form = array(); + + $form['projects'] = array( + '#type' => 'fieldset', + '#title' => t('Projects'), + ); + $form['projects']['devshop_git_repo_suggestions'] = array( + '#title' => t('Suggested Git Repositories'), + '#description' => t('Enter Git URLs, one per line. When creating a new project, these Git Repositories will available to use as a starting codebase.'), + '#type' => 'textarea', + '#default_value' => implode(PHP_EOL, variable_get('devshop_git_repo_suggestions', array( + 'git@github.com:opendevshop/devshop-composer-template.git' + ))), + '#element_validate' => array('devshop_element_validate_explode_array'), + ); + $form['projects']['devshop_composer_project_suggestions'] = array( + '#title' => t('Suggested Composer Project'), + '#description' => t('Enter composer project names, one per line. These projects will be available to users creating new DevShop Project Git repositories.'), + '#type' => 'textarea', + '#default_value' => implode(PHP_EOL, variable_get('devshop_composer_project_suggestions', array( + 'devshop/composer-template:8.x-dev' + ))), + '#element_validate' => array('devshop_element_validate_explode_array'), + ); + + $form['paths'] = array( + '#type' => 'fieldset', + '#title' => t('Paths'), + ); + $form['paths']['devshop_projects_allow_custom_code_path'] = array( + '#title' => t('Allow custom code path per project.'), + '#description' => t('Allow each project to have a custom "Code Path". If not checked, project paths are set as "/var/aegir/projects/{PROJECT_NAME}.'), + '#type' => 'checkbox', + '#default_value' => variable_get('devshop_projects_allow_custom_code_path', FALSE), + ); + $form['paths']['devshop_project_base_path'] = array( + '#title' => t('Projects Base Path'), + '#type' => 'textfield', + '#description' => t('The default base path that all projects will be created in. Projects each get their own folder inside this path.'), + '#default_value' => variable_get('devshop_project_base_path', '/var/aegir/projects'), + ); + $form['paths']['devshop_project_default_drupal_path'] = array( + '#title' => t('Default Document Root'), + '#type' => 'textfield', + '#description' => t("If index.php isn't in the root of the git repo, you can edit the 'Path to Drupal' setting on each project. Set a default 'Path to Drupal' here. (For example, an Acquia hosted repo uses 'docroot'.)"), + '#default_value' => variable_get('devshop_project_default_drupal_path', ''), + ); + + + $form['urls'] = array( + '#type' => 'fieldset', + '#title' => t('URLs'), + ); + + $form['urls']['devshop_projects_allow_changing_project_git_url'] = array( + '#title' => t("Allow changing a project's Default Git URL."), + '#description' => t("When editing a project, do not allow changes to the Git URL."), + '#type' => 'checkbox', + '#default_value' => variable_get('devshop_projects_allow_changing_project_git_url', TRUE), + ); + $form['urls']['devshop_projects_allow_custom_base_url'] = array( + '#title' => t('Allow custom Environment Domain Name Pattern per project.'), + '#description' => t('If enabled each project can set an Environment URL Pattern.'), + '#type' => 'checkbox', + '#default_value' => variable_get('devshop_projects_allow_custom_base_url', FALSE), + ); + $form['urls']['devshop_project_environment_url_pattern'] = array( + '#title' => t('Environment Domain Name Pattern'), + '#type' => 'textfield', + '#description' => t("Each environment will have a system domain name generated for it based on it's name. Use @project for project name, @hostname for '%host', @environment for the environment's name.", array('%host' => $_SERVER['SERVER_NAME'])), + '#default_value' => variable_get('devshop_project_environment_url_pattern', '@project.@environment.@hostname'), + '#element_validate' => array( + 'devshop_project_settings_validate_environment_url_pattern', + ), + ); + +// $form['support'] = array( +// '#type' => 'fieldset', +// '#title' => t('DevShop Support'), +// ); +// $form['support']['devshop_support_widget_enable'] = array( +// '#title' => t('Show Help Widget'), +// '#description' => t('Uncheck this box if you want to hide the Help widget that appears at the bottom right of the page.'), +// '#type' => 'checkbox', +// '#default_value' => variable_get('devshop_support_widget_enable', TRUE), +// ); + + // Server settings. + $form['server'] = array( + '#type' => 'fieldset', + '#title' => t('DevShop Server'), + ); + $form['server']['devshop_public_key'] = array( + '#title' => t('Aegir User Public Key'), + '#description' => t('The public key of the aegir user on this server. If you change the SSH keys located at /var/aegir/.ssh/id_rsa.pub, then you should update this variable. This variable is for user reference only. If using GitHub API, then devshop will check to make sure the server has access using this public key.'), + '#type' => 'textarea', + '#default_value' => variable_get('devshop_public_key', ''), + ); + return system_settings_form($form); +} + +/** + * Validate that the default environment domain pattern has @environment and @project. + */ +function devshop_project_settings_validate_environment_url_pattern($element, &$form_state, $form) { + if (strpos($form_state['values']['devshop_project_environment_url_pattern'], '@environment') === FALSE || strpos($form_state['values']['devshop_project_environment_url_pattern'], '@project') === FALSE) { + form_error($element, t('The placeholders @project and @environment must be in the Environment Domain Name Pattern.')); + } +} \ No newline at end of file diff --git a/modules/devshop/devshop_projects/inc/create-wizard.inc b/modules/devshop/devshop_projects/inc/create-wizard.inc new file mode 100644 index 000000000..a4abe2daf --- /dev/null +++ b/modules/devshop/devshop_projects/inc/create-wizard.inc @@ -0,0 +1,2 @@ + NULL, + ); + + // Setup project + $project = ctools_object_cache_get('project', 'devshop_project'); + + // Setup Step. + if ($step == NULL) { + drupal_goto('projects/add/' . current(array_keys($form_info['order']))); + } + + // Create default project object + if (empty($project) || !isset($project->git_url)) { + // set form to first step -- we have no data + $step = current(array_keys($form_info['order'])); + $project = new stdClass(); + $project->step = $step; + $project->git_url = ''; + $project->nid = NULL; + $project->name = ''; + + // These are empty until we have our project name + $project->code_path = ''; + $project->base_url = ''; + + // ** set the storage object so its ready for whatever comes next + ctools_object_cache_set('project', 'devshop_project', $project); + } + else { + // Load latest project object from database and use that for the ctools cache. + $project_node = node_load($project->nid); + $project = $project_node->project; + + // If we have a name but no code_path or base URL. + if (!empty($project->name) && empty($project->code_path)) { + $project->code_path = variable_get('devshop_project_base_path', '/var/aegir/projects') . '/' . $project->name; + } + if (!empty($project->name) && empty($project->base_url)) { + $project->base_url = variable_get('devshop_project_environment_url_pattern', "@project.@environment.@hostname"); + } + + // Save the current step + $project->step = $step; + ctools_object_cache_set('project', 'devshop_project', $project); + } + + // Check verification status + if (!empty($project->nid)) { + $tasks = hosting_task_fetch_tasks($project->nid); + } + if (isset($tasks['verify']['nid'])) { + // Get "verify" task status for the project + $project->verify_task_status = isset($tasks['verify']['task_status']) ? $tasks['verify']['task_status'] : HOSTING_TASK_ERROR; + $project->verify_task_nid = $tasks['verify']['nid']; + } + else { + $project->verify_task_status = NULL; + $project->verify_task_nid = NULL; + } + + // If project verification failed, we need to ask for a new git url. + if ($project->verify_task_status == HOSTING_TASK_ERROR && !empty($project_node->nid)) { + $messages = db_query("SELECT * FROM {hosting_task_log} WHERE nid = :nid AND type LIKE :type ORDER BY vid, lid", array(':nid' => $project->verify_task_nid, ':type' => 'p_%')); + + foreach ($messages as $message) { + if ($message->type == 'p_info') { + $messages_to_user[] = $message->message; + } + } + $project->verify_error = theme('devshop_ascii', array('output' => implode('\n', $messages_to_user))); + + + // If not on the first step, go to it. + if ($step != current(array_keys($form_info['order']))) { + drupal_goto('projects/add/' . current(array_keys($form_info['order']))); + } + } + else { + $project->verify_error = NULL; + } + + // All forms can access $form_state['project']; + $form_state['project'] = $project; + + // Saving the last visited step for redirects from node + $_SESSION['last_step'] = $step; + + // Generate our ctools form and output + $output = ctools_wizard_multistep_form($form_info, $step, $form_state); + return $output; +} + +/** + * The form_info for the ctools wizard + */ +function devshop_projects_create_wizard_info() { + return array( + 'id' => 'devshop_project_create', + 'path' => "projects/add/%step", + 'show trail' => TRUE, + 'show back' => TRUE, + 'show cancel' => TRUE, + 'show return' => FALSE, + 'next text' => 'Next', + 'next callback' => 'devshop_projects_create_wizard_next', + 'finish callback' => 'devshop_projects_create_wizard_finish', + 'cancel callback' => 'devshop_projects_create_wizard_cancel', + 'order' => array( + 'git_url' => t('Step 1: Source Code'), + 'settings' => t('Step 2: Settings'), + 'environments' => t('Step 3: Environments'), + 'sites' => t('Step 4: Install Profile'), + ), + 'forms' => array( + 'git_url' => array( + 'form id' => 'devshop_project_create_step_git' + ), + 'settings' => array( + 'form id' => 'devshop_project_create_step_settings' + ), + 'environments' => array( + 'form id' => 'devshop_project_create_step_environments' + ), + 'sites' => array( + 'form id' => 'devshop_project_create_step_sites' + ), + ), + ); +} + +/** + * WIZARD TOOLS + */ + + +/** + * NEXT callback + * Saves anything in $form_state['project'] to ctools cache. + * + * The form submit callbacks are responsible for putting data into + * $form_state['project']. + */ +function devshop_projects_create_wizard_next(&$form_state) { + $project = &$form_state['project']; + $cache = ctools_object_cache_set('project', 'devshop_project', $project); +} + + + + +/** + * CANCEL callback + * Callback generated when the 'cancel' button is clicked. + * Remove the project data cache and send back to projects page. + */ +function devshop_projects_create_wizard_cancel(&$form_state) { + // Update the cache with changes. + $project = &$form_state['project']; + ctools_object_cache_clear('project', 'devshop_project'); + + // Redirect to projects list + $form_state['redirect'] = 'projects'; + + // If we have a project node, create a "delete" hosting task + if (!empty($project->nid)) { + hosting_add_task($project->nid, 'delete'); + } + + // Message + drupal_set_message(t('Project creation cancelled.')); + + // Removing last step session variable. + unset($_SESSION['last_step']); + +} + +/** + * FINISH callback + * Callback generated when the add page process is finished. + */ +function devshop_projects_create_wizard_finish(&$form_state) { + + $project = &$form_state['project']; + $project_node = node_load($project->nid); + + // Save the extra options to the project node. + $project_node->project->install_profile = $form_state['values']['install_profile']; + + // Create the site nodes, saving to the environment. + // @TODO: Can we speed things up here by only running install for the first, + // then "Cloning" to create the rest? + foreach ($project_node->project->environments as $environment_name => &$environment) { + // @TODO: Does this set the http_server as well?? Doesn't look like it. + $db_server = $environment->db_server_nid; + $site_node = devshop_projects_create_site($project_node->project, node_load($environment->platform), $environment_name, $db_server); + $environment->site = $site_node->nid; + } + + // Set to not verify and to publish. + $project_node->no_verify = TRUE; + $project_node->status = 1; + node_save($project_node); + + ctools_object_cache_clear('project', 'devshop_project'); + $form_state['redirect'] = 'node/' . $project_node->nid; + + // Removing last step session variable. + unset($_SESSION['last_step']); + + // Friendly message + drupal_get_messages('status'); + drupal_set_message(t('Your project has been created. Your sites are being installed.')); +} + +/** + * Returns JSON showing the state of the project + */ +function devshop_projects_add_status($type = 'platform') { + $return = array(); + + // Get Project Cache + ctools_include('wizard'); + ctools_include('object-cache'); + $project = ctools_object_cache_get('project', 'devshop_project'); + + + $project_node = node_load($project->nid); + + $all_tasks_completed = TRUE; + $nids = array(); + + // When checking project... + if ($type == 'project') { + $nids = array($project_node->nid); + $tasks = hosting_get_tasks(array('rid' => $project_node->nid)); + $last_task = array_shift($tasks); + $status = _hosting_parse_error_code($last_task->task_status); + + $return['tasks'][$last_task->nid] = array( + 'status' => $last_task->task_status, + ); + + $return['tasks_complete'] = $last_task->task_status == HOSTING_TASK_SUCCESS || $last_task->task_status == HOSTING_TASK_ERROR || $last_task->task_status == HOSTING_TASK_WARNING; + drupal_json_output($return); + exit; + } + + // When checking platforms... + if ($type == 'platform') { + foreach ($project_node->project->environments as $name => $environment) { + $nids[] = $environment->platform; + } + } + + // Check verification task for all nids + foreach ($nids as $nid) { + $platform = node_load($nid); + $profiles_shortnames = hosting_get_profiles($platform->nid, 'short_name'); + if (is_array($profiles_shortnames) && !empty($profiles_shortnames)) { + $install_profiles = array_combine($profiles_shortnames, (array) hosting_get_profiles($platform->nid)); + } + else { + $install_profiles = array(); + } + + $status = _hosting_parse_error_code($platform->environment->last_task_node->task_status); + if ($status == 'Queued') { + $status = '...'; + } + $return['tasks'][$nid] = array( + 'status' => $status, + 'version' => !empty($platform->release->version)? $platform->release->version: '', + 'profiles' => implode(', ', $install_profiles), + ); + // If task is not completed, mark all tasks not complete. + if ($platform->environment->last_task_node->task_status == HOSTING_TASK_SUCCESS || $platform->environment->last_task_node->task_status == HOSTING_TASK_ERROR || $platform->environment->last_task_node->task_status == HOSTING_TASK_WARNING) { + continue; + } + else { + $all_tasks_completed = FALSE; + } + + } + $return['tasks_complete'] = $all_tasks_completed; + drupal_json_output($return); + exit; +} + +/** + * Helper for adding reloading feature to form + */ +function devshop_form_reloader(&$form, $type = 'platform') { + // Add JS that reloads the page when tasks finish. + $form['item'] = array( + '#type' => 'item', + '#value' => '', + '#weight' => 10 + ); + $settings['devshopReload'] = array( + 'type' => $type, + 'delay' => 1000, + ); + + drupal_add_js($settings, array('type' => 'setting')); + drupal_add_js(drupal_get_path('module', 'devshop_projects') . '/inc/reload.js'); +} diff --git a/modules/devshop/devshop_projects/inc/create/create.js b/modules/devshop/devshop_projects/inc/create/create.js new file mode 100644 index 000000000..fc750ea3a --- /dev/null +++ b/modules/devshop/devshop_projects/inc/create/create.js @@ -0,0 +1,99 @@ +/** + * Start New Project: + * + * Step 1: Takes the entered name and creates a base url and code path from it. + */ +(function ($) { + + Drupal.devshopFilterProjectName = function (inputElement) { + return inputElement.val().split("/").pop().replace('.git', '').replace(/[^a-z0-9]/gi, '').toLowerCase() + } + + Drupal.devshopSetProjectName = function (inputElement) { + var fixedProjectName = Drupal.devshopFilterProjectName(inputElement); + if (fixedProjectName != '') { + $('#edit-title').val(fixedProjectName); + } + } + + Drupal.behaviors.devshopSourceSelect = { + attach: function (context, settings) { + Drupal.settings.devshop.projectNameSourceElements.forEach(function( selector ) { + + if ($(selector).prop("tagName") == 'SELECT') { + var eventName = 'change'; + } + else { + var eventName = 'keyup'; + } + + $(selector).bind(eventName, function(event) { + Drupal.devshopSetProjectName($(this)); + }); + }); + } + }; + + Drupal.behaviors.devshopShowSelect = { + attach: function (context, settings) { + $('a.put-back-suggested-projects').click(function(e) { + e.preventDefault(); + var optionVal = $('#edit-settings-github-repository-source-composer-project-suggestions option').val(); + $('#edit-settings-github-repository-source-composer-project-suggestions').val(optionVal).change(); + }); + } + } + Drupal.behaviors.devshopShowSelectRepos = { + attach: function (context, settings) { + $('a.put-back-suggested-repos').click(function(e) { + e.preventDefault(); + var optionVal = $('#edit-settings-github-repository-source-import-suggestions option').val(); + $('#edit-settings-github-repository-source-import-suggestions').val(optionVal).change(); + }); + } + } + + Drupal.behaviors.autoSelect = { + attach: function (context, settings) { + $('select#edit-settings-github-repository-source-composer-project-suggestions').change(function(e) { + if ($(this).val() == 'custom') { + $('#edit-settings-github-repository-source-composer-project').select(); + } + + }); + $('select#edit-settings-github-repository-source-import-suggestions').change(function(e) { + if ($(this).val() == 'custom') { + $('#edit-settings-github-repository-source-import').select(); + } + + }); + } + } + +}(jQuery)); + +/** + * Step 2: Settings + */ +(function ($) { + Drupal.behaviors.createStep2 = { + attach: function (context, settings) { + + // Hide unless + $('#edit-project-settings-live-live-domain-www-wrapper').hide(); + $('#edit-project-settings-live-environment-aliases-wrapper').hide(); + + $('#edit-project-settings-live-live-domain').keyup(function(){ + if ($(this).val()){ + $('#edit-project-settings-live-live-domain-www-wrapper').show(); + $('#edit-project-settings-live-environment-aliases-wrapper').show(); + } + else { + $('#edit-project-settings-live-live-domain-www-wrapper').hide(); + $('#edit-project-settings-live-environment-aliases-wrapper').hide(); + } + }); + + } + }; +}(jQuery)); diff --git a/modules/devshop/devshop_projects/inc/create/step-1.inc b/modules/devshop/devshop_projects/inc/create/step-1.inc new file mode 100644 index 000000000..49dbc7719 --- /dev/null +++ b/modules/devshop/devshop_projects/inc/create/step-1.inc @@ -0,0 +1,334 @@ +verify_error) { + $form['note'] = array( + '#markup' => t('Something went wrong. Check the errors and settings and try again.'), + '#prefix' => '
', + '#suffix' => '
', + '#weight' => -999, + ); + $form['error'] = array( + '#markup' => $project->verify_error, + '#weight' => -998, + ); + + // Check for "host key" + if (strpos($project->verify_error, 'Host key verification failed')) { + $form['help'] = array( + '#markup' => t('Looks like you need to authorize this host. SSH into the server as aegir user and run the command git ls-remote !repo.
Add StrictHostKeyChecking no to your ~/.ssh/config file to avoid this for all domains in the future.', array( + '!repo' => $project->git_url, + )), + '#prefix' => '
', + '#suffix' => '
', + ); + } + } + + if (empty($project->name)) { + $form['title'] = array( + '#type' => 'textfield', + '#title' => t('Project Code Name'), + '#required' => TRUE, + '#suffix' => '

' . t('Choose a unique name for your project. Only lowercase letters and numbers are allowed. No punctuation, spaces or special characters.') . '

', + '#size' => 40, + '#maxlength' => 255, + '#attributes'=> array( + 'placeholder' => t('myproject') + ), + '#weight' => 1, + ); + } + else { + $form['title'] = array( + '#type' => 'value', + '#value' => $project->name, + ); + $form['title_display'] = array( + '#type' => 'item', + '#title' => t('Project Code Name'), + '#markup' => $project->name, + '#weight' => 1, + ); + } + + $username = variable_get('aegir_user', 'aegir'); + +// +// // Project code path. +// $form['code_path'] = array( +// '#type' => variable_get('devshop_projects_allow_custom_code_path', FALSE) ? 'textfield' : 'value', +// '#title' => t('Code path'), +// '#description' => t('The absolute path on the filesystem that will be used to create all platforms within this project. There must not be a file or directory at this path.'), +// '#required' => variable_get('devshop_projects_allow_custom_code_path', FALSE), +// '#size' => 40, +// '#default_value' => $project->code_path, +// '#maxlength' => 255, +// '#attributes' => array( +// 'data-base_path' => variable_get('devshop_project_base_path', '/var/aegir/projects'), +// ), +// ); +// +// // Project base url +// $form['base_url'] = array( +// '#type' => variable_get('devshop_projects_allow_custom_base_url', FALSE) ? 'textfield' : 'value', +// '#title' => t('Base URL'), +// '#description' => t('All sites will be under a subdomain of this domain.'), +// '#required' => variable_get('devshop_projects_allow_custom_base_url', FALSE), +// '#size' => 40, +// '#default_value' => $project->base_url, +// '#maxlength' => 255, +// '#attributes' => array( +// 'data-base_url' => devshop_projects_url($project->name, ''), +// ), +// ); + + // Display helpful tips for connecting. + $pubkey = variable_get('devshop_public_key', ''); + + // If we don't yet have the server's public key saved as a variable... + if (empty($pubkey)) { + $output = t("This DevShop doesn't yet know your server's public SSH key. To import it, run the following command on your server as aegir user:"); + $command = 'drush @hostmaster vset devshop_public_key "$(cat ~/.ssh/id_rsa.pub)" --yes'; + $output .= "
"; + } + else { + $msg = t('If you haven\'t granted this server access to your Git repository, you should do so now using it\'s public SSH key:'); + + $link = l(t('Edit devshop public key'), 'admin/devshop', array( + 'query' => array( + 'destination' => $_GET['q'], + ), + 'attributes' => array( + 'class' => 'btn btn-default' + ), + )); + $output = <<

$msg

+ $link + +HTML; + } + + // Add info about connecting to Repo + $form['connect'] = array( + '#markup'=> $output, + ); + + $tips[] = t('For best results, use the !link project as a starting point for Composer based projects.', [ + '!link' => l(t('DevShop Composer Template'), 'https://github.com/opendevshop/devshop-composer-template', array( + 'attributes' => array( + 'target' => '_blank', + ) + )), + ]); + + $form['welcome'] = array( + '#markup' => '

' . t("Start a new DevShop Project.") . '

' . t("A project represents your website. DevShop keeps track of the source code for your website so it can launch copies of it quickly.") . '

', + '#prefix' => '
', + '#suffix' => '
', + '#weight' => -1000, + ); + + $options = module_invoke_all('devshop_project_git_repo_options'); + $form['source'] = array( + '#type' => 'container', + '#attributes' => array( + 'class' => array( + 'well' + ), + ), + ); + $form['source']['git_source'] = array( + '#title' => t('Choose the source of your code'), + '#type' => 'radios', + '#options' => $options, + '#description' => t(''), + '#default_value'=> 'custom', + ); + + $form['source']['git_url'] = array( + '#type' => 'textfield', + '#required' => 1, + '#title' => t('Git Repository URL'), + '#default_value' => $project->git_url, + '#maxlength' => 1024, + '#attributes' => array( + 'placeholder' => 'git@githost.com:myteam/myproject.git' + ), + '#states' => array( + 'visible' => array( + ':input[name="git_source"]' => array( + 'value' => 'custom' + ), + ), + ), + ); + + + // We have to tell the front-end what type of field so we can assign the right event. + drupal_add_js(array( + 'devshop' => array( + 'projectNameSourceElements' => array( + '#edit-git-url' + ), + ), + ), 'setting'); + + $tips[] = t('Use the "ssh" url whenever possible. For example: git@github.com:opendevshop/repo.git'); + $tips[] = t('You can use a repository with a full Drupal stack committed, in the root or in a sub folder'); + $tips[] = t('If a composer.json file is found in the root of the project, composer install will be run automatically.'); + $tips[] = t("If your repository is not public, make sure your server has SSH access. Check the !link to see this server's public key.", array( + '!link' => l(t('DevShop Settings'), 'admin/devshop'), + )); + $tips = theme('item_list', array('items' => $tips)); + + + $form['tips'] = array( + '#markup' => $tips, + '#prefix' => '
', + '#suffix' => '
', + '#weight' => 10, + ); + return $form; +} + +/** + * Implements hook_devshop_project_git_repo_options(). + */ +function devshop_projects_devshop_project_git_repo_options() { + return array( + 'custom' => t('Git repository URL'), + ); +} + + +/** + * Force the project to be lowercase and remove any non numbers or letters. + * + * @param $element + * @param $form_state + * @param $form + */ +function devshop_project_wizard_title_validate($element, &$form_state, $form) { + // No spaces or special characters allowed. + // Set the value to our trimmed and lowercased project name. + $project_name = strtolower(preg_replace('/[^a-z0-9]/gi', '', $element['value'])); + form_set_value($form['title'], $project_name, $form_state); +} + +/** + * STEP 1: Validate + */ +function devshop_project_create_step_git_validate(&$form, &$form_state) { + $project = &$form_state['project']; + + if (empty($project->nid)) { + + $project_name = $form_state['values']['title']; // domain names are case-insensitive + + // Check for duplicate project name here. + // hosting_context_load() only works if a node exists with that ID. + if (db_query("SELECT nid FROM {hosting_context} WHERE name = :name", array(':name' => 'project_' . $project_name))->fetch()) { + form_set_error('title', t('The name @project is in use. Please try again.', array( + '@project' => $project_name, + ))); + } + } + +// +// // CODE PATH & BASE URL +// // Code path and base url must be unique +// if (db_query('SELECT n.nid FROM {hosting_devshop_project} d LEFT JOIN {node} n ON d.nid = n.nid WHERE status = :status AND code_path = :code_path;', array(':status' => 1, ':code_path' => $form_state['values']['code_path']))->fetchField()) { +// form_set_error('code_path', t('Another project already has this code path. Code path must be unique.')); +// } +// if (db_query('SELECT n.nid FROM {hosting_devshop_project} d LEFT JOIN {node} n ON d.nid = n.nid WHERE status = :status AND base_url = :base_url;', array(':status' => 1, ':base_url' => $form_state['values']['base_url']))->fetchField()) { +// form_set_error('base_url', t('Another project already has this base url. Base URL must be unique.')); +// } +// +// // If custom code paths are not allowed, set the value here using the project name. +// if (!variable_get('devshop_projects_allow_custom_code_path', FALSE)) { +// form_set_value($form['code_path'], variable_get('devshop_project_base_path', '/var/aegir/projects') . '/' . $project_name, $form_state); +// } +// +// // If custom base URLs are not allowed, set the value here using the project name. +// if (!variable_get('devshop_projects_allow_custom_base_url', FALSE)) { +// form_set_value($form['base_url'], devshop_projects_url($project_name), $form_state); +// } +} + +/** + * STEP 1: Submit + */ +function devshop_project_create_step_git_submit(&$from, &$form_state) { + global $user; + + $project = &$form_state['project']; + + // If the project already exists, this means the git url has changed... + if ($project->nid) { + // Change the git url and save the node. Verification will run again. + $node = node_load($project->nid); + $node->project->git_url = $form_state['values']['git_url']; + $node->project->code_path = $form_state['values']['code_path']; + $node->project->base_url = $form_state['values']['base_url']; + + $project->settings->create_project_settings = $form_state['values']['settings']; + + node_save($node); + } + // Create new project node + elseif (empty($project->nid)) { + // Create the project node now. We will update it with the chosen path. + // This is so we can check for branches and save the hosting_context as soon + // as possible. + $node = new stdClass; + $node->title = $form_state['values']['title']; + + $node->type = 'project'; + $node->status = 0; + $node->uid = $user->uid; + $node->name = $user->name; + + // Create project object + $project->name = $form_state['values']['title']; + $project->git_url = $form_state['values']['git_url']; + $project->code_path = $form_state['values']['code_path']; + $project->base_url = $form_state['values']['base_url']; + + $project->settings->create_project_settings = $form_state['values']['settings']; + + // @TODO: We will clone the code for step 2 and look for drupal. + $project->drupal_path = variable_get('devshop_projects_default_drupal_path', ''); + + // Attach to node + $node->project = $project; + + // Save project node, triggering verification. + if ($node = node_submit($node)) { + node_save($node); + } + + // Save NID to ctools object cache. + if ($node->nid) { + $project->nid = $node->nid; + } + } + + // Remove default "task" messages. + drupal_get_messages(); +} diff --git a/modules/devshop/devshop_projects/inc/create/step-2.inc b/modules/devshop/devshop_projects/inc/create/step-2.inc new file mode 100644 index 000000000..862ab7e9c --- /dev/null +++ b/modules/devshop/devshop_projects/inc/create/step-2.inc @@ -0,0 +1,107 @@ +verify_task_status == HOSTING_TASK_QUEUED || $project->verify_task_status == HOSTING_TASK_PROCESSING) { + $note = '

' . t('Please wait while we connect and analyze your repository.') . '

'; + $note .= '

'; + $note .= '

' . l(t('View Logs'), "node/$project->verify_task_nid", array( + 'attributes' => array( + 'target' => '_blank', + 'class'=> array('btn btn-primary') + ) + )); + $form['note'] = array( + '#markup' => $note, + ); + $form['#no_next'] = TRUE; + + // Add code to reload the page when complete. + devshop_form_reloader($form, 'project'); + return $form; + } + + $node = node_load($project->nid); + + $node_type = 'project'; + $form_id = $node_type . '_node_form'; + $form_state['build_info']['args'] = array($node); + $form_state['build_info']['files'][] = array('inc', 'node', 'node.pages'); + module_load_include('inc', 'node', 'node.pages'); + $project_form = drupal_retrieve_form($form_id, $form_state); + drupal_prepare_form($form_id, $project_form, $form_state); + // Remove node buttons + $actions = $project_form['actions']; + unset($project_form['actions']); + + // Merge project node form into ctools wizard form. + $form = array_merge($project_form, $form); + + // Make sure the project isn't re-verified on this page. + $form['no_verify'] = array( + '#type' => 'value', + '#value' => TRUE, + ); + + // Add additional additional submit and validate handlers for the node form. + $form['buttons']['next']['#submit'] = $actions['submit']['#submit']; + $form['buttons']['next']['#submit'][] = 'ctools_wizard_submit'; + + drupal_add_js(drupal_get_path('module', 'devshop_projects') . '/inc/create/create.js'); + + // Remove "live environment" selector. + $form['project']['codebase']['live_environment']['#type'] = 'value'; + $form['project']['codebase']['live_environment']['#suffix'] = ''; + + return $form; +} + +/** + * STEP 2: Validate + */ +function devshop_project_create_step_settings_validate(&$from, &$form_state) { + // Nothing is needed here. We've replaced our validator with node_form_validate, + // which passes through to devshop_projects_validate() +} + +/** + * STEP 2: Submit + * + * If webroot was updated, update platforms path. + */ +function devshop_project_create_step_settings_submit(&$form, &$form_state) { + + // Update any platforms if the path to drupal changed + if ($form['project']['codebase']['drupal_path']['#default_value'] != $form_state['values']['project']['drupal_path']) { + $environments = db_query(" + SELECT + e.platform, e.name + FROM {hosting_devshop_project_environment} e + WHERE project_nid = :nid + ", array( + ':nid' => $form_state['values']['nid'] + ))->fetchAll(); + foreach ($environments as $environment) { + $platform = node_load($environment->platform); + $platform->publish_path = $form_state['values']['project']['code_path'] . DIRECTORY_SEPARATOR . $environment->name . DIRECTORY_SEPARATOR . $form_state['values']['project']['drupal_path']; + node_save($platform); + } + $count = count($environments); + if ($count) { + drupal_set_message(t('You changed the web document root to %root. @count updated with the new information.', array( + '%root' => $form_state['values']['project']['drupal_path'], + '@count' => format_plural($count, t('1 site was'), t( '@count sites were', array('@count' => $count))), + )), 'success'); + } + } +} diff --git a/modules/devshop/devshop_projects/inc/create/step-3.inc b/modules/devshop/devshop_projects/inc/create/step-3.inc new file mode 100644 index 000000000..aec1d1afa --- /dev/null +++ b/modules/devshop/devshop_projects/inc/create/step-3.inc @@ -0,0 +1,403 @@ +verify_task_status == HOSTING_TASK_QUEUED || $project->verify_task_status == HOSTING_TASK_PROCESSING) { + $note = '

' . t('Please wait while we connect to your repository and determine any branches.') . '

'; + $form['note'] = array( + '#markup' => $note, + ); + $form['#no_next'] = TRUE; + + // Add code to reload the page when complete. + devshop_form_reloader($form, 'project'); + return $form; + } + + $form['nid'] = array( + '#type' => 'value', + '#value' => $project->nid, + ); + $form['project'] = array( + '#tree' => TRUE, + ); + $form['project']['environments'] = array( + '#theme' => 'devshop_projects_create_settings_form', + '#tree' => TRUE, + '#prefix' => '
', + '#suffix' => '
', + ); + + // If there was a problem with loading branches or tags, hide the form and show a link to re-verify. + if (empty($project->settings->git['branches']) && empty($project->settings->git['tags'])) { + $form['error'] = array( + '#markup' => t('Unable to find any branches or tags. Please !verify.', array( + '!verify' => l(t('Re-verify the project'), "hosting_confirm/{$project->nid}/project_verify", array( + 'query' => array('token' => drupal_get_token($user->uid)), + 'attributes' => array( + 'class' => array('btn btn-success'), + ), + )) + )), + '#prefix' => '
', + '#suffix' => '
', + ); + return $form; + } + + // Ensure a blank row exists (happens when using 'Back' button). + if (!isset($project->environments['NEW']) || !is_array($project->environments['NEW'])) { + $project->environments['NEW'] = array(); + } + foreach ($project->environments as $name => $environment) { + // No platforms exist yet + if ($name == 'NEW') { + $env_title = ''; + } + else { + $env_title = $name; + } + + $form['project']['environments'][$name] = array( + '#tree' => TRUE, + '#type' => 'fieldset', + '#theme' => 'devshop_projects_settings_table', + ); + + // Environment properties + // Generate field prefix and suffix using domain name pattern. + if (variable_get('devshop_projects_allow_custom_base_url')) { + $pattern = $project->base_url; + } + else { + $pattern = variable_get('devshop_project_environment_url_pattern', '@project.@environment.@hostname'); + } + + $labels = explode('@environment', strtr($pattern, array( + '@project' => $project->name, + '@hostname' => $_SERVER['SERVER_NAME'], + ))); + $form['project']['environments'][$name]['name'] = array( + '#type' => 'textfield', + '#default_value' => $env_title, + '#size' => 6, + '#maxlength' => 64, + '#attributes' => array( + 'placeholder' => t('name'), + ), + '#field_prefix' => '
http://' . $labels[0] . '
', + '#field_suffix' => '
' . $labels[1] .'
+
', + ); + $form['project']['environments'][$name]['site'] = array( + '#type' => 'value', + '#value' => isset($environment->site) ? $environment->site : NULL, + ); + $form['project']['environments'][$name]['platform'] = array( + '#type' => 'value', + '#value' => isset($environment->platform) ? $environment->platform : NULL, + ); + $form['project']['environments'][$name]['git_ref'] = array( + '#type' => 'select', + '#bootstrap_ignore_pre_render' => TRUE, + '#options' => devshop_projects_git_ref_options($project), + '#default_value' => isset($environment->git_ref) ? $environment->git_ref : 'HEAD', + ); + + // HTTP Server select. + $http_servers = hosting_get_servers('http', FALSE); + + // Determine default web server. + $default_web_server = isset($environment->settings, $environment->settings->web_server) ? + $environment->settings->web_server : + $project->settings->default_environment['web_server']; + + // Fallback if environment or default not found. + if (empty($default_web_server)) { + $default_web_server = key($http_servers); + } + + if (count($http_servers) == 1) { + $form['project']['environments'][$name]['settings']['web_server'] = array( + '#type' => 'value', + '#value' => $default_web_server, + ); + } + elseif (count($http_servers) > 1) { + $form['project']['environments'][$name]['settings']['web_server'] = array( + '#title' => t('Web server'), + '#type' => 'select', + '#options' => $http_servers, + '#default_value' => $default_web_server + ); + } + + // DB Server select. + $db_servers = hosting_get_servers('db', FALSE); + + // Determine default db server. + $default_db_server = isset($environment->settings, $environment->settings->db_server) ? + $environment->settings->db_server : + $project->settings->default_environment['db_server']; + + // Fallback if environment or default not found. + if (empty($default_db_server) || !isset($db_servers[$default_db_server])) { + $default_db_server = key($db_servers); + } + + if (count($db_servers) == 1) { + $form['project']['environments'][$name]['settings']['db_server'] = array( + '#type' => 'value', + '#default_value' => $default_db_server, + ); + } + elseif (count($db_servers) > 1) { + $form['project']['environments'][$name]['settings']['db_server'] = array( + '#title' => t('Database server'), + '#type' => 'select', + '#options' => $db_servers, + '#default_value' => $default_db_server + ); + } + + // Add Environment + $form['add_environment'] = array( + '#type' => 'submit', + '#value' => t('Add environment'), + '#name' => 'add_environment', + '#prefix' => '
', + '#suffix' => '
', + ); + } + $form['project']['environments']['NEW']['name']['#attributes']['autofocus'] = 'autofocus'; + return $form; +} + + +/** + * STEP 3: Validate + */ +function devshop_project_create_step_environments_validate(&$form, &$form_state) { + $project = &$form_state['project']; + + $values = &$form_state['values']; + + if (isset($values['project'])) { + + // Check environment titles + foreach ($values['project']['environments'] as $env => $env_settings) { + // Check for illegal chars + if ($env != 'NEW' && !empty($env_settings['name'])) { + if (!preg_match('!^[a-z0-9_]+$!', $env_settings['name'])) { + $form_item = 'environments][' . $env . '][name'; + form_set_error($form_item, t('The environment name must contain only lowercase letters, numbers, and underscores.')); + } + } + + // Check for unique names. + if ($env == 'NEW' && array_key_exists($env_settings['name'], $values['project']['environments'])) { + $form_item = 'environments][' . $env . '][name'; + form_set_error($form_item, t('The environment name must be unique.')); + } + if ($env != $env_settings['name'] && array_key_exists($env_settings['name'], $values['project']['environments'])) { + $form_item = 'environments][' . $env . '][name'; + form_set_error($form_item, t('The environment name must be unique.')); + } + + } + + // Reject if empty + if (count($values['project']['environments']) < 1) { + if ($form_state['clicked_button']['#name'] == 'add_environment') { + form_set_error('project][environments][NEW][name', t('Name this environment before you add another.')); + } + else { + form_set_error('project][environments][NEW][name', t('You must add at least one environment.')); + } + } + } + + +} + +///** +// * Functionality for add a new environment. +// */ +//function devshop_projects_save_environments($form, &$form_state) { +// +// $environments = $form_state['values']['project']['environments']; +// +// if (!empty($environments['NEW']['name'])) { +// $new_environment_name = $environments['NEW']['name']; +// $environments[$new_environment_name] = $environments['NEW']; +// } +// unset($environments['NEW']); +// +// // Delete all environments +// db_delete('hosting_devshop_project_environment') +// ->condition('project_nid', $form_state['values']['nid']) +// ->execute(); +// +// // Create all environments +// foreach ($environments as $environment) { +// $name = trim($environment['name']); +// if (!empty($name)) { +// $id = db_insert('hosting_devshop_project_environment') +// ->fields(array( +// 'project_nid' => $form_state['values']['nid'], +// 'name' => $environment['name'], +// 'git_ref' => $environment['git_ref'], +// 'platform' => $environment['platform'], +// 'settings' => serialize($environment['settings']), +// )) +// ->execute(); +// } +// } +// +// // Go back to the same page. +// if ($form_state['clicked_button']['#name'] == 'add_environment') { +// drupal_goto('projects/add/environments'); +// } +//} + +/** + * STEP 3: SUBMIT + */ +function devshop_project_create_step_environments_submit(&$form, &$form_state) { + // Get project and reset properties.. + $project_node = node_load($form_state['values']['nid'], NULL, TRUE); + $project = $project_node->project; + if (isset($form_state['project']->code_path) && empty($project->code_path)) { + $project->code_path = $form_state['project']->code_path; + } + if (isset($form_state['project']->drupal_path) && empty($project->drupal_path)) { + $project->drupal_path = $form_state['project']->drupal_path; + } + + // Save environments data + $environments = $form_state['values']['project']['environments']; + if (!empty($environments['NEW']['name'])) { + $new_environment_name = $environments['NEW']['name']; + $environments[$new_environment_name] = $environments['NEW']; + } + unset($environments['NEW']); + + foreach ($environments as $name => $environment) { + if ($name != $environment['name']) { + $environments[$environment['name']] = $environment; + unset($environments[$name]); + } + } + + // Delete all existing environments + db_delete('hosting_devshop_project_environment') + ->condition('project_nid', $project_node->nid) + ->execute(); + + // Create all environments + foreach ($environments as $name => $environment) { + $environment = (object) $environment; + $environment->settings = (object) $environment->settings; + $name = trim($environment->name); + + if (!empty($name)) { + $id = db_insert('hosting_devshop_project_environment') + ->fields(array( + 'project_nid' => $form_state['values']['nid'], + 'name' => $environment->name, + 'platform' => $environment->platform, + 'settings' => serialize($environment->settings), + )) + ->execute(); + } + + // Create platform node if we don't have one. + if (empty($environment->platform)) { + //If platform hasn't been created yet, build platform node + $platform = new stdClass; + $platform->type = 'platform'; + $platform->title = $project->name . '_' . $name; + + // Platform publish_path + + // If drupal path is set, repo path and platform path are different. Append drupal_path to repo path to create publish path. + if ($project->drupal_path) { + $platform->git['repo_path'] = $project->code_path . '/' . $environment->name; + $platform->publish_path = $project->code_path . '/' . $environment->name . '/' . $project->drupal_path; + } + else { + $platform->git['repo_path'] = $project->code_path . '/' . $environment->name; + $platform->publish_path = $project->code_path . '/' . $environment->name; + } + + // Git information + $platform->git['repo_url'] = $project->git_url; + $platform->git['git_ref'] = $environment->git_ref; + + // Other attributes + $platform->web_server = $environment->settings->web_server; + $platform->db_server = $environment->settings->db_server; + + // Save the node + global $user; + node_object_prepare($platform); + $platform->language = LANGUAGE_NONE; // Or e.g. 'en' if locale is enabled + $platform->uid = $user->uid; + $platform->status = 1; + $platform->promote = 0; + $platform->comment = 0; + $platform = node_submit($platform); + node_save($platform); + + if (!$platform->nid) { + form_set_error('projects', t('An unknown problem has occured: the platform node was not created for the %env environment.', array('%env' => $environment->name))); + } + $environment->platform = $platform->nid; + + // Update environment with our platform + db_update('hosting_devshop_project_environment') + ->fields(array( + 'platform' => $environment->platform, + )) + ->condition('project_nid', $form_state['values']['nid']) + ->condition('name', $environment->name) + ->execute(); + } + } + + // Go back to the same page. + if ($form_state['clicked_button']['#name'] == 'add_environment') { + drupal_goto('projects/add/environments'); + } + + // For all removed platforms, trigger a delete task + $removed_environments = array_diff_key($project->environments, $environments); + foreach ($removed_environments as $environment_name => $settings) { + $platform_nid = $settings->platform; + $platform_node = node_load($platform_nid); + + // Determine what to do here based on task status... + // if verify task hasn't even run yet (and has never run) we can just delete + // the platform node. + if (empty($platform_node->verified)) { + node_delete($platform_node); + } + // Create 'delete' task for the removed platform + else if ($platform_nid) { + hosting_add_task($platform_nid, 'delete'); + } + } + + + // Remove default "task" messages. + drupal_set_message('Environments saved! Now preparing codebases...'); +} diff --git a/modules/devshop/devshop_projects/inc/create/step-4.inc b/modules/devshop/devshop_projects/inc/create/step-4.inc new file mode 100644 index 000000000..0d80d4614 --- /dev/null +++ b/modules/devshop/devshop_projects/inc/create/step-4.inc @@ -0,0 +1,263 @@ + 'value', + '#value' => $project->nid, + ); + + // Display the platforms + $rows = array(); + $header = array( + t('Name'), + t('Branch'), + t('Version'), + t('Install Profiles'), + t('Status') + ); + $all_tasks_queued = TRUE; + $all_tasks_succeeded = TRUE; + $modals = ''; + foreach ($project->environments as $name => $environment) { + + // Get platform and latest verify task. + $platform_nid = $environment->platform; + $platform = node_load($platform_nid); + $tasks = devshop_get_tasks($environment, 'verify'); + $task = array_shift($tasks['verify']); + + if (empty($task)) { + $row = array( + $environment->name, + $environment->git_ref, + t('Error: Platform node was not saved. Unable to continue.'), + '', '', + ); + $rows[] = $row; + continue; + } + + // Build a table. + $row = array(); + $row['name'] = $name; + $row['branch'] = $environment->git_ref; + $row['version'] = t('...'); + + // If platform verified successfully: + if ($task->task_status == HOSTING_TASK_SUCCESS || $task->task_status == HOSTING_TASK_WARNING) { + + // It's not really ready until we get a version. + if (empty($platform->release->version)) { + $completed = FALSE; + $row['version'] = '...'; + $row['profiles'] = '...'; + } + else { + $row['version'] = $platform->release->version; + } + + // Collect install profiles + $profiles_shortnames = hosting_get_profiles($platform->nid, 'short_name'); + + if (is_array($profiles_shortnames) && !empty($profiles_shortnames)) { + $profiles[$name] = array_combine($profiles_shortnames, (array) hosting_get_profiles($platform->nid)); + $row['profiles'] = implode(', ', $profiles[$name]); + } + else { + $profiles[$name] = array(); + } + + // If no tasks have failed, save available profiles + if ($all_tasks_succeeded) { + if (empty($available_profiles)) { + $available_profiles = $profiles[$name]; + } + else { + $available_profiles = array_intersect_key($available_profiles, $profiles[$name]); + } + } + } + // If platform verification failed: + elseif ($task->task_status == HOSTING_TASK_ERROR) { + $completed = TRUE; + $all_tasks_succeeded = FALSE; + $available_profiles = array(); + + $errors = db_query("SELECT * FROM {hosting_task_log} WHERE nid = :nid AND (type LIKE :type OR type = :error) ORDER BY vid, lid", array(':nid' => $task->nid, ':type' => 'devshop%', ':error' => 'error')); + + foreach ($errors as $error) { + if ($error->type == 'p_command' || $error->type == 'error') { + $messages_to_user[] = $error->message; + } + $messages[] = $error; + } + $errors_rendered = theme('devshop_ascii', array('output' => implode('\n', $messages_to_user))); + + $codebase_verification_failed = t('Codebase Verification Failed'); + $close = t('Close'); + $view_logs = t('View Logs'); + $logs_url = url("node/{$task->nid}"); + + $button = << + + $codebase_verification_failed + +HTML; + + $modals .= << + +HTML; + + $row['version'] = array( + 'data' => $button, + 'colspan' => 2, + ); + } + // If platform is still processing: + elseif ($task->task_status == HOSTING_TASK_PROCESSING || $task->task_status == HOSTING_TASK_QUEUED) { + $completed = FALSE; + $row['version'] = "..."; + $row['profiles'] = "..."; + } + + // If a single task is not queued, $all_tasks_queued == FALSE + if ($task->task_status != HOSTING_TASK_QUEUED) { + $all_tasks_queued = FALSE; + } + + // If a single task is not queued, $all_tasks_queued == FALSE + if ($task->task_status == HOSTING_TASK_ERROR) { + $task_failed = TRUE; + } + + // Add hosting task status. + $status = _hosting_parse_error_code($task->task_status); + if ($status == 'Queued') { + $status = '...'; + } + $row['status'] = l("{$status}", "node/{$task->nid}", array('html' => TRUE)); + + // Store rows for display + $rows[] = $row; + } // end foreach platform + + // Output our table. + $form['platforms'] = array( + '#markup' => theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('class' => array('table')))), + ); + + // Not completed means show all tasks are not completed (or errored) + if (!$completed) { + $form['#no_finish'] = TRUE; + $note = '

' . t('Please wait while we clone your repo and verify your drupal code.') . '

'; + $note .= '

'; + + $form['help'] = array( + '#markup' => $note, + ); + + // Add code to reload the page when complete. + devshop_form_reloader($form, 'platform'); + return $form; + } + elseif ($task_failed) { + $project->no_finish = TRUE; + $form['error'] = array( + '#prefix' => '

', + '#markup' => t('Something went wrong when trying to clone your codebase.'), + '#suffix' => '

', + ); + $form['retry'] = array( + '#type' => 'submit', + '#value' => t('Retry'), + '#prefix' => '
', + '#suffix' => '
', + ); + $form['#no_finish'] = TRUE; + } + + // If ready... + else { + + // If no common profiles found, just set to standard. + if (count($available_profiles) == 0) { + $available_profiles['standard'] = 'No default profile.'; + $default_profile = 'standard'; + } + + $project->no_finish = FALSE; + + // Install Profile + // Sensible default? + // Lets go with standard for now... we can update later. + if (isset($available_profiles['standard'])) { + $default_profile = 'standard'; + } + // If 'drupal' profile exists, it is likely drupal6! + elseif (isset($available_profiles['drupal'])) { + $default_profile = 'drupal'; + } + + $form['install_profile'] = array( + '#type' => 'radios', + '#options' => $available_profiles, + '#title' => t('Project Install Profile'), + '#required' => 1, + '#description' => t('Choose the default installation profile for this project.'), + '#default_value' => $default_profile, + ); + } + + $form['#suffix'] = $modals; + return $form; +} + +/** + * STEP 4: Validate + */ +function devshop_project_create_step_sites_validate(&$from, &$form_state) { + if ($form_state['triggering_element']['#value'] == t('Retry')) { + foreach ($form_state['project']->environments as $name => $environment) { + $task = current($environment->tasks['verify']); + if (isset($task->nid)) { + hosting_task_retry($task->nid); + } + } + } + elseif (empty($form_state['values']['install_profile'])) { + form_set_error('install_profile', t('You must choose an install profile. Please wait for all environments to verify.')); + } +} diff --git a/modules/devshop/devshop_projects/inc/drush.inc b/modules/devshop/devshop_projects/inc/drush.inc new file mode 100644 index 000000000..f807a7c90 --- /dev/null +++ b/modules/devshop/devshop_projects/inc/drush.inc @@ -0,0 +1,45 @@ +environments as $name => $environment) { + + $remote_user = 'aegir'; + $remote_host = $environment->remote_host; + $root = $environment->root; + $uri = $environment->uri; + $file_path = "sites/{$environment->uri}/files"; + + $output .= << '$root', + 'uri' => '$uri', + 'remote-user' => '$remote_user', + 'remote-host' => '$remote_host', + 'path-aliases' => array( + '%files' => '$file_path', + ), +); + +PHP; + } + + return $output; +} + + +/** + * Downloads the drush aliases for this site. + * @param $project + */ +function devshop_project_drush_aliases_page($node) { + $project = $node->project; + $filename = $project->name . '.aliases.drushrc.php'; + header("Content-Disposition: attachment; filename='$filename'"); + print devshop_project_aliases($project); +} diff --git a/modules/devshop/devshop_projects/inc/forms.inc b/modules/devshop/devshop_projects/inc/forms.inc new file mode 100644 index 000000000..773e1a59c --- /dev/null +++ b/modules/devshop/devshop_projects/inc/forms.inc @@ -0,0 +1,1755 @@ +project; + + // Save last project data + $form['old'] = array( + '#value' => $node, + '#type' => 'value', + ); + + // Project Settings + // Every value under $form['project'] gets serialized and saved into a project's "data" column. + $form['project'] = array( + '#tree' => TRUE, + ); + + // Hidden fields that can't change. + $form['title'] = array( + '#type' => 'textfield', + '#title' => t('Project Code Name'), + '#required' => TRUE, + '#description' => t('Choose a unique name for your project.'), + '#size' => 40, + '#default_value' => $node->title, + '#maxlength' => 255, + ); + $form['project']['codebase'] = array( + '#type' => 'fieldset', + '#title' => t('General Settings'), + '#group' => 'project_settings', + '#tree' => FALSE, + '#weight' => -11, + ); + $form['project']['codebase']['git_url'] = array( + '#type' => 'textfield', + '#title' => t('Default Git Repository URL'), + '#description' => t('The SSH or HTTP url for the git repository to use for new environments. Changing this will not change the git remote for existing environments.'), + '#default_value' => $project->git_url, + '#parents' => array( + 'project', + 'git_url', + ), + ); + $form['project']['codebase']['code_path'] = array( + '#type' => 'textfield', + '#title' => t('Base path'), + '#description' => t('All environments in this project will be installed inside this path. Changing this will only affect new environments. Existing environments will not change.'), + '#required' => TRUE, + '#size' => 40, + '#default_value' => $project->code_path, + '#maxlength' => 255, + '#weight' => 2, + '#parents' => array( + 'project', + 'code_path', + ), + ); + + $items = []; + $items[] = t('Enter the relative path to the exposed document root within your repository. Leave blank if index.php is in the root. Common paths are docroot or web.'); + $items[] = t('If you are using the drupal-composer project for Drupal 8, the default document root is "web".'); + + $form['project']['codebase']['drupal_path'] = array( + '#type' => 'textfield', + '#title' => t('Document Root'), + '#description' => theme('item_list', ['items' => $items]), + '#size' => 40, + '#default_value' => $project->drupal_path, + '#maxlength' => 255, + '#weight' => 3, + '#parents' => array( + 'project', + 'drupal_path', + ), + ); +// This was accidentally merged from the makefiles branch. +// $form['project']['codebase']['makefile_path'] = array( +// '#type' => 'textfield', +// '#title' => t('Path to Makefile'), +// '#description' => t('Enter the relative path to a makefile inside your repository. Leave blank if a full Drupal codebasee is in the repository.'), +// '#size' => 40, +// '#default_value' => $project->settings->makefile_path, +// '#maxlength' => 255, +// '#weight' => 4, +// '#parents' => array( +// 'project', +// 'settings', +// 'makefile_path', +// ), +// ); + $form['project']['codebase']['base_url'] = array( + '#type' => 'textfield', + '#title' => t('Environment Domain Name Pattern'), + '#description' => t("Each environment will have a system domain name generated for it based on it's name. Use @project for project name, @hostname for '%host', @environment for the environment's name.", array('%host' => $_SERVER['SERVER_NAME'])) . '' . t('Changing this will only affect new environments.'). '', + '#required' => TRUE, + '#size' => 40, + '#default_value' => $project->base_url, + '#maxlength' => 255, + '#weight' => 4, + '#access' => variable_get('devshop_projects_allow_custom_base_url', FALSE), + '#parents' => array( + 'project', + 'base_url', + ), + ); + + // Don't allow editing + if ($node->nid) { + + // Title + $form['title']['#value'] = $form['title']['#default_value']; + $form['title']['#type'] = 'value'; + + } + + // Prevent editing code path unless allowed. + if (!variable_get('devshop_projects_allow_custom_code_path', FALSE)) { + $form['project']['codebase']['code_path']['#type'] = 'value'; + $form['project']['codebase']['code_path']['#value'] = $form['project']['codebase']['code_path']['#default_value']; + } + + // Prevent editing git url path unless allowed. + if (!variable_get('devshop_projects_allow_changing_project_git_url', TRUE)) { + $form['project']['codebase']['git_url']['#type'] = 'value'; + $form['project']['codebase']['git_url']['#value'] = $form['project']['codebase']['git_url']['#default_value']; + } + + // Project Settings + $form['project']['settings'] = array( + '#weight' => -10, + ); + + // Project Settings Vertical Tabs + $form['project_settings'] = array( + '#type' => 'vertical_tabs', + '#weight' => -11, + ); + + // Save git branches and tags + $form['project']['settings']['git']['branches'] = array( + '#type' => 'value', + '#value' => isset($project->settings->git['branches']) ? $project->settings->git['branches'] : NULL, + ); + $form['project']['settings']['git']['tags'] = array( + '#type' => 'value', + '#value' => isset($project->settings->git['tags']) ? $project->settings->git['tags'] : NULL, + ); + + // Live Environment settings. + $form['project']['settings']['live'] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => arg(1) != $project->nid, + '#title' => t('Domain Name Settings'), + '#group' => 'project_settings', + ); + + // Live Environment + $environments = array_keys($project->environments); + if (empty($environments)) { + $environments_options = array(); + } + else { + $environments_options = array_combine($environments, $environments); + $environments_options[''] = t('None'); + } + + $form['project']['codebase']['live_environment'] = array( + '#type' => 'select', + '#title' => t('Primary Environment'), + '#suffix' => '

' . t('Select the primary environment for this project, typically the live environment. This environment will be marked with a icon. This is the environment that will be cloned when pull requests are created.') . '

', + '#options' => $environments_options, + '#default_value' => isset($project->settings->live) ? $project->settings->live['live_environment'] : '', + '#parents' => array( + 'project', + 'settings', + 'live', + 'live_environment', + ), + ); + + // Live Domain + $form['project']['settings']['live']['live_domain'] = array( + '#type' => 'textfield', + '#title' => t('Live Domain'), + '#description' => t('The live domain for this project. Used only for links and when creating subdomain aliases for other environments. You must still add the Domain to your live environment manually.'), + '#size' => 40, + '#default_value' => isset($project->settings->live) ? $node->project->settings->live['live_domain'] : '', + ); + + // Use aliases + $form['project']['settings']['live']['environment_aliases'] = array( + '#type' => 'checkbox', + '#title' => t('For new environments, create subdomains under Live Domain.'), + '#description' => t('When new environments are created, automatically add a domain name such as http://ENVIRONMENT.LIVEDOMAIN.com. Does not affect existing environments. Does not remove domains when disabled.'), + '#default_value' => isset($project->settings->live) ? $project->settings->live['environment_aliases'] : FALSE, + ); + + // Pull Code Method + $form['project']['settings']['deploy'] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => arg(1) != $project->nid, + '#title' => t('Deployment Automation'), + '#description' => t('Configure how code is delivered to the servers. Post Deploy hooks are configured per environment.'), + '#weight' => -9, + '#group' => 'project_settings', + ); + + $form['project']['settings']['deploy']['method'] = array( + '#title' => 'Deploy Code Method', + '#type' => 'radios', + '#description' => t('Choose the method used to deploy code to the server.'), + '#default_value' => isset($project->settings->deploy['method']) ? $project->settings->deploy['method'] : 'webhook', + ); + + // Commit Webhook + $form['project']['settings']['deploy']['method']['#options']['webhook'] = t('Immediate Deployment'); + $form['project']['settings']['deploy']['method']['#options']['webhook'] .= '
' . t('Recommended. Deploy code as it is delivered to your repository.') . ' ' . t('Requires setting up a webhook with your git repository host.') . '' . '
'; + + // Queue + $queues = hosting_get_queues(); + if ($queues['pull']['enabled']) { + $form['project']['settings']['deploy']['method']['#options']['queue'] = t('Queued Deployment'); + + $t = array(); + $t['@freq'] = format_interval($queues['pull']['frequency'], 1); + + $form['project']['settings']['deploy']['method']['#options']['queue'] .= '
'; + $form['project']['settings']['deploy']['method']['#options']['queue'] .= t('Git pull code every @freq. Use only if repository webhooks are not available.', $t); + + + if (user_access('administer hosting queues')) { + $form['project']['settings']['deploy']['method']['#options']['queue'] .= ' ' . l(t("Pull Queue configured to run every @freq.", $t), 'admin/hosting/queues'); + } + else { + $form['project']['settings']['deploy']['method']['#options']['queue'] .= ' ' . t("Pull Queue configured to run every @freq.", $t); + } + + $form['project']['settings']['deploy']['method']['#options']['queue'] .= '
'; + + } + + // Manual Pull + $form['project']['settings']['deploy']['method']['#options']['manual'] = t('Manual Deployment'); + $form['project']['settings']['deploy']['method']['#options']['manual'] .= '
' . t('Deploy code to servers manually via devshop or drush.'); + + $form['project']['settings']['deploy']['method']['#options']['manual'] .= ' ' . t('Not recommended. All environments must be manually updated.') . '' . '
'; + + // Add link to hosting queues admin if the user can access them. + if (!$queues['deploy']['enabled'] && user_access('administer hosting queues')) { + $form['project']['settings']['deploy']['queue_admin'] = array( + '#value' => t('The !link is disabled. Enable it to allow projects to pull code in the queue.', array( + '!link' => l(t('Pull Queue'), 'admin/hosting/queues'), + )), + '#prefix' => '

', + '#suffix' => '

', + ); + } + + // Load deploy hooks form element. + $form['project']['settings']['deploy']['default_hooks'] = devshop_environment_deploy_hooks_form($project); + + // Deploy hooks configuration + $form['project']['settings']['deploy']['default_hooks']['label'] = array( + '#value' => t('Default Deploy Hooks:'), + '#prefix' => '', + '#weight' => -100, + ); + $form['project']['settings']['deploy']['default_hooks']['description'] = array( + '#value' => t('New environments will run these actions when new code or data is deployed.'), + '#prefix' => '

', + '#suffix' => '

', + '#weight' => -100, + ); + + $form['project']['settings']['deploy']['default_hooks']['allow_environment_deploy_config'] = array( + '#type' => 'checkbox', + '#title' => t('Allow environment-specific deploy hook configuration.'), + '#default_value' => isset($node->project->settings->deploy) ? $node->project->settings->deploy['allow_environment_deploy_config'] : FALSE, + '#description' => t('Each environment can be configured to have different deploy hooks. Be sure to check your environments settings if you enable this.' ), + '#parents' => array( + 'project', + 'settings', + 'deploy', + 'allow_environment_deploy_config', + ), + ); + $form['project']['settings']['deploy']['default_hooks']['allow_environment_deploy_hooks_override'] = array( + '#type' => 'checkbox', + '#title' => t('Allow users to override hooks when deploying manually.'), + '#default_value' => isset($node->project->settings->deploy) ? $node->project->settings->deploy['allow_environment_deploy_hooks_override'] : FALSE, + '#description' => t('Check this box to allow users to override the hooks that are run on manual deployments. If left unchecked, all environments will run the deploy hooks configured above on every deployment.' ), + '#parents' => array( + 'project', + 'settings', + 'deploy', + 'allow_environment_deploy_hooks_override', + ), + ); + + // @TODO: is there a better way to save certain values? We lose data without these. + $form['project']['settings']['deploy']['last_webhook'] = array( + '#type' => 'value', + '#value' => isset($node->project->settings->deploy) ? $node->project->settings->deploy['last_webhook'] : NULL, + ); + $form['project']['settings']['deploy']['last_webhook_status'] = array( + '#type' => 'value', + '#value' => isset($node->project->settings->deploy) ? $node->project->settings->deploy['last_webhook_status'] : NULL, + ); + $form['project']['settings']['deploy']['last_webhook_ip'] = array( + '#type' => 'value', + '#value' => isset($node->project->settings->deploy) ? $node->project->settings->deploy['last_webhook_ip'] : NULL, + ); + + //All settings git pull in project page + $form['project']['settings']['default_environment'] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => arg(1) != $project->nid, + '#title' => t('Server Stack'), + '#description' => t('The servers to use when creating new environments.'), + '#group' => 'project_settings', + ); + + // Install Profile + $available_profiles = array(); + foreach ($project->environments as $name => $environment) { + // Passing null to hosting_get_profiles returns all profiles. + if (is_null($environment->platform)) { + continue; + } + $profiles_shortnames = hosting_get_profiles($environment->platform, 'short_name'); + if (is_array($profiles_shortnames) && !empty($profiles_shortnames)) { + $profiles[$name] = array_combine($profiles_shortnames, (array)hosting_get_profiles($environment->platform)); + } else { + $profiles[$name] = array(); + } + if (empty($available_profiles)) { + $available_profiles = $profiles[$name]; + } else { + $available_profiles = array_intersect_key($available_profiles, $profiles[$name]); + } + } + + $form['project']['settings']['default_environment']['install_profile'] = array( + '#type' => 'radios', + '#options' => $available_profiles, + '#title' => t('Default Install Profile'), + '#required' => 1, + '#description' => t('New environments will be created using this install profile. Existing environments will not be affected.'), + '#default_value' => $node->project->install_profile, + '#access' => count($available_profiles), + ); + + // HTTP Server select. + $http_servers = hosting_get_servers('http', FALSE); + if (count($http_servers)) { + $form['project']['settings']['default_environment']['web_server'] = array( + '#title' => t('Web server'), + '#type' => 'select', + '#options' => $http_servers, + '#default_value' => isset($project->settings->default_environment) ? $project->settings->default_environment['web_server'] : current($http_servers), + ); + } + + // DB Server select. + $db_servers = hosting_get_servers('db', FALSE); + if (count($db_servers)) { + $form['project']['settings']['default_environment']['db_server'] = array( + '#title' => t('Database server'), + '#type' => 'select', + '#options' => $db_servers, + '#default_value' => isset($project->settings->default_environment) ? $project->settings->default_environment['db_server'] : current($db_servers), + ); + } + + // Solr Server Select + $solr_servers = hosting_get_servers('solr', FALSE); + if (count($solr_servers)) { + $form['project']['settings']['default_environment']['solr_server'] = array( + '#title' => t('Solr server'), + '#type' => 'select', + '#options' => $solr_servers, + '#default_value' => $project->settings->default_environment['solr_server'], + ); + } + + // Force the servers for this project's environments + $form['project']['settings']['default_environment']['force_default_servers'] = array( + '#title' => t('Force new environments to use these servers.'), + '#description' => t('When new environments are created, they will use the servers selected above.'), + '#type' => 'checkbox', + '#default_value' => $project->settings->default_environment['force_default_servers'], + ); + return $form; +} + +/** + * Implementation of hook_validate(). + * + * This function is no longer used since we have a ctools wizard for + * project creation. + */ +function devshop_projects_validate($node, &$form, &$form_state) { + + // Code path and base url must be unique + if (db_query('SELECT n.nid FROM {hosting_devshop_project} d LEFT JOIN {node} n ON d.nid = n.nid WHERE status = :status AND code_path = :code_path AND n.nid != :nid;', array(':status' => 1, ':code_path' => $form_state['values']['project']['code_path'], ':nid' => $form_state['values']['nid']))->fetchField()) { + form_set_error('code_path', t('The code path %path is in use by another project. Please enter a different path.', array( + '%path' => $form_state['values']['project']['code_path'], + ))); + } + // If custom domain pattern is allowed, AND domain pattern does not have "@project" in it, AND domain pattern is not the default AND it is found on another project, throw an error. + if (variable_get('devshop_projects_allow_custom_base_url', FALSE) && strpos($form_state['values']['project']['base_url'], '@project') === FALSE && $form_state['values']['project']['base_url'] != variable_get('devshop_project_environment_url_pattern', '@project.@environment.@hostname') && db_query('SELECT n.nid FROM {hosting_devshop_project} d LEFT JOIN {node} n ON d.nid = n.nid WHERE status = :status AND base_url = :base_url AND n.nid != :nid;;', array(':status' => 1, ':base_url' => $form_state['values']['project']['base_url'], ':nid' => $form_state['values']['nid']))->fetchField()) { + form_set_error('base_url', t('The Environment Domain Pattern %url is in use by another project. Please enter a different Environment Domain Pattern.', array( + '%url' => $form_state['values']['project']['base_url'], + ))); + } + + // "Environment Domain Name Pattern" (Base URL) + // Ensure @environment is included. + if (strpos($form_state['values']['project']['base_url'], '@environment') === FALSE ) { + form_set_error('project][base_url', t('Environment Domain Name Pattern must include @environment placeholder.')); + } +} + +/** + * Implements hook_form_alter(). + */ +function devshop_projects_form_alter(&$form, &$form_state, $form_id) { + + // Removing unneccesary fieldgroups + if ($form_id == 'project_node_form') { + global $user; + + $project = $form['#node']; + unset($form['menu']); + unset($form['revision_information']); + unset($form['author']); + unset($form['options']); + unset($form['actions']['delete']); + unset($form['actions']['preview']); + if (isset($form['retry']['#value'])) { + $form['actions']['submit']['#value'] = t('Save and Retry'); + } + + //Add button for delete project + $form['actions']['delete'] = array( + '#markup' => l(' ' . t('Delete Project'), 'hosting_confirm/' . $project->nid . '/project_delete', array( + 'query' => array('token' => drupal_get_token($user->uid)), + 'attributes' => array( + 'class' => array('btn btn-danger pull-right'), + ), + 'html' => TRUE, + )), + '#weight' => 10, + '#access' => drupal_valid_path('hosting_confirm/' . $project->nid . '/project_delete'), + ); + } + + // Create Project Wizard + if ( + $form_id == 'devshop_project_create_step_git' || + $form_id == 'devshop_project_create_step_sites' || + $form_id == 'devshop_project_create_step_settings' || + $form_id == 'devshop_project_create_step_environments' + ){ + if (isset($form['#no_finish'])) { + unset($form['buttons']['return']); + } + if (isset($form['#no_next'])) { + unset($form['buttons']['next']); + } + + if (isset($form['buttons']['next']['#value'])) { + $form['buttons']['next']['#value'] .= ' '; + } + + if (isset($form['buttons']['return']['#value'])) { + $form['buttons']['return']['#value'] = ' ' . t('Create Project & Environments'); + } + + $form['buttons']['next']['#attributes'] = + $form['buttons']['return']['#attributes'] = array( + 'class' => array( + 'btn btn-success', + ), + ); + $form['buttons']['previous']['#attributes'] = array( + 'class' => array( + 'btn btn-default', + ), + ); + $form['buttons']['cancel']['#attributes'] = array( + 'class' => array( + 'btn btn-link', + ), + ); + + } + + // Hosting Task Forms + if ($form_id == 'hosting_task_confirm_form') { + switch ($form['task']['#value']) { + + // Migrate Form: used for changing database server. + case 'migrate': + + // To change the database server, we use the migrate form. + if ($_GET['deploy'] == 'stack') { + drupal_set_title(t('Change Database Server')); + $site_node = node_load($form['nid']['#value']); + $environment = $site_node->environment; + $form['help']['#weight'] = 100; + $form['help']['#value'] = t("Are you sure you want to change this environment's database server?"); + + // Hide "URI" field unless "rename" GET parameter is added. + if (!isset($_GET['rename'])) { + $form['parameters']['new_uri']['#type'] = 'value'; + $form['parameters']['new_uri']['#value'] = $form['parameters']['new_uri']['#default_value']; + } + + // Display something helpful + $form['old'] = array( + '#type' => 'item', + '#title' => t('Current Database Server'), + '#value' => l($environment->servers['db']['name'], 'hosting/c/server_' . $environment->servers['db']['nid']), + '#weight' => '-1', + ); + } + if (isset($_GET['rename'])) { + drupal_set_title(t('Change Domain Name for environment @env in project @proj?', array( + '@env' => $environment->name, + '@proj' => $environment->project_name, + ))); + $form['help']['#value'] = t("Are you sure you want to change this environment's domain name?"); + $form['submit']['#value'] = t('Change Domain'); + } + if (isset($_GET['rename']) || $_GET['deploy'] == 'stack') { + foreach ($form['parameters'] as $key => $element) { + if (is_int($key)) { + // Don't unset the target platform. Just hide it. + if (isset($element['target_platform']['#default_value'])) { + $form['parameters'][$key]['target_platform']['#type'] = 'value'; + $form['parameters'][$key]['target_platform']['#value'] = $form['parameters'][$key]['target_platform']['#default_value']; + } + else { + unset($form['parameters'][$key]); + } + } + } + } + break; + + // Deploy task form + case 'devshop-deploy': + + // Alter title of deploy task. + $node = node_load($form['nid']['#value']); + drupal_set_title(t('Deploy code to Environment "@env"', array('@env' => $node->environment->name)), PASS_THROUGH); + $form['actions']['cancel']['#value'] = l(t('Cancel'), "node/{$node->project->nid}"); + break; + + // Sync task form + case 'sync': + if (isset($_GET['source'])) { + $node = node_load($form['nid']['#value']); + + if ($_GET['source'] == 'other') { + $source = ''; + } + elseif (is_int($_GET['source'])) { + $source = node_load($_GET['source']); + if (isset($source->nid) && $source->type != 'site') { + break; + } + $source_alias = $source->environment->system_alias; + } + elseif (isset($node->project->settings->aliases[$_GET['source']])) { + $alias_name = $source = $_GET['source']; + $source_alias = "@{$node->project->name}.{$alias_name}"; + $alias_data = $node->project->settings->aliases[$_GET['source']]; + } + else { + return; + } + +// print_r($source); die; +// print $source_alias ; die; + + drupal_set_title(t('Sync Data to Environment "@env"', array('@env' => $node->environment->name)), PASS_THROUGH); + $environment = $node->environment; + + // If source is empty, ask for an alias. + if (empty($source)) { + $form['parameters']['source'] = array( + '#type' => 'textfield', + '#title' => t('Drush Alias for Source'), + '#description' => t('Enter a full drush alias to use as the source. Make sure to include the "@" character at the beginning.'), + ); + $form['parameters']['destination'] = array( + '#type' => 'value', + '#value' => $node->environment->system_alias, + ); + } + // If a source is specified, add help and set it as a value. + else { + + $source_environment = $source->environment; + $form['parameters']['source'] = array( + '#type' => 'value', + '#value' => $source_alias, + ); + $form['parameters']['destination'] = array( + '#type' => 'value', + '#value' => $node->environment->system_alias, + ); + } + + // Don't copy modules and themes. + unset($form['parameters']['modules']); + unset($form['parameters']['themes']); + unset($form['parameters']['libraries']); + + // Better output + $form['parameters']['database']['#prefix'] = ''; + $form['parameters']['registry-rebuild']['#prefix'] = ''; + $form['parameters']['backup']['#weight'] = -11; + + $form['actions']['submit']['#value'] = '' . t('Sync Data'); + $form['actions']['submit']['#attributes'] = array( + 'class' => array( + 'btn-success' + ), + ); + $form['actions']['cancel']['#value'] = l(t('Cancel'), "node/{$node->project->nid}"); + + $form['warning']['#markup'] = '
' . t('Clicking "Sync Data" will DESTROY the database for the environment !link.', array('!link' => l($environment->url, $environment->url, array('attributes' => array('target' => '_blank'))))) . '
'; + + + $destroyed = t('This environment will be destroyed.'); + $source = t('This environment will be copied.'); + + $source_environment_url = $source_environment->url? $source_environment->url: 'http://' . $alias_data['uri']; + $source_environment_name = $source_environment->name? $source_environment->name: $alias_name; + $html = << + +
+ + + +
+ + +HTML; + + $form['notes'] = array( + '#markup' => $html, + '#weight' => -10, + ); + } + break; + case 'delete': + $node = node_load($form['nid']['#value']); + if ($node->type == 'project') { + + // Set a better title. + drupal_set_title(''); + $form['title'] = array( + '#weight' => -10, + '#markup' => '

' .t('Delete Project: !project', array( + '!project' => l($node->project->name, "node/{$node->nid}"), + )) . '

' + ); + + // Get a list of environments. + foreach ($node->project->environments as $environment) { + $items[] = l($environment->uri, 'http://' . $environment->uri); + + } + $environments = theme('item_list', array('items' => $items)); + + // Display a scary warning message. + $form['help']['#markup'] = '
' . t('Are you sure you wish to destroy the project !project and all of these environments?', array( + '!project' => l($node->project->name, "node/{$node->nid}"), + )) . $environments . '
'; + + // Make the button big + $form['actions']['submit']['#value'] = t('Delete Project & Environments'); + $form['actions']['submit']['#attributes']['class'][] = 'btn btn-large btn-danger'; + + } + break; + } + } + + // Platform "Edit" page. + if ($form_id == 'platform_node_form') { + + // Set to values + $platform_node = node_load($form['nid']['#value']); + if (empty($platform_node->project)) { + return; + } + + $form['title']['#type'] = 'value'; + unset($form['info']); + + if (isset($platform_node->project)) { + // Hide makefile + $form['frommakefile']['#access'] = FALSE; + $form['git']['pull_url']['#access'] = FALSE; + } + + // If switching webservers... + if ($_GET['web_server']) { + $environment = $platform_node->environment; + $form['help'] = array( + '#markup' => t("Are you sure you want to change this site's web server? NOTE: You will have to change DNS as well!"), + '#weight' => -10, + ); + + $web_server = $_GET['web_server']; + $web_server_nid = db_query('SELECT nid FROM {node} WHERE type = :type && title = :title', array(':type' => "server", ':title' => $web_server))->fetchField(); + + // Set values + $form['web_server']['#default_value'] = $web_server_nid; + $form['web_server']['#type'] = 'hidden'; + + // Load servers to display IP addresses. + $old_server = node_load($environment->servers['http']['nid']); + $new_server = node_load($web_server_nid); + + // Display something helpful + $form['old'] = array( + '#type' => 'item', + '#title' => t('Current Web Server'), + '#markup' => l($environment->servers['http']['name'], 'node/' . $environment->servers['http']['nid']) . '
' . implode($old_server->ip_addresses, '
'), + '#weight' => '-1', + ); + + // Display something helpful + $form['new'] = array( + '#type' => 'item', + '#title' => t('New Web Server'), + '#markup' => l($web_server, "node/$web_server_nid") . '
' . implode($new_server->ip_addresses, '
'), + '#weight' => '0', + ); + } + } +} + +/** + * Implements hook_form_FORM_ID_alter() for node_site_form + * + * "Environment" Settings form. + */ +function devshop_projects_form_site_node_form_alter(&$form, &$form_state, $form_id) { + + $node = $form['#node']; + + $form['environment'] = array( + '#weight' => -9, + '#tree' => true, + ); + + // On site create page, use devshop_projects_create_environment_form() + if (arg(1) == 'add') { + devshop_projects_create_environment_form($form, $form_state, $form_id); + + $project = devshop_projects_load_by_name(arg(3)); + } + + // Only act on site nodes that are in projects. + elseif (!isset($node->environment)) { + return; + } + else { + + // Get environment object. (Convert if it is an array. This happens when clicking "Add an Alias". + $environment = (object) $node->environment; + $project = $node->project; + + // Remove aegir's helpful info panel. + unset($form['info']); + + // Values + $form['environment']['project_nid'] = array( + '#value' => $environment->project_nid, + '#type' => 'value', + ); + $form['environment']['project_name'] = array( + '#value' => $environment->project_name, + '#type' => 'value', + ); + $form['environment']['name'] = array( + '#value' => $environment->name, + '#type' => 'value', + ); + $form['environment']['git_ref'] = array( + '#value' => $environment->git_ref, + '#type' => 'value', + ); + $form['environment']['site'] = array( + '#value' => $environment->site, + '#type' => 'value', + ); + $form['environment']['platform'] = array( + '#value' => $environment->platform, + '#type' => 'value', + ); + + $form['environment']['last_task'] = array( + '#type' => 'value', + '#value' => $environment->last_task, + ); + $form['form_title'] = array( + '#markup' => t('Environment Settings'), + '#prefix' => '

', + '#suffix' => '

', + '#weight' => -1000, + ); + } + + // User-configurable Settings + $form['environment']['settings'] = array( + '#type' => 'fieldset', + '#title' => t('Environment Settings'), + '#tree' => true, + ); + $form['environment']['settings']['locked'] = array( + '#type' => 'checkbox', + '#title' => t('Lock Database'), + '#default_value' => isset($environment->settings->locked) ? $environment->settings->locked : FALSE, + '#description' => t('Prevent devshop users from destroying the database.') . '

Drush users may still overwrite the database.

', + ); + + $form['environment']['settings']['pull_disabled'] = array( + '#type' => 'checkbox', + '#title' => t('Disable Deploy on Commit'), + '#default_value' => isset($environment->settings->pull_disabled) ? $environment->settings->pull_disabled : FALSE, + '#description' => t('Do not pull code to the server on commit & push.'), + ); + + // Load deploy hooks form element. + $form['environment']['settings']['deploy'] = devshop_environment_deploy_hooks_form($project, $environment); + + // UX Improvements + // Project Settings Vertical Tabs + $form['environment_settings'] = array( + '#type' => 'vertical_tabs', + '#weight' => -11, + ); + + $form['environment']['settings']['#title'] = t('General Settings'); + $form['environment']['settings']['#group'] = 'environment_settings'; + $form['environment']['settings']['#weight'] = -100; + $form['environment']['settings']['deploy']['#group'] = 'environment_settings'; + $form['hosting_backup_queue']['#group'] = 'environment_settings'; + $form['http_basic_auth']['#group'] = 'environment_settings'; + $form['http_basic_auth']['#title'] = t('Password Protection'); + $form['hosting_logs']['#group'] = 'environment_settings'; + + // Have to add new element for Domains because of the way drupal ajax callback works. + $form['domains'] = array( + '#title' => t('Domain Names'), + '#type' => 'fieldset', + '#group' => 'environment_settings', + ); + $form['domains']['aliases_wrapper'] = $form['aliases_wrapper']; + $form['domains']['aliases_wrapper']['#type'] = 'container'; + $form['domains']['aliases_wrapper']['add_alias']['#ajax']['callback'] = 'devshop_hosting_alias_add_alias_callback'; + unset($form['aliases_wrapper']); + + $form['environment']['settings']['client'] = $form['client']; + $form['environment']['settings']['client']['#tree'] = FALSE; + unset($form['client']); + + $form['environment']['settings']['site_language'] = $form['site_language']; + $form['environment']['settings']['site_language']['#tree'] = FALSE; + unset($form['site_language']); + + if (isset($form['hosting_ssl_wrapper'])) { + $form['hosting_ssl_wrapper']['#group'] = 'environment_settings'; + $form['hosting_ssl_wrapper']['#title'] = t('HTTPS'); + $form['hosting_ssl_wrapper']['#prefix'] = ''; + $form['hosting_ssl_wrapper']['#suffix'] = ''; + } + if (isset($form['hosting_https_wrapper'])) { + $form['hosting_https_wrapper']['#group'] = 'environment_settings'; + $form['hosting_https_wrapper']['#title'] = t('HTTPS'); + $form['hosting_https_wrapper']['#prefix'] = ''; + $form['hosting_https_wrapper']['#suffix'] = ''; + } + + // Ensure values are saved. + if (!isset($form['install_method'])) { + $form['install_method'] = array( + '#type' => 'value', + '#value' => $environment->settings->install_method, + ); + } + + // Add our own submit handler. + $form['actions']['submit']['#submit'][] = 'devshop_projects_environment_settings_submit'; + +} + + +/** + * Ajax callback for returning "Add Alias" form element, since we moved ours to a sub fieldset. + */ +function devshop_hosting_alias_add_alias_callback($form, $form_state) { + return $form['domains']['aliases_wrapper']; +} + + +/** + * Form alters for "site_node_form", which is our "Create Environment" form. + * + * Code that only applies to "site create" form should go here. If it needs to + * be in create and edit, it should go in devshop_projects_form_site_node_form_alter() + * + * This is invoked from devshop_projects_form_site_node_form_alter() + * + * @see devshop_projects_form_site_node_form_alter() + * + */ +function devshop_projects_create_environment_form(&$form, &$form_state, $form_id) { + + // Look for project. If there is none, return. + $project_node = devshop_projects_load_by_name(arg(3)); + if ($project_node->type != 'project') { + return; + } + $project = $project_node->project; + + // Wrap the form in a container so we can use grid + $form['#prefix'] = '
'; + $form['#suffix'] = '
'; + + $form['form_title'] = array( + '#markup' => t('Create New Environment'), + '#prefix' => '

', + '#suffix' => '

', + '#weight' => -1000, + ); + + // Add our own validator and submit handlers. + $form['actions']['submit']['#value'] = t('Create New Environment'); +// array_unshift($form['actions']['submit']['#submit'], 'devshop_projects_create_environment_form_submit'); + + // Generate field prefix and suffix using domain name pattern. + if (variable_get('devshop_projects_allow_custom_base_url')) { + $pattern = $project->base_url; + } + else { + $pattern = variable_get('devshop_project_environment_url_pattern', '@project.@environment.@hostname'); + } + + $labels = explode('@environment', strtr($pattern, array( + '@project' => $project_node->title, + '@hostname' => $_SERVER['SERVER_NAME'], + ))); + + // Hide the "Domain Name" field, as this will be generated + $form['title']['#access'] = false; + $form['title']['#required'] = false; + + $form['environment_name'] = array( + '#title' => t('Environment Name'), + '#type' => 'textfield', + '#description' => t('Enter a system name for your environment. For consistency, you might want to match the branch name.'), + '#required' => TRUE, + '#field_prefix' => '
http://' . $labels[0] . '
', + '#field_suffix' => '
' . $labels[1] .'
+
', + '#size' => 10, + '#maxlength' => 64, + '#weight' => -101, + '#attributes' => array( + 'data-placement' => 'bottom', + ), + '#element_validate' => array( + 'devshop_projects_create_environment_form_validate_name' + ), + '#wrapper_attributes' => array( + 'class' => array('col-sm-8 col-md-8'), + ), + ); + + $branch_options = devshop_projects_git_ref_options($project); + $form['git_ref'] = array( + '#title' => t('Branch or Tag'), + '#type' => 'select', + '#description' => t('The git reference to checkout for this environment. You can change this later using the "Deploy Code" button.'), + '#options' => $branch_options, + '#required' => TRUE, + '#weight' => -100, + '#wrapper_attributes' => array( + 'class' => array('col-sm-4 col-md-4'), + ), + ); + + $form['install_method'] = array( + '#type' => 'fieldset', + '#weight' => -99, + '#tree' => TRUE, + '#attributes' => array( + 'class' => array('clearfix'), + ), + ); + $form['install_method']['method'] = array( + '#type' => 'radios', + '#required' => TRUE, + '#title' => t('Install Method'), + '#process' => array('devshop_environment_method_process'), + '#weight' => -99, + '#options' => array( + 'clone' => t('Clone Environment'), + 'profile' => t('Drupal Profile'), + 'manual' => t('Empty Database'), + 'import' => t('Import Database'), + ), + '#default_value' => 'clone', + ); + + $form['install_method']['none'] = array( + '#type' => 'container', + '#weight' => -98, + '#states' => array( + 'visible' => array( + ':input[name="install_method[method]"]' => array('value' => 'manual'), + ), + ), + 'note' => array( + '#markup' => t('An empty database will be created. You can install Drupal manually by visiting install.php, by using Drush, or you can manually import a database.'), + ), + ); + + $form['install_method']['profile'] = array( + '#type' => 'radios', + '#title' => t('Drupal Install Profile'), + '#description' => t('These install profiles were found in other environments in this project. If you choose an install profile that does not exist in the codebase, the "Standard" profile will be used.'), + '#description_display' => 'before', + '#weight' => -98, + '#required' => TRUE, + '#options' => devshop_environment_add_form_profile_options($project), + '#states' => array( + 'visible' => array( + ':input[name="install_method[method]"]' => array('value' => 'profile'), + ), + ), + ); + + // Set default profile. + if (count($form['install_method']['profile']['#options']) == 1 || empty($project->install_profile)) { + $form['install_method']['profile']['#default_value'] = '_other'; + } + else { + $form['install_method']['profile']['#default_value'] = $project->install_profile; + } + + $form['install_method']['profile_other'] = array( + '#type' => 'textfield', + '#title' => t('Other Install Profile'), + '#title_display' => 'invisible', + '#description' => t('Enter the exact name the install profile to use. It must already exist in your git branch or tag.'), + '#element_validate' => array( + 'devshop_projects_create_environment_form_validate_profile' + ), + '#attributes' => array( + 'placeholder' => t('Profile Name'), + ), + '#weight' => -97, + '#states' => array( + 'visible' => array( + ':input[name="install_method[method]"]' => array('value' => 'profile'), + ':input[name="install_method[profile]"]' => array('value' => '_other'), + ), + ), + ); + + // Generate environments options. + foreach ($project->environments as $e => $environment) { + $link = l(' ' . $environment->uri, $environment->url, array( + 'html' => TRUE, + 'attributes' => array( + 'target' => '_blank', + 'class' => array('btn-text'), + ), + )) . ''; + $git_ref = $environment->git_ref; + $icon = $environment->git_ref_type == 'tag'? 'tag': 'code-fork'; + $environment_options[$environment->system_alias] = "{$environment->name} $git_ref $link"; + } + + // Add other aliases to the clone source. + if (isset($project->settings->aliases)) { + foreach ($project->settings->aliases as $alias => $data) { + $link = l(' ' . $data['uri'], 'http://' . $data['uri'], array( + 'html' => TRUE, + 'attributes' => array( + 'target' => '_blank', + 'class' => array('btn-text'), + ), + )) . ''; + $remote = t('Unknown'); + $environment_options["@{$project->name}.{$alias}"] = "{$alias} $remote $link"; + } + } + + $environment_options['_other'] = t('Other Drush Alias'); + + if (count($environment_options)) { + $form['install_method']['clone_source'] = array( + '#type' => 'radios', + '#title' => t('Environment to clone'), + '#weight' => -98, + '#required' => TRUE, + '#options' => $environment_options, + '#default_value' => key($environment_options), + '#states' => array( + 'visible' => array( + ':input[name="install_method[method]"]' => array('value' => 'clone'), + ), + ), + ); + } + + // Detect clone link. Set defaults. + if (arg(4) == 'clone') { + $environment_to_clone = arg(5); + $form['install_method']['method']['#default_value'] = 'clone'; + $form['install_method']['clone_source']['#default_value'] = $project->environments[$environment_to_clone]->system_alias; + $form['git_ref']['#default_value'] = $project->environments[$environment_to_clone]->git_ref; + } + + // Detect fork link. + // @TODO: Get "forking" back in place for the next beta. +// if (arg(4) == 'fork') { +// $environment_to_clone = arg(5); +// $form['install_method']['method']['#default_value'] = 'clone'; +// $form['install_method']['clone_source']['#default_value'] = $project->environments[$environment_to_clone]->system_alias; +// $form['git_ref']['#default_value'] = $project->environments[$environment_to_clone]->git_ref; +// $form['git_ref']['#title'] = t('Base Branch or Tag'); +// $form['git_ref']['#description'] = t('The git reference you want to create a new branch from.'); +// +// $form['git_ref_new'] = array( +// '#title' => t('New branch name'), +// '#description' => t('New branch name'), +// ); +// } + + + $form['install_method']['clone_source_drush'] = array( + '#type' => 'textfield', + '#title' => t('Drush Alias'), + '#description' => t('Enter a Drush alias you would like to Sync your database and files from. It must already exist on the server.'), + '#element_validate' => array( + 'devshop_projects_create_environment_form_validate_clone_source' + ), + '#weight' => -98, + '#attributes' => array( + 'placeholder' => t('@drush.alias'), + ), + '#states' => array( + 'visible' => array( + ':input[name="install_method[clone_source]"]' => array('value' => '_other'), + ':input[name="install_method[method]"]' => array('value' => 'clone'), + ), + ), + ); + + $form['install_method']['import'] = array( + '#type' => 'textfield', + '#title' => t('Path to SQL'), + '#description' => + t('Enter either a remote MySQL address (such as mysql://username:password@host/database), or an absolute path to an SQL dump (such as /var/aegir/site-backup.sql).') . + '' . t('This string is stored in plain text. Use with caution.') . '' + , + '#weight' => -98, + '#states' => array( + 'visible' => array( + ':input[name="install_method[method]"]' => array('value' => 'import'), + ), + ), + '#element_validate' => array( + 'devshop_projects_create_environment_form_validate_import' + ), + ); + + $form['server_stack'] = array( + '#type' => 'fieldset', + '#title' => t('Server Stack'), + '#group' => 'environment_settings', + ); + + $form['server_stack']['db_server'] = $form['db_server']; + $form['server_stack']['db_server']['#group'] = 'environment_settings'; + $form['server_stack']['db_server']['#tree'] = FALSE; + unset($form['db_server']); + + $web_servers = hosting_get_servers('http', FALSE); + $form['server_stack']['web_server'] = array( + '#type' => 'radios', + '#title' => t('Web server'), + '#description' => t('The web server the site will be hosted on.'), + '#options' => $web_servers, + '#required' => TRUE, + '#tree' => FALSE, + '#default_value' => $project->settings->default_environment['web_server'], + ); + + // Force the new environment to use the "default" servers. + if ($project->settings->default_environment['force_default_servers']) { + $form['server_stack']['#description'] = t('All environments in this project must use this server stack.'); + + $web_server_nid = $project->settings->default_environment['web_server']; + $form['server_stack']['web_server']['#type'] = 'value'; + $form['server_stack']['web_server']['#value'] = $web_server_nid; + $form['server_stack']['web_server_label'] = array( + '#type' => 'item', + '#title' => t('Web server'), + '#markup' => l($web_servers[$web_server_nid], "node/$web_server_nid", array('attributes' => array('target' => '_blank'))), + ); + + $db_servers = hosting_get_servers('db', FALSE); + $db_server_nid = $project->settings->default_environment['db_server']; + $form['server_stack']['db_server']['#type'] = 'value'; + $form['server_stack']['db_server']['#value'] = $db_server_nid; + $form['server_stack']['db_server_label'] = array( + '#type' => 'item', + '#title' => t('Database server'), + '#markup' => l($db_servers[$db_server_nid], "node/$db_server_nid", array('attributes' => array('target' => '_blank'))), + ); + } + + $form['project_nid'] = array( + '#value' => $project->nid, + '#type' => 'value', + ); + + $form['platform_node'] = array( + '#type' => 'value', + '#value' => NULL, + ); + + + // Remove platform and profile options. + // @TODO: Patch Aegir so we don't have to load all of these platform options. + $form['platform']['#access'] = FALSE; + $form['profile']['#access'] = FALSE; + $form['git']['#access'] = FALSE; +} + +/** + * Validator for the domain name field: Sets domain according to pattern. + * @param $element + * @param $form_state + * @param $form + */ +function devshop_projects_create_environment_form_validate_name($element, &$form_state, $form) { + + if (!empty($element['#value'])) { + $project_node = node_load($form_state['values']['project_nid']); + $project = $project_node->project; + + // Check existence of the environment name + if (!empty($project->environments[$element['#value']])) { + form_set_error('environment_name', t('There is already an environment named %name in this project. Please choose a different name.', array('%name' => $form_state['values']['environment_name']))); + } + + // Check for illegal chars + if (!preg_match('!^[a-z0-9_]+$!', $element['#value'])) { + form_set_error('environment_name', t('The environment name must contain only lowercase letters, numbers, and underscores.')); + } + + // Generate field prefix and suffix using domain name pattern. + form_set_value($form['title'], devshop_environment_url($project, $element['#value']), $form_state); + + // Attach a new platform node. + $platform_node = devshop_prepare_platform_node($project, $element['#value'], $form_state['values']['git_ref'], $form_state['values']['web_server']); + + // Set value in form_state. + form_set_value($form['platform_node'], $platform_node, $form_state); + form_set_value($form['profile'], 0, $form_state); + form_set_value($form['platform'], 0, $form_state); + + } +} + +/** + * Helper to prepare the platform object to be attached to the site node. + * + * Example: + * + * $platform = devshop_prepare_platform_node($project, $environment_name, $git_ref, $web_server, $prepare_node); + * if ($platform = node_submit($platform)) { + * node_save($platform); + * } + * + * @param $project + * A fully loaded project object. + * + * @param $environment_name + * The name of the environment. + * + * @param $git_ref + * The desired git reference for the environment. + * + * @param $web_server + * The NID of a web server to use for this platform. + * + * @return stdClass + * A populated object ready for node_submit and node_save. + */ +function devshop_prepare_platform_node($project, $environment_name, $git_ref, $web_server) { + $platform_node = new stdClass(); + $platform_node->type = 'platform'; + $platform_node->title = $project->name . '_' . $environment_name; + + // Specify GIT Url for hosting_git.module + $platform_node->git['repo_url'] = $project->git_url; + $platform_node->git['git_ref'] = $git_ref; + + // Enable git pull queue for this platform if chosen. + if ($project->settings->deploy['method'] == 'queue') { + $platform_node->git['pull_method'] = HOSTING_GIT_PULL_QUEUE; + } + // For devshop "webhook" and "manual" methods, disable git pull for now. + // @TODO: Remove devshop "code deploy" task in exchange for hosting_git_pull module. + else { + $platform_node->git['pull_method'] = HOSTING_GIT_PULL_DISABLED; + } + + + // Determine which web server to use. + // If no web server was set and project has a default, make sure we set it. + if (empty($web_server) && !empty($project->settings->default_environment['web_server'])) { + $platform_node->web_server = $project->settings->default_environment['web_server']; + } + else { + $platform_node->web_server = $web_server; + } + + $platform_node->git['repo_path'] = $project->code_path . '/' . $environment_name; + + // Append drupal_path to repo_path if there is one. If not, repo_path is publish_path. + if ($project->drupal_path) { + $platform_node->publish_path = $platform_node->git['repo_path'] . '/' . $project->drupal_path; + $platform_node->git['repo_docroot'] = $project->drupal_path; + } + else { + $platform_node->publish_path = $platform_node->git['repo_path']; + } + return $platform_node; +} + +/** + * Validator for the "other profile" field. + * + * @param $element + * @param $form_state + * @param $form + */ +function devshop_projects_create_environment_form_validate_profile($element, &$form_state, &$form) { + if ($form_state['values']['install_method']['method'] == 'profile' && $form_state['values']['install_method']['profile'] =='_other' && empty($element['#value'])) { + form_set_error('install_method][profile_other', t('You must enter a profile name.')); + } + elseif ($form_state['values']['install_method']['method'] == 'profile' && $form_state['values']['install_method']['profile'] =='_other') { + form_set_value($form['install_method']['profile'], $element['#value'], $form_state); + } +} + +/** + * Validator for the "clone_source" field. + * + * @param $element + * @param $form_state + * @param $form + */ +function devshop_projects_create_environment_form_validate_clone_source($element, &$form_state, $form) { + if ($form_state['values']['install_method']['method'] == 'clone' && $form_state['values']['install_method']['clone_source'] =='_other' && empty($element['#value'])) { + form_set_error('install_method][clone_source_drush', t('You must enter a drush alias.')); + } +} + +/** + * Validator for the "Path to SQL" field. + * + * @param $element + * @param $form_state + * @param $form + */ +function devshop_projects_create_environment_form_validate_import($element, &$form_state, $form) { + + $url_components = parse_url($element['#value']); + + if ($form_state['values']['install_method']['method'] == 'import' && empty($element['#value'])) { + form_set_error('install_method][import', t('If using the "Import Database" Install Method, you must enter a MySQL connection URL or absolute path to an SQL file.')); + } + elseif ($url_components['scheme'] == 'mysql') { + $database = array( + 'database' => ltrim($url_components['path'], '/'), + 'username' => $url_components['user'], + 'password' => $url_components['pass'], + 'host' => $url_components['host'], + 'driver' => 'mysql', // replace with your database driver + ); + Database::addConnectionInfo('devshop_remote_db', 'default', $database); + + try { + db_set_active('devshop_remote_db'); + $tables = db_query('SHOW TABLES'); + drupal_set_message(t('Database connection successful.')); + } + catch (\PDOException $e) { + form_set_error('install_method][import', t('Unable to connect to the database: %e', array('%e' => $e->getMessage()))); + } + finally { + db_set_active(); + } + } +} + +/** + * Validator for site_node_form for node creation, when creating an environment + * in a project. + */ +function devshop_projects_create_environment_form_submit($form, &$form_state) { + + // @TODO: Clean up install_method values. + +} + +/** + * Form element processor for Install Method radio buttons. + * @param $element + * @return array + */ +function devshop_environment_method_process($element) { + $element = form_process_radios($element); + + foreach (element_children($element) as $i) { + $element[$i]['#label_attributes']['class'] = array( + 'btn btn-link' + ); + $element[$i]['#wrapper_attributes']['class'] = array( + 'install-method-wrapper' + ); + } + + return $element; +} + +/** + * Return all available install profiles found in all platforms for this project. + */ +function devshop_environment_add_form_profile_options($project) { + + + foreach ($project->environments as $e => $environment) { + + $packages = hosting_package_instances_load(array( + 'package_type' => 'profile', + 'rid' => $environment->platform, + )); + + foreach ($packages as $instance) { + $options[$instance->short_name] = $instance->title; + $options[$instance->short_name] .= ' ' . $instance->description . ''; + } + } + $options['_other'] = t('Other') . ' ' . t('Choose another profile.') . ''; + return $options; +} + +/** + * Helper to output the forms selection for Deploy Hooks. + * + * Used in the Projects settings form, environment settings form, deploy task form, and sync form. + * + * @param $project + * @param null $environment + * @return array + */ +function devshop_environment_deploy_hooks_form($project, $environment = NULL, $task_type = NULL) { + + // If environment not specified, just grab one. + if (!$environment) { + $environment = current($project->environments); + $is_environment_form = FALSE; + } + else { + $is_environment_form = TRUE; + } + $return = array( + '#type' => 'fieldset', + '#group' => 'project_settings', + '#title' => t('Deployment Hooks'), + '#description' => t('Deployment hooks are run whenever your codebase changes. It is recommended to always enable database updates and cache clearing.'), + '#weight' => -10, + ); + + $return['#project'] = $project; + $return['#environment'] = $environment; + + // If we are on the project creation wizard, set some sane defaults + if (current_path() == 'projects/add/settings') { + $environment_update = FALSE; + $environment_cache = FALSE; + $environment_revert = FALSE; + $environment_dothooks = FALSE; + $environment_acquia_hooks = FALSE; + $environment_composer_install = FALSE; + $project_update = TRUE; + $project_cache = TRUE; + $project_revert = FALSE; + $project_dothooks = FALSE; + $project_acquia_hooks = FALSE; + $project_composer_install = FALSE; + } + else { + $environment_update = isset($environment->settings->deploy) ? $environment->settings->deploy['update'] : FALSE; + $environment_cache = isset($environment->settings->deploy) ? $environment->settings->deploy['cache'] : FALSE; + $environment_revert = isset($environment->settings->deploy) ? $environment->settings->deploy['revert'] : FALSE; + $environment_dothooks = isset($environment->settings->deploy) ? $environment->settings->deploy['dothooks'] : FALSE; + $environment_acquia_hooks = isset($environment->settings->deploy) ? $environment->settings->deploy['acquia_hooks'] : FALSE; + $environment_composer_install = isset($environment->settings->deploy) ? $environment->settings->deploy['composer'] : FALSE; + $project_update = isset($project->settings->deploy) ? $project->settings->deploy['default_hooks']['update'] : FALSE; + $project_cache = isset($project->settings->deploy) ? $project->settings->deploy['default_hooks']['cache'] : FALSE; + $project_revert = isset($project->settings->deploy) ? $project->settings->deploy['default_hooks']['revert'] : FALSE; + $project_dothooks = isset($project->settings->deploy) ? $project->settings->deploy['default_hooks']['dothooks'] : FALSE; + $project_acquia_hooks = isset($project->settings->deploy) ? $project->settings->deploy['default_hooks']['acquia_hooks'] : FALSE; + $project_composer_install = isset($project->settings->deploy) ? $project->settings->deploy['default_hooks']['composer'] : FALSE; + } + + $return['update'] = array( + '#type' => 'checkbox', + '#title' => t('Run database updates.'), + '#default_value' => empty($environment->settings->deploy) ? $project_update : $environment_update, + ); + $return['cache'] = array( + '#type' => 'checkbox', + '#title' => t('Clear all caches.'), + '#default_value' => empty($environment->settings->deploy)? $project_cache : $environment_cache, + ); + $return['revert'] = array( + '#type' => 'checkbox', + '#title' => t('Revert all features.'), + '#description' => t('If features is enabled, revert all of them.'), + '#default_value' => empty($environment->settings->deploy)? $project_revert : $environment_revert, + ); + // Look for .hooks or .hooks.yml + if (!$is_environment_form || isset($project->settings->deploy, $project->settings->deploy['default_hooks']['dothooks'])) { + $return['dothooks'] = array( + '#type' => 'checkbox', + '#title' => t('Run deploy commands in the .hooks file.'), + '#default_value' => empty($environment->settings->deploy) ? $project_dothooks : $environment_dothooks, + '#description' => t('You can add your desired deploy hooks to a file in the root folder of your project. This is recommended as it gives your developers control over what happens when their code is deployed. See the !dothooks for more information. If you use a .hooks file you probably want to uncheck the deploy hooks here.', array( + '!dothooks' => l(t('.hooks documentation'), 'admin/help/devshop_dothooks'), + )), + ); + } + + // Look for acquia cloud hooks. + if (module_exists('devshop_acquia') && (!$is_environment_form || isset($project->settings->deploy, $project->settings->deploy['default_hooks']['acquia_hooks']))) { + $return['acquia_hooks'] = array( + '#type' => 'checkbox', + '#title' => t('Run Acquia Cloud Hooks'), + '#description' => '

' . t('Acquia Cloud Hooks were detected in your project. Check this box to run all acquia cloud hooks.') . '

' . t('Available cloud hooks are: "post-code-update" when deploying code via git, "post-code-deploy" when deploying code manually, "post-db-copy" when running a sync task with a database, and "post-files-copy" when running a sync task with files.') . '

', + '#default_value' => empty($environment->settings->deploy) ? $project_acquia_hooks : $environment_acquia_hooks, + ); + } + + // Allow other modules to alter the deploy hooks form elements. + // @TODO: Create a new hook: "hook_devshop_deploy_hooks()" + drupal_alter('devshop_deploy_hooks_form_elements', $return, $is_environment_form); + + // Disable deploy hooks on environment form, deploy task form ,and sync task form. + if ( + // Environment form + ($is_environment_form && !$project->settings->deploy['allow_environment_deploy_config']) || + ($task_type == 'devshop-deploy' && !$project->settings->deploy['allow_environment_deploy_hooks_override']) + ) { + + foreach (element_children($return) as $i) { + $return[$i]['#type'] = 'value'; + $return[$i]['#value'] = $project->settings->deploy['default_hooks'][$i]; + + $return["{$i}_display"] = array( + '#type' => 'markup', + '#markup' => t($return[$i]['#title']), + '#prefix' => '
' . ($return[$i]['#default_value']? '': ''), + '#suffix' => '
' + ); + } + $return['#description'] = t('Environment-specific deploy hook configuration is not allowed.'); + + $project_node = node_load($project->nid); + if (node_access('update', $project_node)) { + $return['#description'] .= t('Visit !link to change.', array( + '!link' => l(t('Project Settings'), "node/{$project->nid}/edit", array( + 'fragment' => 'edit-project-settings-deploy-default-hooks', + )), + )); + } + } + return $return; +} + +/** + * Submit handler for site/environment settings page. + */ +function devshop_projects_environment_settings_submit($form, &$form_state) { + + // Save the environment record. + $site = $form_state['node']; + + // Prepare record for saving + // When creating a new environment, there is no environment value. + if (empty($form_state['values']['environment']['name'])) { + $environment = new stdClass(); + $environment->project_nid = $form_state['values']['project_nid']; + $environment->name = $form_state['values']['environment_name']; + $environment->site = $site->nid; + $environment->platform = $site->platform; + } + else { + $environment = (object) $form_state['values']['environment']; + } + + $form_state['values']['environment']['settings']['install_method'] = $form_state['values']['install_method']; + + $environment->settings = serialize($form_state['values']['environment']['settings']); + + // Get platform verify task + $tasks = hosting_get_tasks('rid', $site->nid); + $environment->last_task = current($tasks)->nid; + + // Save environment record. + // If environment already exists, set primary keys. + $project = node_load($environment->project_nid); + if (isset($project->project->environments[$environment->name])) { + $primary_keys = array('project_nid', 'name'); + } + else { + $primary_keys = array(); + } + + if (drupal_write_record('hosting_devshop_project_environment', $environment, $primary_keys)) { + + // Remove the "Site dev.drupal.devshop.local has been updated. + drupal_get_messages('status'); + + // Send our own message + if (!empty($primary_keys)) { + drupal_set_message(t('Settings saved for environment !env.', array('!env' => l($environment->name, "node/{$environment->site}")))); + } + else { + drupal_set_message(t('Environment !env created in project %project.', array( + '!env' => l($environment->name, "node/{$environment->site}"), + '%project' => $project->project_name, + ))); + } + } + else { + drupal_set_message(t('Environment was not saved for %env in project %project!', array('%env' => $environment->name, '%project' => $project->project_name))); + + watchdog('error', 'Environment record not saved: ' . print_r($environment, 1)); + } + + $form_state['redirect'] = 'node/' . $environment->project_nid; +} + + +/** + * Implements hook_form_alter(). + */ +function devshop_projects_form_server_node_form_alter(&$form, &$form_state, $form_id) { + + $items = array(); + $items[] = t('To create a new server, you must first connect it to this one via SSH.'); + $items[] = t("You must grant %aegir access to your new server by adding it's SSH Key (below) to ~/.ssh/authorized_keys.", array( + '%aegir' => "aegir@" . $_SERVER['HTTP_HOST'], + )); + $items[] = t('If you wish to provision a new server, add the key to the +root user, then run the devshop remote:install command.'); + $items[] = t('If you have already provisioned the server, add the key to the aegir user.'); + + $form['note'] = array( + '#markup' => theme('item_list', array('items' => $items)), + '#weight' => -100, + '#prefix' => '
', + '#suffix' => '
', + ); + + $form['key'] = array( + '#type' => 'item', + '#title' => t(' SSH Public Key for %aegir', array( + '%aegir' => "aegir@" . $_SERVER['HTTP_HOST'], + )), + '#markup' => '
' . variable_get('devshop_public_key', '') . '
', + '#weight' => -99, + ); +} + +function devshop_projects_form_hosting_task_update_status_form_alter(&$form, &$form_state, $form_id){ + $form['update-status']['#value'] = t('Cancel'); +} + +/** + * Implements hook_hosting_site_options_alter(). + */ +function devshop_projects_hosting_site_options_alter(&$return, $node) { + $return['profile'][] = 0; + $return['platform'][] = 0; +} + +function devshop_deploy_git_ref_form($form, $form_state, $project, $environment) { + + $form['git_ref'] = array( + '#type' => 'select', + '#bootstrap_ignore_pre_render' => TRUE, + '#options' => devshop_projects_git_ref_options($project), + '#default_value' => isset($environment->git_ref) ? $environment->git_ref : 'HEAD', + ); + return $form; +} \ No newline at end of file diff --git a/modules/devshop/devshop_projects/inc/logs.inc b/modules/devshop/devshop_projects/inc/logs.inc new file mode 100644 index 000000000..8e182da2b --- /dev/null +++ b/modules/devshop/devshop_projects/inc/logs.inc @@ -0,0 +1,15 @@ +project->environments[$environment]; + + $env = t('Environment Tasks Log'); + $output = "

{$environment->name} {$env}

"; + $output .= views_embed_view('hosting_task_log', 'page_1', $environment->site); + + return $output; +} diff --git a/modules/devshop/devshop_projects/inc/nodes.inc b/modules/devshop/devshop_projects/inc/nodes.inc new file mode 100644 index 000000000..77828b877 --- /dev/null +++ b/modules/devshop/devshop_projects/inc/nodes.inc @@ -0,0 +1,364 @@ + 'DevShop Project', + "base" => 'devshop_projects', + "description" => t('A project is a website with multiple environments.'), + "has_title" => TRUE, + "title_label" => t('Project Codename'), + "locked" => TRUE, + ); + return $types; +} + +/** + * Implements hook_node_load(). + * + * Loads data into Project nodes. + */ +function devshop_projects_node_load($nodes, $types) { + + // Only act on project and related nodes. + $types = array( + 'project', + 'site', + 'platform', + 'task', + ); + if (count(array_intersect($types, $types)) == 0) { + return; + } + + global $user; + $project = new stdClass(); + + $refs_types = db_select('node', 'n') + ->fields('n', array('nid', 'type')) + ->condition('type', array('site', 'platform', 'project', 'server'), 'IN') + ->execute() + ->fetchAllKeyed(); + + foreach ($nodes as $nid => &$node) { + + // Tasks + if ($node->type == 'task') { + + // Prepare bootstrap-compatible things. + // Load up task types to get our language. + $task_types = hosting_available_tasks(); + + // In Aegir, "executed" and "delta" does not get updated properly. + // Handle "delta" being not computed yet + if ($node->task_status == HOSTING_TASK_PROCESSING) { + $node->duration = format_interval(time() - $node->executed, 1); + } + elseif ($node->task_status == HOSTING_TASK_QUEUED) { + $node->duration = t('Queued for %time', + array('%time' => format_interval(time() - $node->changed), 1)); + } + else { + $node->duration = format_interval($node->delta, 1); + } + + // To test out constantly changing tasks, uncomment this line. + // $node->task_status = rand(-1, 3); + $node->status_class = devshop_task_status_class($node->task_status); + $node->status_name = _hosting_parse_error_code($node->task_status); + + // Override "ago" text. + $node->ago = format_interval(time() - $node->executed, + 1) . ' ' . t('ago'); + if ($node->task_status == HOSTING_TASK_QUEUED) { + $node->ago = t('Queued for %time', + array('%time' => format_interval(time() - $node->changed))); + } + elseif ($node->task_status == HOSTING_TASK_PROCESSING) { + $node->ago = format_interval(time() - $node->changed); + } + + // ISO 8601 + if ($node->executed == 0) { + $executed = $node->created; + } + else { + $executed = $node->executed; + } + $node->task_timestamp = date('c', $executed); + $node->task_date = format_date($executed); + + // Set task type + $node->type_name = $task_types[$refs_types[$node->rid]][$node->task_type]['title']; + + // If platform's first verify... + if (isset($node->ref) && $node->ref->type == 'platform' && $node->ref->verified == 0) { + $node->type_name = t('Cloning Codebase'); + } + + // Get Icon + $node->icon = devshop_task_status_icon($node->task_status); + + // Get desired URL + $node->url = url("node/{$node->nid}"); + + } + // Projects + elseif ($node->type == 'project') { + + // Load the "hosting context". The unique name in the aegir system. + $node->project_name = $node->title; + $node->hosting_name = 'project_' . $node->title; + + $node->project = devshop_project_load($node); + } + elseif ($node->type == 'site') { + + // @TODO: Check if this is ever needed now that install_method is in core aegir. + // Load install method into an easy to access property. + if (empty($node->install_method) && isset($node->environment->settings->install_method)) { + $node->install_method = $node->environment->settings->install_method['method']; + } + + // If profile ID is empty but name is stored, load to $node->profile_name so that provision will load it. + if (empty($node->profile) && !empty($node->environment->settings->install_method['profile'])) { + $node->profile_name = $node->environment->settings->install_method['profile']; + } + } + } +} + +/** + * Implementation of hook_insert(). + * + * 1. Saves data into our table. + * 2. Saves a hosting context name. + * 3. Adds a "Verify" task for this project. + * + * @see hosting_platform_insert() + */ +function devshop_projects_node_insert($node) { + + // On task insert, save last task. + if ($node->type == 'task') { + $site = node_load($node->rid); + + // @TODO: task_type should be task_status... but the system still works. Was this never running? It's possible the other hooks that save last task info is all we need. + if (($site->type == 'site' || $site->type == 'platform') && isset($site->environment) && ($node->task_type == HOSTING_TASK_QUEUED || $node->task_type == HOSTING_TASK_PROCESSING)) { + $site->environment->last_task = $node->nid; + devshop_environment_save_last_task($site->environment); + } + + // When a task is created or updated that has a project, redirect to the project. + if (isset($_GET['redirect']) && $_GET['redirect'] == 'task-page') { + drupal_goto("node/{$node->nid}"); + } + elseif (isset($site->project) && arg(0) == 'hosting_confirm' && arg(1) == $site->nid) { + drupal_goto("node/{$site->project->nid}"); + } + } + + if ($node->type != 'project') { + return; + } + + if (!isset($node->no_verify)) { + hosting_add_task($node->nid, 'verify'); + } + + $info = new stdClass(); + $info->nid = $node->nid; + $info->git_url = $node->project->git_url; + $info->code_path = hosting_path_normalize($node->project->code_path); + $info->drupal_path = hosting_path_normalize($node->project->drupal_path); + $info->base_url = $node->project->base_url; + $info->install_profile = $node->project->install_profile; + + // Save serialized data, minus environments + $info->settings = serialize($node->project->settings); + + drupal_write_record('hosting_devshop_project', $info); + + // Save hosting context + if (!$node->old_vid) { + + // Save the "hosting context", which includes setting the path alias. + $name = ($node->hosting_name) ? $node->hosting_name : $node->title; + + // Ensure "project_" prefix on hosting context name. + if (strpos($name, 'project_') !== 0) { + $context_name = 'project_' . $name; + } + else { + $context_name = $name; + } + + hosting_context_register($node->nid, $context_name); + + // Replace the alias created by hosting_context_register. + $path['source'] = "node/{$node->nid}"; + $path['alias'] = "project/{$name}"; + path_save($path); + } + + // @TODO: The wizard always creates the project before the environments. + // Not sure if we need this, but we might to enable importing via drush. + // Save Environment records. + if (!empty($node->project->environments)) { + foreach ($node->project->environments as $name => $environment) { + // Ensure correct data types + $environment = (object) $environment; + $environment->settings = (array) $environment->settings; + + $info = new stdClass(); + $info->project_nid = $node->nid; + $info->name = $name; + $info->site = $environment->site; + $info->platform = $environment->platform; + + // Remove primary settings from settings array before saving. + unset($environment->settings['git_ref']); + unset($environment->settings['site']); + unset($environment->settings['platform']); + $info->settings = serialize($environment->settings); + + // Save environment record. + drupal_write_record('hosting_devshop_project_environment', $info); + } + } +} + +/** + * Implementation of hook_update(). + * + * 1. Updates our table. + * 2. Adds a "Verify" task for this project. + * + */ +function devshop_projects_node_update($node) { + + + // On task insert, save last task. + if ($node->type == 'task') { + $site = node_load($node->rid); + + // When a task is created or updated that has a project, redirect to the project. + if (isset($site->project) && arg(0) == 'hosting_confirm' && arg(1) == $site->nid) { + drupal_goto("node/{$site->project->nid}"); + } + } + + if ($node->type != 'project') { + return; + } + + $project = (object) $node->project; + $project->settings = (object) $project->settings; + + $info = new stdClass(); + $info->nid = $node->nid; + $info->git_url = $project->git_url; + $info->code_path = hosting_path_normalize($project->code_path); + $info->drupal_path = hosting_path_normalize($project->drupal_path); + $info->base_url = $project->base_url; + $info->install_profile = $project->install_profile? $project->install_profile: $project->settings->default_environment['install_profile']; + + // Save serialized data, minus environments + $info->settings = serialize($project->settings); + + // Write project record. + drupal_write_record('hosting_devshop_project', $info, 'nid'); + if (!isset($node->no_verify) || $node->no_verify == FALSE) { + hosting_add_task($node->nid, 'verify'); + } +} + +/** + * Implements hook_node_delete(). + */ +function devshop_projects_node_delete($node) { + + // When a platform or site node is FULLY deleted, remove the environment record. + // This should never happen, site and platform nodes are never fully deleted. + if (($node->type == 'site' || $node->type == 'platform') && !empty($node->project)) { + db_delete('hosting_devshop_project_environment') + ->condition($node->type, $node->nid) + ->execute(); + } +} + +/** + * Implementation of hook_delete(). + */ +function devshop_projects_delete($node) { + + db_delete('hosting_devshop_project') + ->condition('nid', $node->nid) + ->execute(); + + db_delete('hosting_devshop_project_environment') + ->condition('project_nid', $node->nid) + ->execute(); + + hosting_context_delete($node->nid); + hosting_task_delete_related_tasks($node->nid); +} + +/** + * @param $environment + */ +function devshop_environment_save(stdClass &$environment) { + $environment->settings = (array) $environment->settings; + + // Prepare record for saving + $info = new stdClass(); + $info->project_nid = $environment->project_nid; + $info->name = $environment->name; + $info->site = $environment->site; + $info->platform = $environment->platform; + $info->settings = serialize($environment->settings); + $info->last_task = $environment->last_task; + + // Check for existing records + $result = db_select('hosting_devshop_project_environment', 'e') + ->fields('e', array('project_nid')) + ->condition('e.name', $info->name) + ->condition('e.project_nid', $info->project_nid) + ->execute() + ->fetchField() + ; + + $keys = $result? array('project_nid', 'name'): array(); + + // Save environment record. + if (drupal_write_record('hosting_devshop_project_environment', $info, $keys)) { + watchdog('ok', 'Environment record saved: ' . print_r($info, 1)); + return TRUE; + } + else { + watchdog('error', 'Environment record not saved: ' . print_r($info, 1)); + return FALSE; + } +} + +/** + * Helper to save last task info. + */ +function devshop_environment_save_last_task($environment) { + + db_update('hosting_devshop_project_environment') + ->fields(array( + 'last_task' => $environment->last_task, + )) + ->condition('name', $environment->name) + ->condition('project_nid', $environment->project_nid) + ->execute(); +} diff --git a/modules/devshop/devshop_projects/inc/queue.inc b/modules/devshop/devshop_projects/inc/queue.inc new file mode 100644 index 000000000..0534e01f9 --- /dev/null +++ b/modules/devshop/devshop_projects/inc/queue.inc @@ -0,0 +1,62 @@ + 'batch', +// 'name' => t('Deploy Queue'), +// 'description' => t('Deploy Queue: Runs a Deploy task on projects configured to use "Manual Deployment" for environments that are not set to "Disable Deploy on Commit".'), +// 'total_items' => count(devshop_projects_get_deploy_queue_environments()), +// 'frequency' => strtotime("5 minutes", 0), +// 'singular' => t('environment'), +// 'plural' => t('environments'), +// ); +// return $items; +//} +// +///** +// * Get the environments to be pulled in the queue. +// * +// * @param $limit +// * Limit to a maximum of this number of platforms. +// * @return +// * An array of site nodes that have a pull queue enabled. +// * +// */ +//function devshop_projects_get_deploy_queue_environments() { +// +// $results = db_query("SELECT nid FROM {hosting_devshop_project}")->fetchCol(); +// $environments = array(); +// foreach ($results as $nid) { +// $node = node_load($nid); +// if (!isset($node->project)) { +// continue; +// } +// $project = $node->project; +// if (isset($project->settings->deploy) && $project->settings->deploy['method'] == 'queue'){ +// foreach ($project->environments as $environment) { +// if ($project->settings->git['refs'][$environment->git_ref] == 'branch' && $environment->settings->pull_disabled == 0) { +// $environments[] = $environment; +// } +// } +// } +// } +// return $environments; +//} +// +///** +// * Implements hosting_QUEUE_TYPE_queue(). +// */ +//function hosting_deploy_queue($count) { +// $environments = devshop_projects_get_deploy_queue_environments($count); +// foreach ($environments as $environment) { +// $args = array(); +// $args['git_ref'] = $environment->git_ref; +// $args['update'] = $environment->settings->deploy['update']; +// $args['revert'] = $environment->settings->deploy['revert']; +// $args['cache'] = $environment->settings->deploy['cache']; +// hosting_add_task($environment->site, 'devshop-deploy', $args); +// } +//} diff --git a/modules/devshop/devshop_projects/inc/reload.js b/modules/devshop/devshop_projects/inc/reload.js new file mode 100644 index 000000000..36af22f37 --- /dev/null +++ b/modules/devshop/devshop_projects/inc/reload.js @@ -0,0 +1,32 @@ +(function ($) { + Drupal.behaviors.devshopReload = { + attach: function (context, settings) { + setTimeout('Drupal.behaviors.devshopReload.checkProject()', Drupal.settings.devshopReload.delay); + }, + checkProject: function() { + var url = '/projects/add/status/' + Drupal.settings.devshopReload.type; + console.log('Checking Project Status...'); + $.getJSON(url, function (data) { + $.each(data, function (i, platform) { + if (platform.version) { + jQuery('#version-' + i).html(platform.version); + } + if (platform.profiles) { + jQuery('#profiles-' + i).html(platform.profiles); + } + if (platform.status) { + if (platform.status == 'Processing') { + platform.status += ' '; + } + jQuery('#status-' + i).html(platform.status); + } + }); + if (data.tasks_complete){ + document.location.reload(); + } else { + setTimeout("Drupal.behaviors.devshopReload.checkProject()", Drupal.settings.devshopReload.delay); + } + }); + } + }; +}(jQuery)); diff --git a/modules/devshop/devshop_projects/inc/task-ajax.js b/modules/devshop/devshop_projects/inc/task-ajax.js new file mode 100644 index 000000000..1516731fb --- /dev/null +++ b/modules/devshop/devshop_projects/inc/task-ajax.js @@ -0,0 +1,240 @@ +(function ($) { + Drupal.behaviors.devshopTasks = { + attach: function (context, settings) { + setTimeout("Drupal.behaviors.devshopTasks.checkTasks()", 1000); + }, + checkTasks: function () { + var url = '/devshop/tasks'; + if (Drupal.settings.devshopTask) { + url = '/devshop/tasks?task=' + Drupal.settings.devshopTask; + } + + $.getJSON(url, function (data) { + + var lastTaskStatus = null; + $.each(data, function (key, task) { + var environment_id = '#' + task.project_name + '-' + task.environment; + var task_id = '#task-display-' + task.nid; + var new_class = 'environment-tasks-alert alert-' + task.status_class; + + var $alert_div = $(task_id + '.environment-tasks-alert'); + + // Project Node Page + if (Drupal.settings.devshopTask == null) { + + // If $alert_div does not exist, create it. + if ($alert_div.length == 0) { + $lastTaskWrapper = $('.last-task-alert', environment_id); + $lastTaskWrapper.html(task.rendered); + + $('.environment-task-logs .tasks-wrapper', environment_id).prepend(task.rendered); + + $alert_div = $(task_id + '.environment-tasks-alert'); + + $("time.timeago", $alert_div).timeago(); + } + + // Set class of wrapper div + $alert_div.attr('class', new_class); + + // Set or remove active class from environment div. + if (task.status_class == 'queued' || task.status_class == 'processing') { + $(environment_id).addClass('active'); + } + else { + $(environment_id).removeClass('active'); + } + + // Remove any status classes and add current status + $(environment_id).removeClass('task-queued'); + $(environment_id).removeClass('task-processing'); + $(environment_id).removeClass('task-success'); + $(environment_id).removeClass('task-error'); + $(environment_id).removeClass('task-warning'); + $(environment_id).addClass('task-' + task.status_class); + + // Set value of label span + $('.alert-link > .type-name', $alert_div).html(task.type_name); + + // If queued or processing, make label empty. + if (task.status_class == 'queued' || task.status_class == 'processing') { + if (task.status_class == 'queued') { + $('.alert-link > .status-name', $alert_div).html(''); + } + else { + $('.alert-link > .status-name', $alert_div).html(task.status_name); + } + $('.alert-link .ago-icon', $alert_div).removeClass('fa-calendar'); + $('.alert-link .ago-icon', $alert_div).addClass('fa-clock-o'); + } + else { + $('.alert-link > .status-name', $alert_div).html(task.status_name); + + $('.alert-link .ago-icon', $alert_div).removeClass('fa-clock-o'); + $('.alert-link .ago-icon', $alert_div).addClass('fa-calendar'); + } + + // Set value of "ago" + $('.alert-link .ago', $alert_div).html(task.ago); + + // Change icon. + $('.alert-link > .fa', $alert_div).attr('class', 'fa fa-' + task.icon); + + // Change href + $('.alert-link', $alert_div).attr('href', task.url); + + // Change "processing" div + } + // Task Node Page + else if (Drupal.settings.devshopTask == task.nid) { + + $badge = $('.label.task-status', '#node-' + task.nid); + + // Change Badge + var html = ' ' + task.status_name; + $badge.html(html); + + // Change Class + $badge.attr('class', 'label label-default task-status label-' + task.status_class); + + // Reload Logs + $logs = $('#task-logs', '#node-' + task.nid); + $logs.html(task.logs); + + // @TODO: + // Change Duration + $('.duration .duration-text', '#node-' + task.nid).html(task.duration); + + + // If task is not processing or queued, hide follow link. + if (task.task_status != 0 && task.task_status != -1) { + // Scroll down one last time if checked. + if ($('#follow').prop('checked')) { + window.scrollTo(0, document.body.scrollHeight); + } + $('.follow-logs-checkbox').remove(); + $('.edit-update-status').remove(); + $('.running-indicator').remove(); + Drupal.settings.lastTaskStopped = true; + } + else { + // Scroll down if follow checkbox is checked. + if ($('#follow').prop('checked')) { + window.scrollTo(0, document.body.scrollHeight); + } + } + + // If running, set text to indicate + if (task.task_status == -1) { + $('.running-indicator .running-label').text('Processing...'); + $('.running-indicator .fa-gear').addClass('fa-spin'); + } + + // If the last task was complete, and this task is complete, stop the autoloader. + if (Drupal.settings.lastTaskStopped && (task.task_status != -1 && task.task_status != 0)) { + console.log('Task is not processing or queued. Stopping the autoloader.'); + Drupal.settings.lastTaskStopped = true; + } + } + + // Projects List Page: Update badges. + if ($('body.page-projects').length) { + var id = '#badge-' + task.project_name + '-' + task.environment; + + // Set class of badge + $(id).attr('class', 'btn btn-small alert-' + task.status_class); + + // Set title + var title = task.type_name + ': ' + task.status_class; + $(id).attr('title', title); + + // Change icon. + $('.fa', id).attr('class', 'fa fa-' + task.icon); + } + + // Update global tasks list. + var task_id = '#task-' + task.project_name + '-' + task.environment; + + // Set class of badge + $(task_id).attr('class', 'list-group-item list-group-item-' + task.status_class); + + // Set value of "ago" + $('small.task-ago', task_id).html(task.ago); + + // Change icon. + $('.fa', task_id).attr('class', 'fa fa-' + task.icon); + + }); + + // Activate or de-activate global tasks icon. + var gear_class = 'fa fa-gear'; + if ($('.list-group-item-queued', '.devshop-tasks').length) { + gear_class += ' active-task'; + } + if ($('.list-group-item-processing', '.devshop-tasks').length) { + gear_class += ' active-task fa-spin'; + } + + // Set class for global gear icon. + $('i.fa', '#navbar-main .task-list-button').attr('class', gear_class); + + // Set count + var count = $('.list-group-item-queued', '.devshop-tasks').length + $('.list-group-item-processing', '.devshop-tasks').length; + if (count == 0) { + count = ''; + } + $('.count', '.task-list-button').html(count); + + if (Drupal.settings.lastTaskStopped != true) { + setTimeout("Drupal.behaviors.devshopTasks.checkTasks()", 1000); + } + }); + } + }; + + Drupal.behaviors.taskInfoScroll = { + attach: function (context, settings) { + + var $task_info = $("#task-info"); + if ($task_info.length == 0) { + return; + } + var task_info_top = $task_info.offset().top; + var $project_links = $("#project-environment-links"); + var project_links_top = $project_links.offset().top; + + // Check on first load. + if (window.pageYOffset >= task_info_top){ + $task_info.addClass("task-info-fixed"); + } + if (window.pageYOffset >= project_links_top){ + $project_links.addClass("project-environment-links-fixed"); + } + + // On scroll, check. + window.onscroll = function() { + if (Drupal.settings.disableScrollCheck) { + return; + } + + if (window.pageYOffset == 0) { + $task_info.removeClass("task-info-fixed"); + } + + if (window.pageYOffset >= task_info_top + 50){ + $task_info.addClass("task-info-fixed"); + } + else if ($('#follow').length && !$('#follow').prop('checked')) { + $task_info.removeClass("task-info-fixed"); + } + + if (window.pageYOffset >= project_links_top){ + $project_links.addClass("project-environment-links-fixed"); + } + else if (!$('#follow').prop('checked')) { + $project_links.removeClass("project-environment-links-fixed"); + } + } + } + } +}(jQuery)); diff --git a/modules/devshop/devshop_projects/inc/tasks-ajax.inc b/modules/devshop/devshop_projects/inc/tasks-ajax.inc new file mode 100644 index 000000000..acc34843d --- /dev/null +++ b/modules/devshop/devshop_projects/inc/tasks-ajax.inc @@ -0,0 +1,93 @@ +last_task; + } + + // Look up any remaining queued or processing tasks. + $query = db_select('node', 'n'); + $query->join('hosting_task', 't', 'n.vid=t.vid'); + $query + ->fields('n', array('nid')) + ->condition('type', 'task') + ->orderBy('n.vid', 'DESC') + ->addTag('node_access'); + + $or = db_or(); + $or->condition('task_status', HOSTING_TASK_PROCESSING); + $or->condition('task_status', HOSTING_TASK_QUEUED); + $query->condition($or); + + if (!empty($nids)) { + $query->condition('n.nid', $nids, 'NOT IN'); + } + $extras = $query->execute(); + foreach ($extras as $row) { + $nids[] = $row->nid; + } + + foreach ($nids as $nid) { + $task_node = node_load($nid); + + if (isset($task_node->site)) { + $task_node->site_node = node_load($task_node->site); + + if ($task_node->site_node->site_status == HOSTING_SITE_DELETED) { + continue; + } + } + + // If no last task node was found, skip. + if (empty($task_node)) { + continue; + } + + // If page is requesting this task's logs, load them. + if (isset($_GET['task']) && $_GET['task'] == $task_node->nid) { + $messages = devshop_task_get_messages($task_node); + if (count($messages)) { + $task_node->logs = implode("\n", $messages); + } + } + + // Output a rendered task node + $task_node->rendered = theme('devshop_task', array('task' => $task_node)); + + $output[] = $task_node; + } + + print json_encode($output); + exit; +} diff --git a/modules/devshop/devshop_projects/inc/theme.inc b/modules/devshop/devshop_projects/inc/theme.inc new file mode 100644 index 000000000..acdfefa8a --- /dev/null +++ b/modules/devshop/devshop_projects/inc/theme.inc @@ -0,0 +1,528 @@ + array( + 'variables' => array( + 'form' => NULL, + ), + ), + 'devshop_projects_create_settings_form' => array( + 'render element' => 'form', + ), + 'devshop_project_nav' => array( + 'variables' => array( + 'node' => NULL, + 'settings_active' => NULL, + 'logs_active' => NULL, + 'branches_class' => NULL, + ), + 'template' => 'project-nav', + ), + 'devshop_project_add_status' => array( + 'variables' => array( + 'project' => NULL, + ), + 'template' => 'project-add', + ), + 'devshop_ascii' => array( + 'variables' => array( + 'output' => '', + ), + ), + 'devshop_task' => array( + 'variables' => array( + 'task' => NULL, + ), + 'template' => 'task', + ), + 'devshop_add_environment_options' => array( + 'render element' => 'form', + 'template' => 'add-environment-options', + 'path' => drupal_get_path('module', 'devshop_projects') . '/includes', + ), + ); +} + +/** + * Theme function for environments settings + */ +function theme_devshop_projects_settings_form($form) { + $rows = array(); + $header = array(); + $header[] = t('Environment'); + foreach (element_children($form) as $env_name) { + $row = array(); + $row[] = $env_name . drupal_render($form[$env_name]['git_ref']); + foreach (element_children($form[$env_name]['settings']) as $setting) { + if (!isset($header[$setting])) { + $header[$setting] = $form[$env_name]['settings'][$setting]['#title']; + } + $form[$env_name]['settings'][$setting]['#title'] = ''; + $row[] = drupal_render($form[$env_name]['settings'][$setting]); + } + $rows[] = $row; + } + $output = theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'project-settings-table'))); + return $output; +} + +/** + * Theme function for create environments settings + * @TODO: Fold into theme_devshop_projects_settings_form() + */ +function theme_devshop_projects_create_settings_form($vars) { + + $form = $vars['form']; + $rows = array(); + $header = array(); + foreach (element_children($form) as $env_name) { + $row = array(); + $header['name'] = 'Name'; + $header['git_ref'] = t('Branch/Tag'); + + $name_element = $form[$env_name]['name']; + $git_ref_element = $form[$env_name]['git_ref']; + $settings_element = $form[$env_name]['settings']; + + $row[] = drupal_render($name_element); + $row[] = drupal_render($git_ref_element); + + foreach (element_children($settings_element) as $setting) { + if (!isset($header[$setting])) { + $header[$setting] = isset($form[$env_name]['settings'][$setting]['#title']) ? $form[$env_name]['settings'][$setting]['#title'] : ''; + } + $form[$env_name]['settings'][$setting]['#title'] = ''; + + $element = $form[$env_name]['settings'][$setting]; + $row[] = drupal_render($element); + } + $rows[] = $row; + } + $output = theme('table', array( + 'header' => $header, + 'rows' => $rows, + 'attributes' => array( + 'id' => 'project-settings-table', + 'class' => array('table'), + ) + )); + $output .= '

' . t('Create as many new environments as you would like. For example: "dev", "test", and "live". You can create more later on if needed.') . '

'; + + return $output; +} + +/** + * Preprocess page + */ +function devshop_projects_preprocess_node(&$vars) { + + $node = $vars['node']; + + // On project node edit page + if (isset($vars['node']) && $vars['node']->type == 'project') { + + $vars['drush_aliases'] = devshop_project_aliases($vars['node']->project); + $vars['aliases_url'] = url("node/{$vars['node']->nid}/aliases"); + + if (module_exists('aegir_ssh') && user_access('manage own SSH public keys')) { + global $user; + $vars['access_note'] = t('NOTE: To access these environments with drush remotely, make sure you have uploaded your public SSH key under !link.', array( + '!link' => l('My Account > SSH Keys', "user/$user->uid/ssh-keys"), + )); + } + else { + $vars['access_note'] = t('NOTE: To access these environments with drush remotely, ask an administrator to add your public SSH key to the file /var/aegir/.ssh/authorized_keys.'); + } + } + + // On task node page + if (isset($vars['node']) && $vars['node']->type == 'task' && ($vars['page'] || current_path() == "hosting/task/{$node->nid}")){ + // Prepare task icon, class, and label. + $icon = devshop_task_status_icon($vars['node']->task_status); + $vars['task_label'] = " " . _hosting_parse_error_code($vars['node']->task_status); + $vars['task_label_class'] = devshop_task_status_class($vars['node']->task_status); + $vars['task_icon'] = devshop_task_status_icon($vars['node']->task_status); + + // Load ref node + $ref_node = node_load($vars['rid']); + if ($ref_node->type == 'site') { + $vars['site_url'] = l($ref_node->environment->url, $ref_node->environment->url); + } + + // Add a "Retry" button, unless it's a clone task. Retrying a clone can be bad if an old clone task is restarted, it uses the latest task arguments, not the one you are viewing. + if ($vars['node']->task_type != 'clone' && user_access('retry failed tasks') && ($vars['node']->task_status == HOSTING_TASK_ERROR)) { + $vars['retry'] = drupal_get_form('hosting_task_retry_form', $vars['node']->nid); + } + + // Show duration + if ($vars['node']->task_status == HOSTING_TASK_QUEUED) { + $vars['duration'] = t('Queued for %time', array('%time' => format_interval(REQUEST_TIME - $vars['node']->changed))); + $vars['date'] = date('D M j Y', $vars['node']->changed); + $vars['executed'] = ''; + } + else { + + if ($vars['node']->task_status == HOSTING_TASK_PROCESSING) { + $vars['duration'] = format_interval(REQUEST_TIME - $vars['node']->executed, 1); + $vars['executed'] = ''; + } + else { + $vars['duration'] = format_interval($vars['node']->delta, 1); + $vars['executed'] = format_interval(REQUEST_TIME - $vars['node']->executed) . ' ' . t('ago'); + } + $vars['date'] = date('D M j Y', $vars['node']->executed); + } + + // Load Logs + $messages = devshop_task_get_messages($vars['node']); + $vars['messages'] = implode("\n", $messages); + switch ($vars['task_type']) { + case 'devshop-deploy': + $vars['type'] = 'Deploy'; + break; + case 'verify': + case 'test': + case 'sync': + case 'install': + default: + $vars['type'] = ucfirst($vars['task_type']); + break; + } + + // Add "Follow Logs" button. + if ($vars['node']->task_status == HOSTING_TASK_PROCESSING || $vars['node']->task_status == HOSTING_TASK_QUEUED ) { + $vars['follow_checkbox'] = array( + '#type' => 'markup', + '#markup' => '', + ); + + $vars['follow_checkbox'] = drupal_render($vars['follow_checkbox']); + } + + // Running indicator + if ($vars['task_status'] == HOSTING_TASK_QUEUED ||$vars['task_status'] == HOSTING_TASK_PROCESSING) { + $vars['is_active'] = 'active'; + + if ($vars['task_status'] == HOSTING_TASK_PROCESSING) { + $vars['is_running'] = 'fa-spin'; + $vars['running_label'] = t('Processing...'); + } + else { + $vars['running_label'] = t('Queued'); + } + } + global $user; + $vars['run_again'] = l(t('Run Again'), "hosting_confirm/{$node->rid}/{$node->ref_type}_{$node->task_type}", array( + 'query' => array( + 'token' => drupal_get_token($user->uid), + 'redirect' => 'task-page', + ), + 'attributes' => array( + 'class' => array('btn btn-default'), + ) + )); + } +} + +/** + * Helper to retrieve helpful messages from aegir task logs. + * @param $task_node + * @return array + */ +function devshop_task_get_messages($task_node) { + $messages = array(); + $outputs = array(); + + $result = db_query(" + SELECT message, type, timestamp FROM {hosting_task_log} + WHERE nid = :nid + AND vid = :vid + AND (type LIKE :devshop OR type = :error OR type = :warning) + ORDER BY lid", array( + ':nid' => $task_node->nid, + ':vid' => $task_node->vid, + ':devshop' => 'p_%', + ':error' => 'error', + ':warning' => 'warning', +// ':success' => 'success', +// ':ok' => 'ok', + )); + $process_logs = FALSE; + + foreach ($result as $record) { + + if (strpos($record->type, 'p_') === 0) { + $process_logs = TRUE; + } + + if ($record->type == 'p_command') { + if (empty($record->message)) { + continue; + } + $command = $record->message; + $outputs[$command] = array( + 'status' => 'default', + 'icon' => 'gear', + 'output' => '', + ); + + if ($task_node->task_status == HOSTING_TASK_PROCESSING) { + $outputs[$command]['icon'] .= ' fa-spin'; + } + continue; + } elseif ($record->type == 'p_info') { + $outputs[$command]['output'] .= $record->message; + $outputs[$command]['status'] = 'default'; + $outputs[$command]['icon'] = 'gear fa-spin'; + } elseif ($record->type == 'p_ok') { + $outputs[$command]['output'] .= $record->message; + $outputs[$command]['status'] = 'success'; + $outputs[$command]['icon'] = 'check'; + } elseif ($record->type == 'p_error') { + $outputs[$command]['status'] = 'danger'; + $outputs[$command]['icon'] = 'exclamation-circle'; + $outputs[$command]['output'] = isset($outputs[$command]['output']) ? $outputs[$command]['output'] .= $record->message : $record->message; + } elseif ($record->type == 'warning') { + $command = 'warning_' . count($outputs); + $outputs[$command]['status'] = 'warning'; + $outputs[$command]['icon'] = 'exclamation-triangle'; + $outputs[$command]['output'] = isset($outputs[$command]['output']) ? $outputs[$command]['output'] .= $record->message : $record->message; + $outputs[$command]['command'] = 'warning'; + + } + + // If error is the one caused by "drush_set_error", don't bother displaying because the process one works well on it's own. + elseif ($process_logs && $record->type == 'error' && $record->message == 'PROVISION_PROCESS_ERROR') { + continue; + + } elseif ($record->type == 'error') { + $command = 'error_' . count($outputs); + $outputs[$command]['status'] = 'danger'; + $outputs[$command]['icon'] = 'exclamation-circle'; + $outputs[$command]['output'] = $record->message; + + // @TODO: Here, we can check for obscure aegir errors and write our own. + // devshop_check_errors($outputs[$command]['output']); + $outputs[$command]['command'] = 'error'; + } elseif ($record->type == 'p_log') { + $command = 'notice_' . count($outputs); + $outputs[$command]['status'] = ''; + $outputs[$command]['icon'] = ''; + $outputs[$command]['command'] = $record->message; + $outputs[$command]['output'] = ''; + } + // Let's show some success messages. There are way too many for verify and install tasks. + elseif (($record->type == 'success' || $record->type == 'ok') && $task_node->task_type != 'verify' && $task_node->task_type != 'install') { + $command = 'success_' . count($outputs); + $outputs[$command]['output'] .= $record->message; + $outputs[$command]['status'] = 'success'; + $outputs[$command]['icon'] = 'check'; + $outputs[$command]['command'] = 'drush'; + } + $outputs[$command]['type'] = $record->type; + } + + foreach ($outputs as $command => $data) { + $status = $data['status']; + $icon = $data['icon']; + + // Allow $outputs array to override the displayed command. + if (isset($data['command'])) { + $command = $data['command']; + } + + // If $command has brackets, convert to span. + if (strpos($command, '[') !== FALSE && strpos($command, ']') !== FALSE) { + $command = strtr($command, array( + '[' => '
', + ']' => '
', + )); + } + + // Convert ASCII to HTML + $output = theme('devshop_ascii', array('output' => $data['output'])); + + if (!empty($output)) { + $body = "
$output
"; + } + else { + $body = ''; + } + $messages[] = << +
$command
+ $body + +HTML; + } + return $messages; +} + +/** + * Preprocess for project_add_status.tpl.php + */ +function template_preprocess_devshop_project_add_status(&$vars) { + if (isset($vars['project']->settings, $vars['project']->settings->default_environment)) { + $vars['web_server_node'] = isset($vars['project']->settings->default_environment['web_server']) ? node_load($vars['project']->settings->default_environment['web_server']) : NULL; + $vars['db_server_node'] = isset($vars['project']->settings->default_environment['db_server']) ? node_load($vars['project']->settings->default_environment['db_server']) : NULL; + } +} + +/** + * Preprocess for project_nav.tpl.php + */ +function template_preprocess_devshop_project_nav(&$vars) { + + global $user; + $project = $vars['project']; + $node = $vars['node'] = node_load($project->nid); + + // @TODO: Detect other web URLs for other git hosts. + if ($project->git_repo_url) { + $vars['github_url'] = $project->git_repo_url; + } + + // Generate branches/tags lists + $vars['branches_count'] = count($project->settings->git['branches']); + $vars['tags_count'] = count($project->settings->git['tags']); + $vars['branches_items'] = array(); + $vars['branches_icon'] = 'code-fork'; + + if ($vars['branches_count'] == 0) { + // If branches are 0 and last verifying is queued... + if ($node->project->verify->task_status == HOSTING_TASK_PROCESSING || $node->verify->task_status === HOSTING_TASK_QUEUED) { + $vars['branches_show_label'] = TRUE; + $vars['branches_label'] = t('Refreshing...'); + $vars['branches_class'] = 'btn-warning'; + $vars['branches_icon'] = 'gear fa-spin'; + $vars['branches_items'][] = l(t('View task log'), 'node/' . $node->project->verify->nid); + + } + // If branches are 0 and last verifying failed... + elseif ($node->project->verify->task_status == HOSTING_TASK_ERROR) { + $vars['branches_show_label'] = TRUE; + $vars['branches_label'] = t('Error'); + $vars['branches_class'] = 'btn-danger'; + $vars['branches_items'][] = t('There was a problem refreshing branches and tags.'); + $vars['branches_items'][] = l(t('View task log'), 'node/' . $node->project->verify->nid); + } + // If branches are 0 and last verifying has completed... This should never happen, because the task would error out. + elseif ($node->project->verify->task_status == HOSTING_TASK_SUCCESS) { + $vars['branches_show_label'] = TRUE; + $vars['branches_label'] = t('No branches found!'); + } + } + // If there are branches... build the branch items + else { + $vars['branches_show_label'] = FALSE; + $vars['branches_label'] = format_plural($vars['branches_count'], t('1 Branch'), t('!count Branches', array('!count' => $vars['branches_count']))); + + foreach ($project->settings->git['branches'] as $branch) { + $href = isset($vars['github_url']) ? $vars['github_url'] . '/tree/' . $branch : '#'; + $vars['branches_items'][] = " $branch "; + } + } + + + if ($vars['tags_count']) { +//
  • + + $vars['branches_label'] .= ' & ' . format_plural($vars['tags_count'], t('1 Tag'), t('!count Tags', array('!count' => $vars['tags_count']))); + + + foreach ($project->settings->git['tags'] as $branch) { + $href = isset($vars['github_url']) ? $vars['github_url'] . '/tree/' . $branch : '#'; + $vars['branches_items'][] = " $branch "; + $vars['git_refs'][] = $branch; + } + } + + $vars['dashboard_link'] = l(' ' . t('Dashboard'), "node/$project->nid", array('html' => TRUE)); + + if (arg(2) == 'edit') { + $vars['settings_active'] = 'active'; + } + if (arg(2) == 'logs') { + $vars['logs_active'] = 'active'; + } + + // Add "refresh branches" link if project is manual deploy mode or is missing a webhook + if ($project->settings->deploy['method'] == 'manual' || empty($project->settings->deploy['last_webhook'])) { + $link_refresh = l(t('Refresh branches'), 'hosting_confirm/' . $node->nid . '/project_verify', array('attributes' => array('class' => array('refresh-link')), 'query' => array('token' => drupal_get_token($user->uid)))); + array_unshift($vars['branches_items'], $link_refresh); + } + + // Scrub git urls + $vars['safe_git_url'] = scrub_repo_url($project->git_url); +} + +/** + * Output pretty console themed ASCII. + * + * Usage: + * theme('devshop_ascii', $string); + */ +function theme_devshop_ascii($vars) { + $string = $vars['output']; + if (empty($string)) { + return $string; + } + + // Prepare ASCII Converter + $theme = new SolarizedXTermTheme(); + $styles = $theme->asCss(); + $styles .= ".ansi_box { overflow: auto; padding: 10px 15px; font-family: monospace; }"; + + drupal_add_html_head(""); + $converter = new AnsiToHtmlConverter($theme); + $output = $converter->convert($string); + + // Make URLs clickable: + $filter = new stdClass(); + $filter->settings = [ + 'filter_url_length' => 1000, + ]; + $output = _filter_url($output, $filter); + + // Change linebreaks to
    tags. + $output = strtr($output, array( + '\n' => '
    ', + )); + + return <<$output +HTML; + +} + +/** + * Replaces a http git URL with an embedded password the ***** + * + * @param $project_git_url + * @return string + */ +function scrub_repo_url($project_git_url){ + + $url = parse_url($project_git_url); + if (isset($url['pass'])) { + return strtr($project_git_url, array( + $url['pass'] => '****', + )); + } + else { + return $project_git_url; + } +} diff --git a/modules/devshop/devshop_projects/inc/ui.inc b/modules/devshop/devshop_projects/inc/ui.inc new file mode 100644 index 000000000..0754aaf57 --- /dev/null +++ b/modules/devshop/devshop_projects/inc/ui.inc @@ -0,0 +1,373 @@ + 1)); + $rows = array(); + + foreach ($results as $result) { + $project_node = node_load($result->nid); + $project = $project_node->project; + + // Make sure user can view the project node. + if (!node_access('view', $project_node)) { + continue; + } + + // Load an environment for extra info like version. + $first_environment = current($project->environments); + if ($first_environment) { + $platform_node = node_load($first_environment->platform); + $version = $platform_node->release->version; + } + else { + $version = ''; + } + $row = array(); + + // Link to Project page + $url = url("node/$project->nid"); + $title = t('View dashboard for !project project.', array( + '' + )); + $first_col = << + $project->name + + +HTML; + + // Live URL + $live_domain = $project->settings->live['live_domain']; + $live_environment = $project->settings->live['live_environment']; + + if ($live_domain) { + $domain = $live_domain; + } + elseif (isset($project->environments[$live_environment])) { + $domain = $project->environments[$live_environment]->uri; + } + elseif (count($project->environments)) { + $domain = current($project->environments)->uri; + } + else { + $domain = ''; + $first_col .= '' . t('No Environments') . ''; + } + + $first_col .= l($domain, "http://$domain", array( + 'attributes' => array( + 'class' => array('small live-link text-muted'), + 'target' => '_blank', + ), + )); + + $row[] = $first_col; + + // Drupal Version + $row[] = " + $version +
    + $project->install_profile +"; + + // Git URL + $row[] = strtr("", array('!url' => scrub_repo_url($project->git_url))); + + // Links to all environments + $items = ''; + foreach ($project->environments as $env => $environment) { + if (empty($environment->last_task)) { + $icon = 'cube'; + $class = 'status'; + } + else { + if ($environment->last_task_node->task_status == HOSTING_TASK_SUCCESS) { + $icon = 'check'; + $class = 'success'; + } + elseif ($environment->last_task_node->task_status == HOSTING_TASK_QUEUED) { + $icon = 'gear'; + $class = 'status'; + } + elseif ($environment->last_task_node->task_status == HOSTING_TASK_PROCESSING) { + $icon = 'gear fa-spin'; + $class = 'status active'; + } + elseif ($environment->last_task_node->task_status == HOSTING_TASK_WARNING) { + $icon = 'exclamation-triangle'; + $class = 'warning'; + } + elseif ($environment->last_task_node->task_status == HOSTING_TASK_ERROR) { + $icon = 'exclamation-circle'; + $class = 'danger'; + } + } + + $nid = $environment->site ? $environment->site : $environment->platform; + $items .= l(" " . $environment->name, "node/{$nid}", array( + 'attributes' => array( + 'class' => array('btn btn-sm alert-' . $class), + 'title' => $tasks[$environment->last_task_node->task_type]['title'] . ': ' . _hosting_parse_error_code($environment->last_task_node->task_status), + 'id' => 'badge-' . $environment->project_name . '-' . $environment->name, + ), + 'html' => TRUE, + )); + } + $count = count($project->environments); + $row[] = << +
    + $items +
    + + +HTML; + + + + + $rows[] = $row; + } + + // No Projects + if (empty($rows)) { + $link = url('projects/add'); + $welcome = t('Welcome to DevShop!'); + $text = t('Create your first project to get started.'); + $button = t('Start a Project'); + $output = << +

    $welcome

    +

    $text

    +

    $button

    + + +HTML; + } + else { + $output = theme('table', array('rows' => $rows, 'attributes' => array('class' => array('table')))); + } + return $output; +} + +/** + * Implementation of hook_view(). + * + * Project Page Display + */ +function devshop_projects_node_view($node, $view_mode, $langcode) { + global $user; + + if ($node->type != 'project') { + return; + } + + $project = &$node->project; + + // Check to see if this project is still in the wizard + ctools_include('object-cache'); + $project_wizard_cache = ctools_object_cache_get('project', 'devshop_project'); + if (current_path() == "node/{$node->nid}" && isset($project_wizard_cache->nid) && $node->nid == $project_wizard_cache->nid) { + drupal_goto('projects/add/' . $project_wizard_cache->step); + } +} + +/** + * Generates the Environment Menu item list. + * + * @param $environment + * @return array + */ +function devshop_environment_menu($environment) { + global $user; + $tasks = hosting_available_tasks('site'); + $first_items = devshop_environment_menu_first_items($environment, $tasks); + $contrib_items = module_invoke_all('devshop_environment_menu', $environment, $tasks); + + $items = array_merge($first_items, $contrib_items); + + // Last menu items + if ($environment->site && $environment->site_status == HOSTING_SITE_ENABLED) { + + if (!empty($contrib_items)) { + $items[] = '
    '; + } + + $items[] = 'verify'; + $items[] = 'flush_cache'; + $items[] = 'rebuild_registry'; + $items[] = 'run_cron'; + $items[] = 'update'; + $items[] = '
    '; + + // Backup & Restore + if (drupal_valid_path("node/$environment->site/backups")) { + $items[] = l(t('Backups'), "node/$environment->site/backups"); + } + } + + // Add all actions from hook_devshop_environment_menu() + foreach ($items as $item) { + if (isset($tasks[$item])) { + + // Special handling for delete/disable/enable + if ($item == 'disable' || $item == 'enable') { + $tasks[$item]['title'] = ' ' . $tasks[$item]['title'] . ' ' . t('Environment'); + } + elseif ($item == 'delete') { + $tasks[$item]['title'] = ' ' . t('Destroy Environment'); + } + + $href = "hosting_confirm/{$environment->site}/site_{$item}"; + $options = array( + 'query' => array( + 'token' => drupal_get_token($user->uid), + ) + ); + + // If on the site node page, set destination to be the site node. + if (arg(0) == 'node' && arg(1) == $environment->site) { + $options['query']['destination'] = current_path(); + } + + $url = url($href, $options); + $title = $tasks[$item]['title']; + if (isset($tasks[$item]['icon'])) { + $icon = ''; + } + else { + $icon = ''; + } + $output_items[] = << + $icon $title + +HTML; + } + else { + $output_items[] = $item; + } + } + return $output_items; +} + +/** + * Implements hook_nodeapi_TYPE_OP() for site nodes and view op. + */ +function devshop_projects_nodeapi_site_view(&$node, $a3, $a4) { + if (!empty($node->project)) { + // Display Project, Environment and Branch. + $node->content['info']['project'] = array( + '#type' => 'item', + '#title' => t('Project'), + '#markup' => l($node->project->name, "node/{$node->project->nid}"), + '#weight' => -12, + ); + $node->content['info']['env'] = array( + '#type' => 'item', + '#title' => t('Environment'), + '#markup' => $node->environment->name, + '#weight' => -11, + ); + $node->content['info']['branch'] = array( + '#type' => 'item', + '#title' => t('Branch/Tag'), + '#markup' => $node->environment->git_ref, + '#weight' => -11, + ); + + // Add Breadcrumbs + $crumbs = array(); + $crumbs[] = l(t('Home'), ''); + $crumbs[] = l(t('Projects'), 'projects'); + $crumbs[] = l($node->project->name, "node/" . $node->project->nid); + drupal_set_breadcrumb($crumbs); + } +} + +/** + * Implements hook_nodeapi_TYPE_OP() + */ +function devshop_projects_nodeapi_platform_view(&$node, $a3, $a4) { + devshop_projects_nodeapi_site_view($node, $a3, $a4); +} + + +/** + * Implements hook_nodeapi_TYPE_OP() for site nodes and view op. + */ +function devshop_projects_nodeapi_task_view(&$node, $a3, $a4) { + + $rows = array(); + if (!isset($node->task_args)) { + $node->task_args = array(); + } + foreach ($node->task_args as $key => $value) { + $rows[] = array( + '' . $key . '', + '' . $value . '', + ); + } + + $task_host = node_load($node->rid); + + $node->content['info']['project'] = array( + '#type' => 'item', + '#title' => t('Project'), + '#markup' => l($task_host->project->name, "node/{$task_host->project->nid}"), + '#weight' => -12, + '#access' => isset($task_host->project), + ); + $node->content['info']['parameters'] = array( + '#type' => 'item', + '#title' => t('Task Parameters'), + '#markup' => theme('table', array('header' => array(), 'rows' => $rows)), + '#weight' => -11, + '#access' => count($rows), + ); + +} + +/** + * Our own version of _hosting_site_goto_link() + */ +function devshop_hosting_site_goto_link($node) { +// $project = node_load($node->environment->project_nid); + + // If this is the live environment, use the live domain. + if ($node->environment->name == $node->project->settings->live['live_environment']) { + $url = $node->project->settings->live['live_domain']; + } + // If environment aliases are enabled + elseif ($node->project->settings->live['environment_aliases']) { + $url = "{$node->environment->name}.{$node->project->settings->live['live_domain']}"; + } + // Otherwise use the base_url + else { + $url = "{$node->environment->name}.{$node->project->base_url}"; + } + + // Get the link from cache. + $cache = cache_get("hosting:site:" . $node->nid . ":login_link"); + if (!is_null($cache) && (REQUEST_TIME < $cache->data['expire'])) { + $title = t("Log in: !url", array('!url' => $url)); + } + else { + $title = t("!url", array('!url' => $url)); + } + $options['attributes']['target'] = '_blank'; + $options['attributes']['class'] = 'hosting-goto-site-link'; + return l($title, "node/" . $node->nid . "/goto_site", $options); +} diff --git a/modules/devshop/devshop_projects/project-add.tpl.php b/modules/devshop/devshop_projects/project-add.tpl.php new file mode 100644 index 000000000..55ffa9060 --- /dev/null +++ b/modules/devshop/devshop_projects/project-add.tpl.php @@ -0,0 +1,56 @@ +name): ?> +
    +
    +

    + +

    + +
    + +
      +
    • + name; ?> +
    • +
    • + + + git_url; ?> + +
    • +
    • + +

      code_path; ?>

      +
    • + + drupal_path): ?> +
    • + +

      drupal_path; ?>

      +
    • + + + settings->live) && !empty($project->settings->live['live_domain'])): ?> +
    • + +

      settings->live['live_domain']; ?>

      +
    • + + + settings->default_environment) && isset($project->settings->default_environment['web_server'])): ?> +
    • + + + + title ?> + + + + title ?> + +
    • + +
    +
    + diff --git a/modules/devshop/devshop_projects/project-nav.tpl.php b/modules/devshop/devshop_projects/project-nav.tpl.php new file mode 100644 index 000000000..9354ec6d9 --- /dev/null +++ b/modules/devshop/devshop_projects/project-nav.tpl.php @@ -0,0 +1,121 @@ + diff --git a/modules/devshop/devshop_projects/projects.inc b/modules/devshop/devshop_projects/projects.inc new file mode 100644 index 000000000..db6ae7e01 --- /dev/null +++ b/modules/devshop/devshop_projects/projects.inc @@ -0,0 +1,420 @@ +type, $types)) { + + // Load project and environment + if ($entity->type == 'task') { + + // If this is a Verify Project task, we don't want to load anything. + if (strpos($entity->title, 'Verify Project:') === 0) { + $entity->project = $entity->rid; + $entity->environment = NULL; + continue; + } + + $nid = $entity->rid; + } + else { + $nid = $entity->nid; + } + + $query = db_query(" +SELECT + project_nid, name, n.title as project_name FROM {hosting_devshop_project_environment} e + LEFT JOIN {node} n ON e.project_nid = n.nid + WHERE site = :site + OR platform = :platform + OR project_nid = :project +", array( + ':site' => $nid, + ':platform' => $nid, + ':project' => $nid, + )); + + $result = $query->fetch(); + + // Only load the project node if there is no task + if ($entity->type == 'task' && !empty($result)) { + $entity->project = $result->project_nid; + $entity->project_name = $result->project_name; + $entity->environment = $result->name; + } + else if (isset($result->project_nid)) { + $project_node = node_load($result->project_nid); + if (!empty($project_node->project)) { + $entity->project = $project_node->project; + if (isset($project_node->project->environments[$result->name])) { + $entity->environment = $project_node->project->environments[$result->name]; + } + } + } + } + } + } +} + +/** + * Load a project object. + */ +function devshop_project_load($node) { + + if (empty($node->nid) || $node->type != 'project') { + return; + } + + // Load project data + $project = (object) db_query('SELECT * FROM {hosting_devshop_project} WHERE nid = :nid', array(':nid' => $node->nid))->fetchAssoc(); + + // Load all project tasks. + $project->tasks = db_query('SELECT * FROM {hosting_task} WHERE rid = :rid ORDER BY vid DESC', array(':rid' => $node->nid))->fetchAllAssoc('task_type'); + + // Load verification status of project node. + if (!empty($project->tasks['verify'])) { + $project->verify = $project->tasks['verify']; + } + + // Load up all project settings. + $project->name = $node->project_name; + $project->settings = (object) unserialize($project->settings); + + + // Create "refs" array to determine ref type. + $project->settings->git['refs'] = array(); + foreach ($project->settings->git as $type => $refs) { + if (is_array($refs)) { + foreach ($refs as $ref) { + $project->settings->git['refs'][$ref] = ($type == 'branches')? 'branch': 'tag'; + } + } + } + + // Git Repo Host + if (strpos($project->git_url, 'github.com') !== FALSE) { + $project->git_provider = 'github'; + $project->git_repo_url = strtr($project->git_url, array( + 'git@github.com:' => 'http://github.com/', + 'git://' => 'http://', + '.git' => '', + )); + } + elseif (strpos($project->git_url, variable_get('bitbucket_repo_url_trigger_word', 'bitbucket.org')) !== FALSE) { + $project->git_provider = 'bitbucket'; + $project->git_repo_url = strtr($project->git_url, array( + 'git@bitbucket.com:' => 'http://bitbucket', + 'git://' => 'http://', + '.git' => '', + )); + } + else { + $project->git_provider = 'git'; + $project->git_repo_url = ''; + } + // @TODO: Set git providers for most common here, then add a hook to detect. + + // Load Environments + // @TODO: Remove environments where the site has been deleted. + $environment_data = db_query(" + SELECT + e.*, + s.status as site_status, + p.status as platform_status, + p.nid as platform, + cp.name as platform_context, + cs.name as site_context, + p.publish_path as root, + p.publish_path as publish_path, + g.git_ref as git_ref, + g.repo_url as git_url, + g.repo_path as repo_path, + http.title as remote_host, + sn.title as uri, + http.title as web_server, + http.nid as web_server_nid, + db.title as db_server, + db.nid as db_server_nid, + s.db_name, + s.profile as install_profile, + pn.title as project_name, + sn.created as created, + s.vid, + sn.title as system_domain, + a.redirection as redirect_domain, + e.last_task as last_task_nid + FROM {hosting_devshop_project_environment} e + LEFT JOIN {hosting_site} s ON e.site = s.nid + LEFT JOIN {hosting_context} cs ON s.nid = cs.nid + LEFT JOIN {node} sn ON s.vid = sn.vid + LEFT JOIN {hosting_platform} p ON s.platform = p.nid OR e.platform = p.nid + LEFT JOIN {hosting_context} cp ON p.nid = cp.nid + LEFT JOIN {hosting_git} g ON p.nid = g.nid + LEFT JOIN {node} http ON p.web_server = http.nid + LEFT JOIN {node} db ON s.db_server = db.nid + LEFT JOIN {node} pn ON e.project_nid = pn.nid + LEFT JOIN {hosting_site_alias} a ON a.vid = s.vid + WHERE project_nid = :nid AND + e.name != '' + ORDER BY + name; ", array( + ':nid' => $node->nid + )); + foreach ($environment_data as $environment) { + + // Don't load the environment if the site and/or platform is deleted. + if ($environment->site_status == HOSTING_SITE_DELETED && $environment->platform_status == HOSTING_PLATFORM_DELETED) { + continue; + } + if ($environment->platform_status == HOSTING_PLATFORM_DELETED) { + continue; + } + + // Drush alias + $environment->drush_alias = "@{$project->name}.{$environment->name}"; + + // Unserialize environment settings. + $environment->settings = (object) unserialize($environment->settings); + + // Get last task + $environment->last_task_node = node_load($environment->last_task_nid); + + // Get all tasks, keyed by type. + $environment->tasks = devshop_get_tasks($environment); + + // Create tasks list, keyed by VID + $environment->tasks_list = array(); + foreach ($environment->tasks as $task_type => $tasks) { + foreach ($tasks as $task) { + $environment->tasks_list[$task->nid] = $task; + } + } + krsort($environment->tasks_list); + + // The URL that the branch or tag links to. + if ($project->git_provider == 'github') { + $environment->git_ref_url = $project->git_repo_url . '/tree/' . $environment->git_ref; + } + else { + $environment->git_ref_url = url("node/$environment->site/logs/commits"); + } + + // Environment Drupal version. + $iid = db_query("SELECT iid FROM {hosting_package_instance} i left join {hosting_package} p on p.nid=i.package_id WHERE p.package_type = :type AND i.rid = :nid", array(':type' => 'platform', ':nid' => $environment->platform))->fetchField(); + $release = hosting_package_instance_load($iid); + + if (isset($release->version)) { + $environment->version = $release->version; + } + else { + $environment->version = t('unknown'); + } + + // @TODO: Make "servers" more abstract so we can add many more. + // HTTP Server + $environment->servers['http'] = array( + 'nid' => $environment->web_server_nid, + 'name' => $environment->web_server, + 'server' => node_load($environment->web_server_nid), + ); + $environment->ip_addresses = $environment->servers['http']['server']->ip_addresses; + + // On project create, there is no site, so there is no db_server. + if (empty($environment->db_server)) { + $db_servers = hosting_get_servers('db', FALSE); + // Project wizard saves db_server to "settings". If that doesn't exist, just use the default db server. + if (empty($environment->settings->db_server)) { + $environment->settings->db_server = $environment->db_server_nid = key($db_servers); + } + else { + $environment->db_server_nid = $environment->settings->db_server; + } + + if (!empty($db_servers[$environment->db_server_nid])) { + $environment->db_server = $db_servers[$environment->db_server_nid]; + } + } + + // DB Server + $environment->servers['db'] = array( + 'nid' => $environment->db_server_nid, + 'name' => $environment->db_server, + 'server' => node_load($environment->db_server_nid), + ); + + // Load Solr if present + if (module_exists('hosting_solr')) { + $server_nid = db_query("SELECT server FROM {hosting_solr} WHERE nid = :nid", array(':nid' => $environment->site))->fetchField(); + + $servers = hosting_get_servers('solr', FALSE); + if ($server_nid && isset($servers[$server_nid])) { + $environment->servers['solr'] = array( + 'nid' => $server_nid, + 'name' => $servers[$server_nid], + ); + } + } + + // Git information. + $environment->git_ref_stored = $environment->git_ref; + + // Load encryption status + if (module_exists('hosting_ssl')) { + $environment->ssl_enabled = db_query('SELECT ssl_enabled FROM {hosting_ssl_site} WHERE nid = :nid', array(':nid' => $environment->site))->fetchField(); + } + elseif (module_exists('hosting_https')) { + $environment->ssl_enabled = db_query('SELECT https_enabled FROM {hosting_https_site} WHERE nid = :nid', array(':nid' => $environment->site))->fetchField(); + } + else { + $environment->ssl_enabled = FALSE; + } + + // Environments URLs + // Get aegir "aliases" + $environment->domains = hosting_alias_get_aliases($environment); + $environment->domains[] = $environment->system_domain; + $protocol = $environment->ssl_enabled ? 'https://' : 'http://'; + if ($environment->redirect_domain) { + $environment->url = $protocol . $environment->redirect_domain; + } + else { + $environment->url = $protocol . $environment->system_domain; + } + + // Look for non-80 ports. + if ($environment->servers['http']['server']->services['http']->port != '80' && $environment->servers['http']['server']->services['http']->port != '0') { + $environment->url .= ':' . $environment->servers['http']['server']->services['http']->port; + } + + // System Alias + $environment->system_alias = '@' . $environment->system_domain; + + // If per-environment hooks config is not allowed, load it from the project. + if (isset($project->settings->deploy) && !$project->settings->deploy['allow_environment_deploy_config']) { + $environment->settings->deploy = $project->settings->deploy['default_hooks']; + } + + // Load Install Method and info. + if (isset($environment->settings->install_method)) { + $environment->install_method = $environment->settings->install_method['method']; + } + else { + $environment->install_method = 'profile'; + } + + switch ($environment->install_method) { + + // Install Method: Clone + case 'clone': + $environment->clone_source = $environment->settings->install_method['clone_source']; + + // Load clone source for "other". + if ($environment->clone_source == '_other') { + $environment->clone_source = $environment->settings->install_method['clone_source_drush']; + } + + // Load clone source nid and environment name. + $environment->clone_source_node = db_query("SELECT nid, e.name FROM {hosting_context} h LEFT JOIN hosting_devshop_project_environment e ON h.nid = e.site WHERE h.name = :name", array(':name' => ltrim($environment->clone_source, '@')))->fetchObject(); + + // If no site node was found with the selected alias, display the alias. + if (empty($environment->clone_source_node->name)) { + $source_label = $environment->clone_source; + } + else { + $source_label = l($environment->clone_source_node->name, "node/{$environment->clone_source_node->nid}"); + } + + // Set Install Method Label + $environment->install_method_label = t('Clone of !source', array( + '!source' => $source_label, + )); + break; + + // Install Method: "Empty Database" / "Manual Install" + case "manual": + $environment->install_method_label = t('Manually Installed'); + break; + + // Install Method: "Import" + case "import": + $environment->install_method_label = t('SQL Import'); + break; + + // Install Method: "Profile" + case "profile": + default: + + // We don't know package ID until platform verifies. + if ($environment->install_profile == 0 && !empty($environment->settings->install_method['profile'])) { + $environment->install_method_label = t('Installed with !profile', array( + '!profile' => $environment->settings->install_method['profile'], + )); + } + elseif (!empty($environment->settings->install_method['profile'])) { + $profile = node_load($environment->install_profile); + $environment->install_method_label = t('Installed with !profile', array( + '!profile' => l($profile->title, "node/{$environment->install_profile}"), + )); + } + else { + $environment->install_method_label = ''; + } + break; + } + + // @TODO: Create a "reinstall" task to replace this. +// $environment->clone_rebuild_url = url( +// "hosting_confirm/{$environment->site}/site_sync", array( +// 'query' => array( +// 'source' => $environment->clone_source, +// 'rebuild' => TRUE, +// ), +// ) +// ); + + // Save to project environments collection. + drupal_alter('devshop_environment', $environment, $project); + $environments[$environment->name] = $environment; + } + + // Put live environment at the top. + if (!empty($environments) && isset($project->settings->live) && $project->settings->live['live_environment'] && isset($environments[$project->settings->live['live_environment']])) { + $project->environments = array(); + $live_env = $project->settings->live['live_environment']; + $project->environments[$live_env] = $environments[$live_env]; + unset($environments[$live_env]); + $project->environments += $environments; + } + elseif (!empty($environments)) { + $project->environments = $environments; + } + elseif (!empty($node->title)) { + $project->environments = array(); + } + + // Make project name and status available. + $project->name = $node->title; + $project->status = $node->status; + + // Webhook Status + // @TODO: Create Hosting Webhooks module. + // @TODO: Remove devshop_pull. + module_load_include('inc', 'devshop_pull'); + $project->webhook_url = _devshop_pull_callback_url($node); + + // Save project object be available at $node->project. + return $project; + +} diff --git a/modules/devshop/devshop_projects/task.tpl.php b/modules/devshop/devshop_projects/task.tpl.php new file mode 100644 index 000000000..691fa26b7 --- /dev/null +++ b/modules/devshop/devshop_projects/task.tpl.php @@ -0,0 +1,16 @@ + \ No newline at end of file diff --git a/modules/devshop/devshop_projects/tasks/commit.inc b/modules/devshop/devshop_projects/tasks/commit.inc new file mode 100644 index 000000000..579290f38 --- /dev/null +++ b/modules/devshop/devshop_projects/tasks/commit.inc @@ -0,0 +1,43 @@ +hosting_name/features/diff/all", array('absolute' => TRUE, 'attributes' => array('target' => '_blank'))) . ' Be patient. It takes a few moments for the diffs to be generatred.'; + } + else { + $descr .= 'enable the Features Diff module for this site, Verify the site, and select this task again.'; + } + + $form['message'] = array( + '#title' => t('Commit Message'), + '#type' => 'textarea', + '#description' => $descr, + ); + $form['push'] = array( + '#title' => t('Push code after commit?'), + '#type' => 'checkbox', + '#default_value' => 1, + ); + $form['revert'] = array( + '#title' => t('Force revert features after commit?'), + '#type' => 'checkbox', + '#default_value' => 1, + ); + // @TODO: Provide a DIFF display to give the user an idea of what has changed. + return $form; +} diff --git a/modules/devshop/devshop_projects/tasks/create.inc b/modules/devshop/devshop_projects/tasks/create.inc new file mode 100644 index 000000000..c9582ce5a --- /dev/null +++ b/modules/devshop/devshop_projects/tasks/create.inc @@ -0,0 +1,127 @@ +type = 'site'; + $site->title = devshop_environment_url($project, $new_environment_name); + $site->status = 1; + $site->uid = $user->uid; + $site->name = $user->name; + $site->client = HOSTING_DEFAULT_CLIENT; + + // Attach a new platform node. + $site->platform_node = devshop_prepare_platform_node($project, $new_environment_name, $new_environment_branch, $settings->web_server); + + // Set db server to the project default, if none was set. + if (empty($settings->db_server) && !empty($project->settings->default_environment['db_server'])) { + $settings->db_server = $project->settings->default_environment['db_server']; + } + // If no db server is set and there is no default set. + elseif (empty($settings->db_server)) { + $servers = hosting_get_servers('db', FALSE); + reset($servers); + $settings->db_server = key($servers); + } + + // If "force servers" is set for the project, force them. + if ($project->settings->default_environment['force_default_servers']) { + $settings->db_server = $project->settings->default_environment['db_server']; + $settings->web_server = $project->settings->default_environment['web_server']; + } + + // Now that desired servers have been determined, save to their respective nodes. + $site->db_server = $settings->db_server; + + // Save the site node, along with the platform. + // This is possible thanks to the patch in https://www.drupal.org/node/2824731 + if ($site = node_submit($site)) { + node_save($site); + } + +// // If no new branch specified and fork source is present, set branch to forked environment's branch. +// if (empty($new_environment_branch) && $source_environment) { +// $branch = $project->environments[$source_environment]->git_ref; +// } +// // If no new branch and fork source not mentioned, return false. +// elseif (empty($new_environment_branch) && empty($source_environment)) { +// return FALSE; +// } +// else { +// $branch = $new_environment_branch; +// } + + // If cloning or forking, check if source environment exists... + if (isset($project->environments[$source_environment])) { + $source_environment = $project->environments[$source_environment]; + + $environment = new stdClass(); + $environment->name = $new_environment_name; + $environment->site = $site->nid; + + // Copy settings from source environment. + $environment->settings = $source_environment->settings; + + // We assume since this is a clone, it does not need to be locked, and it should be auto-pulling. + // Unlock the environment... @TODO: Should this be a project setting? + // Make sure pull_disabled isn't on. + $environment->settings->locked = 0; + $environment->settings->pull_disabled = 0; + + // Save clone source from source_environment's drush alias. + $environment->settings->install_method['method'] = 'clone'; + $environment->settings->install_method['clone_source'] = $source_environment->system_alias; + + if ($action == 'fork') { + $environment->settings->branch_to_fork = $source_environment->git_ref; + } + } + else { + // Next, add the environment record. + $environment = new stdClass(); + $environment->name = $new_environment_name; + $environment->site = $site->nid; + + // Use settings passed in to this function. + $environment->settings = $settings; + + // Save profile to install_method. + $environment->settings->install_method['method'] = 'profile'; + $environment->settings->install_method['profile'] = $project->install_profile; + + } + + // Get platform verify task + $tasks = hosting_get_tasks('rid', $site->platform_node->nid); + $environment->last_task = current($tasks)->nid; + + db_insert('hosting_devshop_project_environment') + ->fields(array( + 'project_nid' => $project->nid, + 'name' => $environment->name, + 'site' => $environment->site, + 'settings' => serialize($environment->settings), + 'last_task' => $environment->last_task, + )) + ->execute(); +} diff --git a/modules/devshop/devshop_projects/tasks/deploy.inc b/modules/devshop/devshop_projects/tasks/deploy.inc new file mode 100644 index 000000000..43afa433f --- /dev/null +++ b/modules/devshop/devshop_projects/tasks/deploy.inc @@ -0,0 +1,74 @@ +project; + $environment = $node->environment; + + $current_ref_type_class = $environment->git_ref_type == 'tag' ? 'tag' : 'code-fork'; + + $form['environment'] = array( + '#type' => 'item', + '#title' => t('Environment'), + '#markup' => l($environment->default_domain, 'http://' . $environment->default_domain, array('attributes' => array('target' => '_blank'))) . "   " . $environment->git_ref, + '#description' => t('The environment URL and current git reference.'), + ); + + // Detect pre-selected git ref. + if (!empty($_GET['git_ref'])) { + + $ref_type = $project->settings->git['refs'][$_GET['git_ref']]; + $ref_type_class = $ref_type == 'tag' ? 'tag' : 'code-fork'; + + $form['git_ref'] = array( + '#type' => 'value', + '#value' => $_GET['git_ref'], + ); + $form['git_ref_label'] = array( + '#type' => 'item', + '#title' => t('Git @ref', array('@ref' => $ref_type)), + '#markup' => " " . $_GET['git_ref'], + '#description' => t("This git reference will be checked out in this environment."), + ); + } + else { + $branch_options = devshop_projects_git_ref_options($project, $environment->git_ref); + $default_value = !empty($_GET['git_ref']) ? $_GET['git_ref'] : $environment->git_ref; + + $form['git_ref'] = array( + '#title' => t('Git Branch or Tag'), + '#description' => t('Choose the branch or tag to deploy to this environment.'), + '#type' => 'select', + '#options' => $branch_options, + '#required' => TRUE, + '#default_value' => $default_value, + ); + } + + // Merge in the deploy hooks form. + $deploy_hooks_form = devshop_environment_deploy_hooks_form($project, $environment, 'devshop-deploy'); + $form = array_merge($form, $deploy_hooks_form); + + // Pass through a value so hostmaster knows this was triggered manually. + // This is mainly to allow acquia cloud hooks compatibility. They have separate + // hooks for manual deployment vs. automatic deployment. + $form['manual'] = array( + '#type' => 'value', + '#value' => 1, + ); + + return $form; +} diff --git a/modules/devshop/devshop_projects/tasks/pull.inc b/modules/devshop/devshop_projects/tasks/pull.inc new file mode 100644 index 000000000..a3fee198a --- /dev/null +++ b/modules/devshop/devshop_projects/tasks/pull.inc @@ -0,0 +1,66 @@ +project->environments as $name => $environment) { + if (isset($form['environments']['#options'][$name]) && ($environment->settings->pull_disabled || in_array($environment->git_ref, $node->project->settings->git['tags']))) { + unset($form['environments']['#options'][$name]); + } + } + + // If empty, send them to project page. + if (empty($form['environments']['#options'])) { + drupal_set_message(t('You do not have any environments with Pull Code enabled, or they are all tracking tags. !link to be able to pull code.', array('!link' => l(t('Edit the project settings'), "node/$node->nid/edit")))); + // @TODO: Close modal window instead? + return $form; + } + + $form['update'] = array( + '#title' => t('Run update.php after code pull?'), + '#type' => 'checkbox', + '#default_value' => 1, + ); + + if (_devshop_projects_project_has_module($node, 'features')) { + $form['revert'] = array( + '#title' => t('Revert all features after code pull?'), + '#type' => 'checkbox', + '#default_value' => 1, + ); + } + $form['cache'] = array( + '#title' => t('Clear cache after code pull?'), + '#type' => 'checkbox', + '#default_value' => 1, + ); + + // Add validator for environments + //$form['#validate'] = array('hosting_task_devshop_pull_form'); + return $form; +} + +/** + * Extra submit function for hosting_task_confirm_form() + * + * @see devshop_projects_form_alter(). We had to add the submit hadler there. + */ +function hosting_task_devshop_pull_form_validate($form, &$form_state) { + $value = implode(' ', array_filter($form_state['values']['parameters']['environments'])); + form_set_value($form['parameters']['environments'], $value, $form_state); +} + diff --git a/modules/devshop/devshop_projects/tasks/tasks.inc b/modules/devshop/devshop_projects/tasks/tasks.inc new file mode 100644 index 000000000..9ca42de5e --- /dev/null +++ b/modules/devshop/devshop_projects/tasks/tasks.inc @@ -0,0 +1,48 @@ + t('Verify Project'), + 'description' => t('Verifies access to the git repository, downloads branch and tag information.'), + 'access callback' => 'devshop_hosting_task_menu_access', + 'provision_save' => TRUE, + ); + $tasks['project']['delete'] = array( + 'title' => t('Delete Project'), + 'description' => t('Delete a project and all associated sites and platforms.'), + 'access callback' => 'devshop_hosting_task_menu_access', + 'dialog' => TRUE, + 'hidden' => TRUE, + ); + $tasks['site']['devshop-deploy'] = array( + 'title' => t('Deploy'), + 'description' => t('Checkout or Pull new code to this environment.'), + 'access callback' => 'devshop_hosting_task_menu_access', + 'dialog' => TRUE, + ); + return $tasks; +} + +/** + * Helper to provide a select list of environments for this project + */ +function devshop_projects_tasks_add_environment_to_form(&$form, $node, $description, $key = 'environment', $title = 'Environment', $type = 'radios') { + $keys = array_keys($node->project->environments); + $options = array_combine($keys, $keys); + $form[$key] = array( + '#type' => $type, + '#title' => t('@title', array('@title' => $title)), + '#options' => $options, + '#default_value' => $type == 'radios' ? key($options) : array(key($options)), + '#required' => $type == 'checkboxes', + '#description' => t('@description', array('@description' => $description)), + ); +} diff --git a/modules/devshop/devshop_pull/README.txt b/modules/devshop/devshop_pull/README.txt new file mode 100644 index 000000000..1d64afa10 --- /dev/null +++ b/modules/devshop/devshop_pull/README.txt @@ -0,0 +1,23 @@ +DevShop Pull +============ + +Provides a way for environments to stay up to date with the git repository. + +Each project can configure to Pull on Queue or Pull on URL Callback. + +Pull on Queue will trigger Pull Code tasks on a regular basis using Hosting +Queues. Pull on URL Callback provides a URL that you can add to your git host +to ping on receiving a commit. + + +GitHub Setup +------------ + +1. Visit your repos page: http://github.com/YOURNAME/YOURREPO +2. Click "Settings". +3. Click "Service Hooks". +4. Click "WebHook URLs" +5. Copy and paste your project's Git Pull Trigger URL into the URL field of the + WebHook URLs page. +6. Click "Test Hook" to run a test, then check your DevShop project to ensure a + Pull Code task was triggered. diff --git a/modules/devshop/devshop_pull/devshop_pull.inc b/modules/devshop/devshop_pull/devshop_pull.inc new file mode 100644 index 000000000..31d1c3983 --- /dev/null +++ b/modules/devshop/devshop_pull/devshop_pull.inc @@ -0,0 +1,197 @@ +project; + $deploy_settings = $project_node->project->settings->deploy; + + $allowed_ips = explode("\n", trim(variable_get('devshop_pull_ip_acl', DEVSHOP_PULL_DEFAULT_ALLOWED_IPS))); + array_filter(array_map('trim', $allowed_ips)); + + // Make sure we got the project. + if (!$project_node) { + $message = "Project $project not found."; + } + // Make sure the security code is valid + else if (_devshop_pull_hash_create($project_node) != $hash) { + $message = "Security code $hash is not valid!"; + $status = DEVSHOP_PULL_STATUS_INVALID_CODE; + } + // Make sure the project has pull callback enabled + if ($deploy_settings['method'] != 'webhook') { + $message = "Project not configured to use webhook deployment."; + } + // Make sure the client's IP address is on the list + else if (!devshop_pull_ip_match(ip_address(), $allowed_ips)) { + $message = ip_address() . " is not authorized to invoke a webhook request."; + $status = DEVSHOP_PULL_STATUS_ACCESS_DENIED; + } + // All checks pass! Server is allowed to trigger tasks! + else { + $status = DEVSHOP_PULL_STATUS_OK; + + // Check headers for GitHub Integration + $headers = getallheaders(); + if ((isset($headers['X-GitHub-Event']) || isset($headers['X-Github-Event'])) && function_exists('devshop_github_webhook')) { + $message = devshop_github_webhook($project_node); + } + elseif ($headers['User-Agent'] == 'Bitbucket-Webhooks/2.0' && function_exists('devshop_bitbucket_webhook')) { + $message = devshop_bitbucket_webhook($project_node); + } + else { + $message = devshop_pull_default_webhook($project_node); + } + } + + // Save the project node with last pull info. + $deploy_settings['last_webhook'] = REQUEST_TIME; + $deploy_settings['last_webhook_status'] = $status; + $deploy_settings['last_webhook_ip'] = ip_address(); + + $project_node->project->settings->deploy = $deploy_settings; + + node_save($project_node); + + // Output a message, no matter what. + watchdog('devshop_pull', $message, array(), WATCHDOG_INFO); + + // @TODO Print an appropriate header. + print $message; + + // Save a variable to help when using the settings page. + variable_set('devshop_pull_last_ip', ip_address()); + variable_set('devshop_pull_last_status', $status); +} + +/** + * Default action to take on webhook init. + */ +function devshop_pull_default_webhook($project_node) { + $project = $project_node->project; + + // Check if body corresponds to a JSON object. + // This is for GitLab Payloads. + // @TODO: We should move this to it's own module. There's more GitLab integration + // that could be done. + $headers = getallheaders(); + if ($headers['Content-Type'] == 'application/json') { + $input = file_get_contents('php://input'); + $data = json_decode($input); + if (json_last_error() == JSON_ERROR_NONE){ + // Attempt to get branch from JSON object + if( isset($data->ref) ) { + // Attempt to get branch/tag being referred + $git_ref = strtr($data->ref, array('refs/tags/' => '', 'refs/heads/' => '')); + } + } + } + + foreach ($project->environments as $environment) { + + // If "pull disabled" is set, or if the git ref isn't for this environment, don't trigger a deploy. + if ($environment->settings->pull_disabled || (isset($git_ref) && $git_ref != $environment->git_ref)) { + continue; + } + + $environments_to_pull[] = $environment->name; + + // Default args to the environments deploy settings. + $args = $environment->settings->deploy; + $args['git_ref'] = $environment->git_ref; + + // Ensure that environment site exists before trying to deploy it. + if (isset($environment->site) && $node = node_load($environment->site)) { + hosting_add_task($environment->site, 'devshop-deploy', $args); + } + } + + return "Commit notification received! Running 'Deploy' on $project->title environments " . implode(', ', $environments_to_pull); +} + +/** + * Check whether a given ip address matches a list of allowed ip addresses, some of which + * may be CIDR. + * + * @param $ip + * The ip addy to test. + * @param $list + * The list to test against. + */ +function devshop_pull_ip_match($ip, $list) { + foreach ($list as $cidr) { + if (trim($ip) === trim($cidr)) { + return TRUE; + } + @list($net, $mask) = explode('/', trim($cidr)); + if (isset($mask)) { + $bitmask = ~((1 << (32 - $mask)) - 1); + if ((ip2long($net) & $bitmask) == (ip2long($ip) & $bitmask)) { + return TRUE; + } + } + } + return FALSE; +} + +/** + * Create the full URL that is displayed in the project node view + * and given to the GitHub WebHook to invoke a pull after a commit. + */ +function _devshop_pull_callback_url($node) { + return url(DEVSHOP_PULL_CALLBACK_URL + . '/' . $node->title + . '/' . _devshop_pull_hash_create($node), + array('absolute' => TRUE)); +} + +/** + * Create a security hash code based on the platform node + */ +function _devshop_pull_hash_create($node) { + return md5($node->title . $node->nid); +} + +/** + * Prepares a "Pull Code" task for a project. + * + * @param $project_nid + * A project nid. + * + * Platforms in a project must be enabled to have this command run on them. + */ +function devshop_pull_project($project_nid) { + + $project_node = node_load($project_nid); + + $args = array('environments' => ''); + + foreach ($project_node->project->environments as $name => $environment) { + if (!$environment->data->pull_disabled) { + $args['environments'] .= $environment->name . ' '; + } + } + $args['environments'] = trim($args['environments']); + if (!empty($args['environments'])) { + hosting_add_task($project_nid, 'devshop-pull', $args); + } + else { + print "No environments configured to pull! Aborting."; + } +} + +if (!function_exists('getallheaders')) { + function getallheaders() { + $headers = ''; + foreach ($_SERVER as $name => $value){ + if (substr($name, 0, 5) == 'HTTP_') { + $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; + } + } + return $headers; + } +} diff --git a/modules/devshop/devshop_pull/devshop_pull.info b/modules/devshop/devshop_pull/devshop_pull.info new file mode 100644 index 000000000..91e8fe91c --- /dev/null +++ b/modules/devshop/devshop_pull/devshop_pull.info @@ -0,0 +1,5 @@ +name = DevShop Pull +description = Provides a variety of methods to trigger a Pull Code task on DevShop projects. +core = 7.x +package = DevShop +dependencies[] = devshop_projects diff --git a/modules/devshop/devshop_pull/devshop_pull.install b/modules/devshop/devshop_pull/devshop_pull.install new file mode 100644 index 000000000..e887b7d00 --- /dev/null +++ b/modules/devshop/devshop_pull/devshop_pull.install @@ -0,0 +1,67 @@ + array( + 'project_nid' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + 'pull_method' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + 'last_pull' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + 'last_pull_status' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + 'last_pull_ip' => array( + 'type' => 'varchar', + 'length' => 15, + 'not null' => TRUE, + ), + ), + 'primary key' => array('project_nid'), + ); + return $schema; +} + +/** + * no-op: code fixed in devshop_pull_update_6201() + */ +function devshop_pull_update_6000() { + $ret = array(); + return $ret; +} +/** + * Add last_pull_ip column to our hosting_devshop_pull_projects table. + */ +function devshop_pull_update_6001() { + $ret = array(); + db_add_field($ret, 'hosting_devshop_pull_projects', 'last_pull_ip', array('type' => 'varchar', 'not null' => TRUE, 'default' => '', 'length' => 15)); + return $ret; +} + +/** + * Remove legacy `hosting_devshop_pull_platforms` table. + */ +function devshop_pull_update_6002() { + $ret = array(); + db_drop_table($ret, 'hosting_devshop_pull_platforms'); + return $ret; +} diff --git a/modules/devshop/devshop_pull/devshop_pull.module b/modules/devshop/devshop_pull/devshop_pull.module new file mode 100644 index 000000000..a3dfc357f --- /dev/null +++ b/modules/devshop/devshop_pull/devshop_pull.module @@ -0,0 +1,64 @@ + array( + 'title' => t('configure devshop pull'), + 'description' => t('Configure DevShop Pull module.'), + ), + ); +} + +/** + * Implements hook_menu(). + */ +function devshop_pull_menu() { + $items['admin/devshop/pull'] = array( + 'title' => 'Git Webhooks', + 'description' => 'Configure Git Webhooks', + 'page callback' => 'devshop_pull_settings_page', + 'access arguments' => array('administer hosting settings'), + 'file' => 'devshop_pull.settings.inc', + 'tab_parent' => 'admin/devshop', + 'type' => MENU_LOCAL_TASK, + ); + $items[DEVSHOP_PULL_CALLBACK_URL] = array( + 'page callback' => 'devshop_pull_callback', + 'access callback' => TRUE, + 'file' => 'devshop_pull.inc', + 'type' => MENU_CALLBACK, + ); + return $items; +} diff --git a/modules/devshop/devshop_pull/devshop_pull.settings.inc b/modules/devshop/devshop_pull/devshop_pull.settings.inc new file mode 100644 index 000000000..4018b9a19 --- /dev/null +++ b/modules/devshop/devshop_pull/devshop_pull.settings.inc @@ -0,0 +1,44 @@ + 'textarea', + '#title' => t('Control Access by IP'), + '#default_value' => variable_get('devshop_pull_ip_acl', DEVSHOP_PULL_DEFAULT_ALLOWED_IPS), + '#rows' => 6, + ); + + // we have a few bullet points + $items = array(); + $items[] = t('Enter the IP addresses that are allowed to trigger a "Deploy" tasks. You may specify address ranges using CIDR notation (e.g. 192.168.1.0/24).'); + $items[] = t('GitHub post-receive callback servers are: %github_ips.', array('%github_ips' => "204.232.175.64/27 and 192.30.252.0/22")); + $items[] = t('BitBucket post-receive callback servers are: %bitbucket_ips.', array('%bitbucket_ips' => '104.192.143.192/28 and 104.192.143.208/28')); + $items[] = t('Your local computer\'s IP address is %ip. ', array('%ip' => $_SERVER['REMOTE_ADDR'])); + + // If there was a last ip logged, display it here so its easy to add to the list. + $last_ip = variable_get('devshop_pull_last_ip', ''); + if ($last_ip) { + $items[] = t('The last IP to attempt a commit notification was %ip', array('%ip' => $last_ip)); + } + else { + $items[] = t('No requests ever detected. If you add the trigger URL for a project to your git repo host, the IP will be logged and displayed here.'); + } + + if (variable_get('devshop_pull_last_status', '') == DEVSHOP_PULL_STATUS_ACCESS_DENIED) { + $items[] = t('The last incoming webhook was denied because it is not in this list.'); + } + + $form['devshop_pull_ip_acl']['#description'] = theme('item_list', array('items' => $items)); + + return system_settings_form($form); +} diff --git a/modules/devshop/devshop_remotes/devshop_remotes.info b/modules/devshop/devshop_remotes/devshop_remotes.info new file mode 100644 index 000000000..1d3f887a7 --- /dev/null +++ b/modules/devshop/devshop_remotes/devshop_remotes.info @@ -0,0 +1,5 @@ +name = DevShop Remotes +description = Manage remote environments with devshop. +core = 7.x +package = DevShop +dependencies[] = devshop_projects diff --git a/modules/devshop/devshop_remotes/devshop_remotes.install b/modules/devshop/devshop_remotes/devshop_remotes.install new file mode 100644 index 000000000..7a8b0f0b8 --- /dev/null +++ b/modules/devshop/devshop_remotes/devshop_remotes.install @@ -0,0 +1,33 @@ +fields(array( + 'weight' => 2 + )) + ->condition('name', 'devshop_remotes') + ->execute(); +} + +/** + * Set the weight of this module higher than devshop_projects.module. + */ +function devshop_remotes_update_7001() { + + // Push devshop_projects's system weight to 1. + db_update('system') + ->fields(array( + 'weight' => 2 + )) + ->condition('name', 'devshop_remotes') + ->execute(); +} diff --git a/modules/devshop/devshop_remotes/devshop_remotes.module b/modules/devshop/devshop_remotes/devshop_remotes.module new file mode 100644 index 000000000..4cfb43c71 --- /dev/null +++ b/modules/devshop/devshop_remotes/devshop_remotes.module @@ -0,0 +1,418 @@ + 'Add remote alias', + 'description' => 'Add a remote alias to a project', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('devshop_remotes_alias_add_form', 1), + 'access arguments' => array('add remote aliases to projects'), + ) ; + $items['project/%/delete-alias/%'] = array( + 'title' => 'Remove remote alias', + 'description' => 'Remove a remote alias from a project', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('devshop_remotes_alias_remove_form', 1, 3), + 'access arguments' => array('remove remote aliases from projects'), + ) ; + return $items; +} + +/** + * Implements hook_permission(). + */ +function devshop_remotes_permission() { + return array( + 'add remote aliases to projects' => array( + 'title' => t('add remote aliases to projects'), + 'description' => t('Allow the user to add remote aliases to the project.'), + ), + 'remove remote aliases from projects' => array( + 'title' => t('remove remote aliases from projects'), + 'description' => t('Allow the user to remove remote aliases from projects.'), + ), + ); +} + +/** + * @param $project_name + */ +function devshop_remotes_alias_add_form($form, &$form_state, $project_name) { + + $node = devshop_load_project_by_name($project_name); + $form['project_nid'] = array( + '#type' => 'value', + '#value' => $node->nid, + ); + $form['name'] = array( + '#title' => t('Alias Name'), + '#description' => t('Add the machine name for the alias. letters, numbers, and underscores only. This must not match a devshop environment name.'), + '#type' => 'textfield', + ); + $form['note'] = array( + '#markup' => t('Enter the following items exactly as they appear in your drush alias.'), + ); + $form['uri'] = array( + '#title' => t('URI'), + '#type' => 'textfield', + ); + $form['root'] = array( + '#title' => t('Root'), + '#type' => 'textfield', + ); + $form['remote_host'] = array( + '#title' => t('Remote Host'), + '#type' => 'textfield', + ); + $form['remote_user'] = array( + '#title' => t('Remote User'), + '#type' => 'textfield', + ); + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Save Alias'), + ); + return $form; +} + +/** + * Form validate for "add remote alias". + * @param $form + * @param $form_state + */ +function devshop_remotes_alias_add_form_validate(&$form, $form_state) { + + $project_node = node_load($form_state['values']['project_nid']); + $project = $project_node->project; + + // Lowercase and trim the name. + $form_state['values']['name'] = trim(strtolower($form_state['values']['name'])); + form_set_value($form['name'], $form_state['values']['name'], $form_state); + + // Don't allow the alias to match the project name, it causes drush aliases to break. + if ($project_node->title == $form_state['values']['name']) { + form_set_error('name', t('The alias name cannot match the project name.')); + } + + // Only allow numbers and letters. + if (!preg_match('!^[a-z0-9]+$!', $form_state['values']['name'])) { + form_set_error('name', t('Only letters and numbers are allowed.')); + } + + // Check for environment. + if (isset($project_node->project->environments[$form_state['values']['name']])) { + form_set_error('name', t('An environment named %name already exists in this project. Please choose a different name.', array( + '%name' => $form_state['values']['name'], + ))); + } + + // Check for existing alias. + if (isset($project->settings->aliases[$form_state['values']['name']])) { + form_set_error('name', t('An alias named %name already exists in this project. Please choose a different name.', array( + '%name' => $form_state['values']['name'], + ))); + } +} + +/** + * Form validate for "add remote alias". + * @param $form + * @param $form_state + */ +function devshop_remotes_alias_add_form_submit(&$form, &$form_state) { + + // Load project node. + $project_node = node_load($form_state['values']['project_nid']); + $project = &$project_node->project; + + // Save alias to "settings" data. + $project->settings->aliases[$form_state['values']['name']] = array( + 'uri' => $form_state['values']['uri'], + 'root' => $form_state['values']['root'], + 'remote-user' => $form_state['values']['remote_user'], + 'remote-host' => $form_state['values']['remote_host'], + ); + + node_save($project_node); + + // Redirect to project + $form_state['redirect'] = "node/{$project_node->nid}"; + +} + + +/** + * @param $project_name + */ +function devshop_remotes_alias_remove_form($form, &$form_state, $project_name, $alias_name) { + // Load project node. + $node = devshop_load_project_by_name($project_name); + $project = &$node->project; + + $alias = $project->settings->aliases[$alias_name]; + + $form['project_nid'] = array( + '#type' => 'value', + '#value' => $node->nid, + ); + $form['name'] = array( + '#type' => 'value', + '#value' => $alias_name, + ); + + $form['project'] = array( + '#title' => t('Project'), + '#type' => 'item', + '#markup' => $project_name, + ); + $form['alias'] = array( + '#title' => t('Alias') . ': ' . $alias_name, + '#type' => 'item', + '#field_prefix' => '
    ',
    +    '#field_suffix' => '
    ', + '#markup' => var_export($alias, 1), + ); + + return confirm_form($form, t('Are you sure you want to remove this alias?'), "node/{$node->nid}", '', t('Delete Alias')); + +} + +/** + * Form validate for "add remote alias". + * @param $form + * @param $form_state + */ +function devshop_remotes_alias_remove_form_submit(&$form, &$form_state) { + + // Load project node. + $project_node = node_load($form_state['values']['project_nid']); + $project = &$project_node->project; + + // Save alias to "settings" data. + unset($project->settings->aliases[$form_state['values']['name']]); + node_save($project_node); + + // Redirect to project + $form_state['redirect'] = "node/{$project_node->nid}"; +} + +/** + * Implements hook_preprocess_node(). + */ +function devshop_remotes_preprocess_node(&$variables) { + + $node = $variables['node']; + if ($node->type != 'project') { + return; + } + + $count = count($variables['node']->project->settings->aliases); + $label = t('Remote Aliases'); + $delete_label = t('Delete Alias'); + + $aliases = ''; + + foreach ($variables['node']->project->settings->aliases as $name => $alias) { + $alias_rendered = var_export($alias, 1); + $uri = $alias['uri']; + + if (user_access('remove remote aliases from projects')) { + $delete_url = url("project/{$node->title}/delete-alias/{$name}"); + $delete_url_link = " $delete_label"; + + } + + $aliases.= << + + $name + +
    + {$delete_url_link} +
    +
    +
    $alias_rendered
    +    
    +
    + +HTML; + } + + if (user_access('add remote aliases to projects')) { + + $aliases_url = url("project/{$node->title}/add-alias"); + $add_link_title = t('Add Remote Alias'); + $add_link = " + {$add_link_title} + "; + } + else { + $add_link = ''; + } + + $variables['project_extra_items'][] = << + + {$label}: $count + + + + +HTML; + +} + +/** + * Implementation of hook_form_FORM_ID_alter(). + * + * The Project Node Form. + */ +function devshop_remotes_form_project_node_form_alter(&$form, &$form_state, $form_id) { + $node = $form['#node']; + $project = $node->project; + + // Make sure alias settings data gets saved in project node form. + $form['project']['settings']['aliases'] = array( + '#value' => $project->settings->aliases, + '#type' => 'value', + ); +} + +/** + * Implementation of hook_form_FORM_ID_alter(). + * + * The Project Node Form. + */ +function devshop_remotes_form_hosting_task_confirm_form_alter(&$form, &$form_state, $form_id) { + + if (arg(2) != 'site_sync') { + return; + } + +// $site = $form_state['build_info']['args'][0]; +// +// if (isset($_GET['source']) && isset($site->project->settings->aliases[$_GET['source']])) { +// $alias_name = $_GET['source']; +// $alias_record = $alias_name. $site->project->settings->aliases[$alias_name]; +// $actual_alias = "@{$site->project->name}.{$alias_name}"; +// +//// $form['parameters']['source']['custom_alias']['text']['#type'] = 'value'; +// $form['parameters']['source']['#default_value'] = 'wtf'; +// +// $form['source_display'] = array( +// '#type' => 'item', +// '#title' => t('Source'), +// '#markup' => l($alias_record['uri'], 'http://' . $alias_record['uri'], array( +// 'attributes' => array( +// 'target' => '_blank', +// ), +// )), +// '#weight' => -10, +// ); +// } +} + +/** + * Implements hook_preprocess_environment(). + * @TODO: This is not working! Get to work then remove from boots/template.php + */ +function devshop_remotes_preprocess_environment(&$vars) { + + kpr($vars); + die; + +} + +/** + * When loading project node, lookup sql sources from composer.json! + * + * @param $nodes + * @param $types + */ +function devshop_remotes_node_load($nodes, $types) { + if (count(array_intersect(array('project'), $types))) { + foreach ($nodes as &$node) { + + if (!is_array($node->project->settings->aliases)) { + $node->project->settings->aliases = array(); + } + + + // Load all composer.json files from all environments. + foreach ($node->project->environments as $env) { + if (file_exists($env->repo_path . '/composer.json')) { + $composer_data = json_decode(file_get_contents($env->repo_path . '/composer.json'), TRUE); + if (!isset($composer_data['config']['devshop']['sync_sources']) || !is_array($composer_data['config']['devshop']['sync_sources'])) { + continue; + } + $sync_sources = $composer_data['config']['devshop']['sync_sources']; + foreach ($sync_sources as $name => $source) { + + // If environment exists with this name and existing alias exists with this name, unset it. + if (isset($node->project->environments[$name]) && $node->project->environments[$name]->site_status == HOSTING_SITE_ENABLED && isset($node->project->settings->aliases[$name])) { + unset($node->project->settings->aliases[$name]); + } + + // If environment does not exist with this name, load it. + if (!isset($node->project->environments[$name])) { + $source['loaded_from'] = $env->repo_path . '/composer.json'; + $node->project->settings->aliases[$name] = $source; + } + else { + global $user; + if (variable_get('hosting_require_disable_before_delete', TRUE)) { + $delete_link = l(t('disable the @name environment', array('@name' => $name)), "hosting_confirm/{$node->project->environments[$name]->site}/site_disable", array('query' => array('token' => drupal_get_token($user->uid)))); + + } + else { + $delete_link = t('!disable or !delete the !name environment', array( + '!name' => l($name, "node/{$node->project->environments[$name]->site}"), + '!disable' => l(t('Disable'), "hosting_confirm/{$node->project->environments[$name]->site}/site_disable", array('query' => array('token' => drupal_get_token($user->uid)))), + '!delete' => l(t('Delete'), "hosting_confirm/{$node->project->environments[$name]->site}/site_delete", array('query' => array('token' => drupal_get_token($user->uid)))) + )); + } + + $node->project->messages['remote_alias_conflict'] = array( + 'message' => t('There is a remote alias sync source defined in composer.json with the name %name that has the same name as an environment. To remove this message, delete or rename the "config.devshop.sync_sources.%name" entry in all composer.json files in all environments, or !delete_link.', array( + '%name' => $name, + '!delete_link' => $delete_link, + )), + 'icon' => '', + 'type' => 'danger', + ); + } + } + } + } + } + } +} diff --git a/modules/devshop/devshop_support/README.md b/modules/devshop/devshop_support/README.md new file mode 100644 index 000000000..0b4a4a785 --- /dev/null +++ b/modules/devshop/devshop_support/README.md @@ -0,0 +1,13 @@ +# DevShop.Support Client + +This module is designed for use in a DevShop server, using the [Devmaster](https://www.drupal.org/project/devmaster) install profile. + +This module is required for DevShop Support customers. + +## What does it do? + +1. Configures the cas.module to connect to DevShop.Support, enabling single-sign-on with your DevShop.Support account. +2. Enables the intercomio.module to connect you to the DevShop.Support Chat system. +2. Retrieves and validates your DevShop Support License. +3. Displays Support License and Monitoring status in DevShop. +4. Links your devshop's users to DevShop.Support License Users. \ No newline at end of file diff --git a/modules/devshop/devshop_support/devshop_support_network_client.info b/modules/devshop/devshop_support/devshop_support_network_client.info new file mode 100644 index 000000000..59058ddfa --- /dev/null +++ b/modules/devshop/devshop_support/devshop_support_network_client.info @@ -0,0 +1,6 @@ +name = DevShop Support Network Client +description = Connects your DevShop server to the DevShop.Support system. +package = DevShop +core = 7.x +configure = admin/devshop/support + diff --git a/modules/devshop/devshop_support/devshop_support_network_client.install b/modules/devshop/devshop_support/devshop_support_network_client.install new file mode 100644 index 000000000..9e1888a93 --- /dev/null +++ b/modules/devshop/devshop_support/devshop_support_network_client.install @@ -0,0 +1,105 @@ + $t('DevShop License Payload'), + 'value' => devshop_payload_to_table(), + 'severity' => REQUIREMENT_INFO, + ); + } + return $requirements; +} + +/** + * Convert the devshop support client data payload to an HTML table. + * @param null $data + * + * @return string + * @throws \Exception + */ +function devshop_payload_to_table($data = NULL) { + + + if (!$data) { + $data = devshop_support_network_client_client_data(); + } + + $rows = array(); + foreach ($data as $name => $value) { + if (is_array($value) and !empty($value)) { + $value = devshop_payload_to_table($value); + } + $rows[] = array( + "{$name}", $value + ); + } + + return theme('table', array( + 'rows' => $rows, + )); +} + +/** + * Implements hook_install() + */ +function devshop_support_network_client_install() { + + // Push devshop_projects's system weight to 1. + db_update('system') + ->fields(array( + 'weight' => 1 + )) + ->condition('name', 'devshop_support_network_client') + ->execute(); + + variable_set('composer_manager_vendor_dir', 'sites/all/composer/vendor'); + variable_set('composer_manager_autobuild_file', 0); + +} + +/** + * + */ +function devshop_support_network_client_update_7000() { + devshop_support_network_client_install(); +} + +/** + * Delete the composer_manager_file_dir so we just use the default. + */ +function devshop_support_network_client_update_7001() { + variable_del('composer_manager_file_dir'); +} + +/** + * Check for CAS library location and save it. + */ +function devshop_support_network_client_update_7002() +{ + + if (file_exists(DRUPAL_ROOT . '/profiles/devmaster/libraries/cas')) { + $cas_library_dir = 'profiles/devmaster/libraries/cas'; + } elseif (file_exists(DRUPAL_ROOT . '/sites/all/libraries/cas')) { + $cas_library_dir = 'sites/all/libraries/cas'; + } else { + $cas_library_dir = ''; + } + variable_set('cas_library_dir', $cas_library_dir); +} \ No newline at end of file diff --git a/modules/devshop/devshop_support/devshop_support_network_client.module b/modules/devshop/devshop_support/devshop_support_network_client.module new file mode 100644 index 000000000..c663ec022 --- /dev/null +++ b/modules/devshop/devshop_support/devshop_support_network_client.module @@ -0,0 +1,894 @@ + 'DevShop Support', + 'description' => 'Configure DevShop Support.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('devshop_support_network_client_settings_form'), + 'access arguments' => array('administer devshop support'), + 'type' => MENU_LOCAL_TASK, + ); + return $items; + +} + +/** + * Implements hook_menu_alter(). + */ +function devshop_support_network_client_menu_alter(&$items) +{ + $items['user/logout']['page callback'] = 'devshop_support_network_client_logout_replacement'; +} + + +/** + * Replaces user/logout: if user is CAS, send to caslogout. + */ +function devshop_support_network_client_logout_replacement() { + if (!empty($_SESSION['phpCAS']['user'])) { + drupal_goto('caslogout'); + } + else { + user_logout(); + } +} + +/** + * Implements hook_permission() + * @return array + */ +function devshop_support_network_client_permission() { + return array( + 'administer devshop support' => array( + 'title' => t('administer DevShop.Support'), + 'description' => t("Configure this devshop\'s support configuration."), + ) + ); +} + +/** + * Return default set of variables to set when activating a license. + */ +function devshop_support_network_default_variables() +{ + $devshop_support_url = 'https://' . variable_get('devshop_support_url', 'devshop.support'); + + // cas_server variable is now passed from support license payload. + // @see devshop_support_network_page_callback() in devshop_support_network.module. + // $conf['cas_server'] = variable_get('devshop_support_url', 'devshop.support'); + + $conf['cas_version'] = '2.0'; + $conf['cas_server'] = variable_get('devshop_support_url', 'devshop.support'); + $conf['cas_uri'] = '/cas'; + $conf['cas_port'] = '443'; + + // DOES NOT AFFECT cas.module when Libraries module is enabled, so I set it in build-devmaster.make. + // $conf['cas_library_dir'] = 'sites/all/libraries/cas'; + $conf['cas_logout_destination'] = $devshop_support_url . '/bye?server=' . $_SERVER['HTTP_HOST']; + $conf['cas_login_message'] = t('Signed into !server as %cas_username via DevShop.Cloud.', array( + '!server' => $_SERVER['HTTP_HOST'], + )); + $conf['cas_login_invite'] = ''; + $conf['cas_login_redir_message'] = ''; + $conf['cas_login_drupal_invite'] = 'Sign in with Drupal'; + $conf['cas_user_register'] = 0; + $conf['cas_domain'] = ''; + $conf['cas_hide_email'] = 1; + $conf['cas_hide_password'] = 1; + $conf['cas_check_frequency'] = 0; + $conf['cas_pgtformat'] = 0; + + $conf['cas_attributes_overwrite'] = 1; + $conf['cas_attributes_sync_every_login'] = 1; + $conf['cas_attributes_relations'] = array( + 'name' => '[cas:attribute:name]', + 'mail' => '[cas:attribute:mail]', + ); + + if (devshop_cloud_license_active()) { + $conf['cas_login_form'] = 2; + $conf['cas_registerURL'] = $devshop_support_url . '/user/register'; + $conf['cas_changePasswordURL'] = $devshop_support_url . '/user/password'; + } + else { + $conf['cas_login_form'] = 0; + } + return $conf; +} + +/** + * Implements hook_intercomio_settings_alter(). + * + * Pass the account "cas_name" as the user_id + */ +function devshop_support_network_client_intercomio_settings_alter(&$settings, $account) { + + if (!empty($account->cas_name)) { + $settings['user_id'] = $account->cas_name; + } +} + +/** + * Implements hook_user_view_alter(). + */ +function devshop_support_network_client_user_view_alter(&$build) { + // Show link to devshop.build account. + $build['devshop_support_network_client'] = array( + '#access' => variable_get('cas_server', '') && $build['#account']->cas_name, + '#type' => 'item', + '#markup' => l(' devshop.support/' . $build['#account']->cas_name, rtrim('https://' . variable_get('devshop_support_url', 'devshop.support'), '/'), [ + 'html' => 1, + 'attributes' => [ + 'class' => ['btn btn-info'], + ], + ]), + '#weight' => 10, + ); +} + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function devshop_support_network_client_settings_form() { + $node = hosting_context_load('hostmaster'); + $license_key = variable_get('devshop_support_license_key', ''); + + $service_url = 'https://' . variable_get('devshop_support_url', 'devshop.support'); + $devshop_hostname = $_SERVER['HTTP_HOST']; + $service_title = t('DevShop.Cloud'); + + $data = (object) variable_get('devshop_support_license_raw_data', NULL); + + $title = t('Hi!'); + $status_items = array(); + + $license_key_status = variable_get('devshop_support_license_key_status', 'none'); + + $key_to_submit = empty($license_key_status) && !empty($_GET['k'])? $_GET['k']: NULL; + + + switch ($license_key_status) { + case 'active': $license_class = 'success'; break; + case 'none': $license_class = 'info'; break; + case 'suspended': $license_class = 'warning'; break; + case 'cancelled': $license_class = 'danger'; break; + } + + + if ($license_key_status == 'none') { + if (empty($key_to_submit)) { + $message = 'There is no license for this server. !get_one_link'; + $alert_class = 'primary'; + } + else { + $message = 'License key is ready to validate! Press Verify License Key below to activate DevShop Support.'; + $alert_class = 'success'; + } + } + else { + $message = "Server license is !status."; + $alert_class = $license_class; + + } + $status_items[] = array( + 'data' => t($message, array( + '!status' => "{$license_key_status}", + '!get_one_link' => l(t('Get One'), "{$service_url}/server/{$devshop_hostname}", array( + 'attributes' => array('class' => array("btn btn-sm btn-{$alert_class}")) + )), + )), + 'class' => "list-group-item list-group-item-{$alert_class}", + ); + + // Calculate user limit (checking for empty data) + if ($license_key_status != 'none') { + if (!empty($data->_LIMITS->users->used) && !empty($data->_LIMITS->users->used)) { + $limit_class = devshop_support_limit_class($data->_LIMITS->users->used, $data->_LIMITS->users->available, 1, 'success'); + $status_items[] = array( + 'data' => t("Using !used of !available allowed users.", array( + '!used' => $data->_LIMITS->users->used, + '!available' => $data->_LIMITS->users->available, + )), + 'class' => "list-group-item list-group-item-{$limit_class}", + ); + } + + // Calculate project limit + if (!empty($data->_LIMITS->users->used) && !empty($data->_LIMITS->users->used)) { + $limit_class = devshop_support_limit_class( + $data->_LIMITS->projects->used, + $data->_LIMITS->projects->available, + 1, + 'success' + ); + $status_items[] = [ + 'data' => t( + "Using !used of !available allowed projects.", + [ + '!used' => $data->_LIMITS->projects->used, + '!available' => $data->_LIMITS->projects->available, + ] + ), + 'class' => "list-group-item list-group-item-{$limit_class}", + ]; + } + } + + $form['status_display'] = array( + '#theme' => 'item_list', + '#items' => $status_items, + '#attributes' => array( + 'class' => array('list-group') + ) + ); + + if (devshop_cloud_hostname($devshop_hostname)) { + $subtitle = t('Welcome to !devshop_cloud_link: The Drupal DevOps Platform.', array( + '!devshop_cloud_link' => l($service_title, $service_url, array( + 'attributes' => array('target' => '_blank') + )), + )); + $description = t('This server is owned and fully managed by the !devshop_cloud_link service.', array( + '!devshop_cloud_link' => l($service_title, $service_url, array( + 'attributes' => array('target' => '_blank') + )), + )); + $button = l(t('Manage @server on @service', array( + '@server' => $devshop_hostname, + '@service' => $service_title, + )), "{$service_url}/server/{$devshop_hostname}", array( + 'attributes' => array( + 'target' => '_blank', + 'class' => array('btn btn-primary btn-lg'), + ) + )); + } + else { + $service_title = t('DevShop.Support'); + $subtitle = t('Welcome to !devshop_support_link: Host Your Own Drupal', array( + '!devshop_support_link' => l(t('DevShop.Support'), $service_url, array( + 'attributes' => array('target' => '_blank') + )), + )); + if (empty($data)) { + $description = ''; + } + elseif ($devshop_hostname == 'devshop.local.computer') { + $description = t('Hey there. This is !link. Thanks for launching!', array( + '!link' => l('devshop.local.computer', 'devshop.local.computer'), + )); + } + elseif (!empty($data->_TEAM->name)) { + + $description = t('This server is managed by !devshop_cloud_link.', array( + '!team_link' => l($data->_TEAM->name, $data->_TEAM->url), + '!devshop_cloud_link' => l($service_title, $service_url, array( + 'attributes' => array('target' => '_blank') + )) + )); + } + + if ($license_key_status != 'none') { + $button = l(t('Manage @server on @service', array( + '@server' => $devshop_hostname, + '@service' => $service_title, + )), "{$service_url}/server/{$devshop_hostname}", array( + 'attributes' => array( + 'target' => '_blank', + 'class' => array('btn btn-primary'), + ) + )); + } + else { + $button = ''; + } + + // If "key to submit" is set, we've been redirected from devshop.support. + if ($key_to_submit) { + $title = t('Almost There!'); + $form['devshop_support_license_key']['#type'] = 'value'; + $form['devshop_support_license_key']['#value'] = $key_to_submit; + + $form['key_to_submit'] = array( + '#type' => 'item', + '#title' => t('DevShop License Key for @server', array( + '@server' => $devshop_hostname, + )), + '#markup' => "
    {$key_to_submit}
    ", + ); + } + } + + $form = system_settings_form($form); + + $form['actions']['submit']['#value'] = t('Verify License Key'); + $form['actions']['submit']['#attributes']['class'][] = 'btn btn-success'; + $form['actions']['submit']['#icon'] = ''; + + $form['devshop_support_license_key'] = array( + '#type' => 'textfield', + '#title' => t('DevShop Support License Key'), + '#description' => t('Enter the devshop.support license key for this devshop. If you are unsure visit !link to confirm your license.', [ + '%site' => $node->title, + '!link' => l('devshop.support/server/' . $node->title, 'https://' . 'devshop.support/server/' . $node->title, [ + 'attributes' => ['target' => '_blank'] + ]), + ]), + '#default_value' => variable_get('devshop_support_license_key', ''), + '#element_validate' => array( + 'devshop_support_network_client_settings_form_validate_key', + ), + '#required' => 1, + '#weight' => 1, + ); + + $form['reset'] = array( + '#type' => 'submit', + '#weight' => 100, + '#value' => t('Remove License Key'), + '#access' => !empty(variable_get('devshop_support_license_key', '')), + // @TODO: Can't override special classes added by bootstrap theme. + '#attributes' => array('class' => array('btn-text pull-right')), + ); + + $form['advanced'] = array( + '#type' => 'fieldset', + '#title' => t('Advanced Settings'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#weight' => 10, + ); + + $form['advanced']['devshop_cloud_license_send_project_data'] = array( + '#type' => 'checkbox', + '#title' => t('Send Project Data (ALPHA)'), + '#default_value' => variable_get('devshop_cloud_license_send_project_data'), + '#description' => t('Send the list of projects on this DevShop to DevShop.Cloud. New features coming soon depend on this feature. See !link to view the data payload.', array('!link' => l(t('Site Status Report'), 'admin/reports/status'))), + ); + + + + if (!devshop_cloud_license_active()) { + $button = ''; + } + + $form['#prefix'] = << + +

    $title

    +

    $subtitle

    + +

    $description

    +

    + $button +

    +HTML; + $form['#suffix'] = ''; + return $form; +} + +/** + * Determine if this hostname is in the devshop.cloud network. + * @param $hostname + * @return bool + */ +function devshop_cloud_hostname($hostname) { + foreach (variable_get('devshop_cloud_hostnames', array( + 'devshop.cloud', + +// Uncomment this for development. +// 'devshop.local.computer' + )) as $supported_domain) { + $main_domain_of_hostname = substr($hostname, -strlen($supported_domain), strlen($supported_domain)); + if ($main_domain_of_hostname == $supported_domain) { + return TRUE; + } + } +} + +/** + * Check the license status. + * @return bool + */ +function devshop_cloud_license_active() { + return + module_exists('cas') && + variable_get('devshop_support_license_key_status', 'none') == 'active'; +} + +/** + * Element validation for License Key. Pings devshop.support + * + * @param $element + * @param $form_state + * @param $form + */ +function devshop_support_network_client_settings_form_validate_key($element, &$form_state, $form) { + + // Respond to Remove License Key button. + if ($form_state['clicked_button']['#parents'][0] == 'reset') { + variable_del('devshop_support_license_key'); + variable_del('devshop_support_license_key_status'); + $form_state['values']['devshop_support_license_key'] = ''; + drupal_set_message(t('Your DevShop Support license key was deleted.')); + return; + } + + $response = devshop_support_network_client_post_data($element['#value']); + + if ($response === TRUE) { + drupal_set_message(t('Test Key presumed valid. Enabling DevShop.Support.')); + + } + elseif ($response->code != 200) { + $message = trim($response->status_message); + $message .= empty($message)? trim($response->error): ''; + $message = !empty($message)? $message: t('Something went wrong. No status message or error returned from server.'); + form_error($element, $message); + } + else { + drupal_set_message(t('License Key and Hostname verified! Your DevShop server is now supported!')); + } + $form_state['redirect'] = 'admin/devshop/support'; +} + +/** + * Collect various stats to send to devshop.support. + */ +function devshop_support_network_client_client_data() { + $devshop_hostname = $_SERVER['HTTP_HOST']; + $data['message'] = 'Hello from ' . $devshop_hostname; + $data['active_users'] = db_select('users', 'u') + ->fields('u') + ->condition('status', 1) + ->countQuery() + ->execute() + ->fetchField(); + + $data['active_projects'] = db_select('node', 'n') + ->fields('n') + ->condition('type', 'project') + ->condition('status', 1) + ->countQuery() + ->execute() + ->fetchField(); + + // STAT: Servers + $query = db_select('node', 'n') + ->fields('n') + ->condition('type', 'server') + ->condition('n.status', 1) + ; + $query->addExpression('COUNT(n.nid)', 'ncount'); + $query->join('hosting_server', 'h', 'n.nid = h.nid'); + $data['active_servers'] = $query->execute()->fetchField(); + + // STAT: Domain aliases + $query = db_select('node', 'n') + ->fields('n', array('nid', 'title')) + ->condition('status', 1) + ->groupBy('n.nid'); + + // Add the COUNT expression + $query->addExpression('COUNT(n.title)', 'ncount'); + + // Add the JOIN + $query->join('hosting_site_alias', 'h', 'n.nid = h.nid'); + + $data['active_domains'] = (int) $query->execute()->fetchField(); + + // PROJECTS + if (variable_get('devshop_cloud_license_send_project_data', FALSE)) { + $query = db_select('node', 'n') + ->fields('n', array('title')) + ->fields('p', array('git_url')) + ->condition('type', 'project') + ->condition('n.status', 1) + ; + $query->join('hosting_devshop_project', 'p', 'n.nid = p.nid'); + $data['projects'] = $query->execute()->fetchAllKeyed(); + } + + return $data; +} + + +/** + * Show a message on every page. + */ +function devshop_support_network_client_page_alter(&$page) { + + if (user_is_anonymous()) { + return; + } + + if ($message = variable_get('devshop_support_customer_message', NULL)) { + drupal_set_message($message, variable_get('devshop_support_customer_message_type', 'status')); + } + + + // On the homepage, put the block on the left. + if (current_path() == 'projects' || current_path() == 'admin/devshop/support') { + + if (empty($page['sidebar_first'])) { + $page['sidebar_first'] = array( + '#region' => 'sidebar_first', + ); + } + + $page['sidebar_first']['devshop_support'] = devshop_support_network_client_support_status(); + } + else { + $page['footer']['devshop_support'] = devshop_support_network_client_support_status(); + } +} + +/** + * Implements hook_preprocess_HOOK(). + */ +function devshop_support_network_client_preprocess_page(&$variables) { + + // On the homepage, put the block on the left. + if (current_path() == 'projects' || current_path() == 'admin/devshop/support') { +// +// if (isset($variables['page']['sidebar_first'])) { +// $variables['page']['sidebar_first']['devshop_support_network_client_devshop_support_license_status'] = $variables['page']['footer']['devshop_network_client_support_devshop_support_license_status']; +// } +// else { +// $variables['page']['sidebar_first']['devshop_support_network_client_devshop_support_license_status'] = $variables['page']['footer']['devshop_network_client_support_devshop_support_license_status']; +// $variables['page']['sidebar_first']['#region'] = 'sidebar_first'; +// } +// unset($variables['page']['footer']['devshop_support_network_client_devshop_support_license_status']); +// +// dsm($variables); + } +} + +/** + * Implements hook_block_view(). + */ +function devshop_support_network_client_support_status() { + + if (!user_is_logged_in()){ + return FALSE; + } + + $license_key = variable_get('devshop_support_license_key', ''); + $license_key_status = variable_get('devshop_support_license_key_status', 'none'); + + $status = empty($license_key)? t('Unsupported'): t('Active'); + + $support_link = [ + '#type' => 'link', + '#title' => ' ' . t('Support the Collective'), + '#href' => 'https://opencollective.com/devshop', + '#options' => [ + 'html' => TRUE, + 'attributes' => [ + 'class' => ['text-muted'], + 'target' => '_blank', + 'alt' => t('Support DevShop on OpenCollective'), + ], + ], + ]; + $links[] = drupal_render($support_link); + + if (empty($license_key)) { + $inactive_support_license_links[] = [ + '#type' => 'link', + '#title' => ' ' . t('Activate Support', [ + '@status' => $status, + ]), + '#href' => 'admin/devshop/support', + '#options' => [ + 'html' => TRUE, + 'attributes' => [ + 'class' => ['text-success'], + ], + ], + ]; + + $links[] = drupal_render($inactive_support_license_links); + + $links[] = '

    ' . t('Your DevShop server is currently unsupported.') . '

    '; + } + else { + + switch ($license_key_status) { + case 'active': $license_class = 'success'; break; + case 'none': $license_class = 'info'; break; + case 'suspended': $license_class = 'warning'; break; + case 'cancelled': $license_class = 'danger'; break; + } + + $active_support_license_links['config'] = [ + '#type' => 'link', + '#title' => ' ' . t('License Status: ', [ + '@status' => ucfirst($license_key_status), + '!label_class' => $license_class, + ]), + '#href' => 'https://' . variable_get('devshop_support_url', 'devshop.support') . '/server/' . $_SERVER['HTTP_HOST'], + '#options' => [ + 'html' => TRUE, + 'attributes' => [ + 'class' => ['text-muted'], + ], + ], + ]; + $links[] = drupal_render($active_support_license_links); + } + + $blocks = [ + 'content' => [ + 'links' => [ + '#theme' => 'item_list', + '#prefix' => '
    ', + '#title' => t('DevShop.Support'), + '#items' => array_filter($links), + '#attributes' => [ + 'class' => ['nav nav-pills nav-stacked'], + 'id' => ['devshop-support-status-block'], + ], + "#suffix" => "
    ", + ] + ], + ]; + return $blocks; +} + + +/** + * @param $num + * @param $max + * @param int $warning_threshold + * @return string + */ +function devshop_support_limit_class($num, $max, $warning_threshold = 1, $default_class = 'default') { + + if (strtolower($max) == 'unlimited') { + return 'success'; + } + + $num = (int) $num; + $max = (int) $max; + + if ($max <= $warning_threshold) { + $warning_threshold = 0; + } + + if ($num > $max) { + return 'danger'; + } + if ($num == $max) { + return 'warning'; + } + if ($num >= ($max - $warning_threshold)) { + return 'warning'; + } + if ($num < $max) { + return $default_class; + } + + return $default_class; +} + +/** + * Implements hook_hosting_queues(). + * + * @todo: In Hosting 4.x change the type to HOSTING_QUEUE_TYPE_SPREAD. + */ +function devshop_support_network_client_hosting_queues() { + $items['devshop_support'] = array( + 'name' => t('DevShop Support Agent'), + 'description' => t('DevShop Support Agent: Connect your DevShop to the DevShop.Support network.'), + 'frequency' => strtotime("1 minute", 0), + 'items' => 1, + 'enabled' => TRUE, + 'singular' => t('License Status Check'), + 'plural' => t('License Status Checks'), + ); + return $items; +} + +/** + * Alter the queues form to prevent editing of the devshop support license checker. + */ +function devshop_support_network_client_form_cas_admin_settings_alter(&$form, $form_state, $form_id) +{ +// $read_only = array(); +// +// $read_only['server'][] = 'cas_server'; +// $read_only['server'][] = 'cas_port'; +// $read_only['server'][] = 'cas_uri'; +// $read_only['server'][] = 'cas_port'; +// $read_only['server'][] = 'cas_version'; +// +// $read_only['login'][] = 'cas_login_redir_message'; +// $read_only['login'][] = 'cas_login_form'; +// $read_only['login'][] = 'cas_login_invite'; +// $read_only['login'][] = 'cas_login_drupal_invite'; +// $read_only['login'][] = 'cas_login_message'; +// +// $read_only['account'][] = 'cas_user_register'; +//// $read_only['account'][] = 'cas_domain'; +// $read_only['account'][] = 'cas_auto_assigned_role'; +// $read_only['account'][] = 'cas_hide_email'; +// $read_only['account'][] = 'cas_hide_password'; +// +//// $read_only['pages'][] = 'cas_check_frequency'; +//// $read_only['pages'][] = 'cas_access'; +//// $read_only['pages'][] = 'cas_pages'; +//// $read_only['pages'][] = 'cas_exclude'; +// +// $read_only['misc'][] = 'cas_first_login_destination'; +//// $read_only['misc'][] = 'cas_logout_destination'; +//// $read_only['misc'][] = 'cas_changePasswordURL'; +//// $read_only['misc'][] = 'cas_registerURL'; +// +// $read_only['advanced'][] = 'cas_proxy'; +// $read_only['advanced'][] = 'cas_proxy_settings'; +// $read_only['advanced'][] = 'cas_proxy_list'; +// $read_only['advanced'][] = 'cas_debugfile'; +// $read_only['advanced'][] = 'cas_single_logout_session_lifetime'; +// +// foreach ($read_only as $group => $items) { +// foreach ($items as $id) { +// $form[$group][$id]['#type'] = 'value'; +// $form[$group][$id . '_display'] = $form[$group][$id]; +// +// $form[$group][$id . '_display']['#type'] = 'item'; +// $form[$group][$id . '_display']['#markup'] = $form[$group][$id]['#default_value']; +// } +// } +} + +/** + * Alter the queues form to prevent editing of the devshop support license checker. + */ +function devshop_support_network_client_form_hosting_queues_configure_alter(&$form, $form_state, $form_id) { + $form['devshop_support']['enabled']['#type'] = 'value'; + $form['devshop_support']['frequency']['items']['#type'] = 'value'; + $form['devshop_support']['frequency']['items']['#suffix'] = t('License Status Checks every 1 minute.'); + $form['devshop_support']['frequency']['ticks']['#type'] = 'value'; + $form['devshop_support']['frequency']['unit']['#type'] = 'value'; +} + +/** + * Implements hosting_QUEUE_TYPE_queue(). + */ +function hosting_devshop_support_queue() { + devshop_support_network_client_post_data(); + watchdog('devshop_support_queue', 'Support License Queue Triggered'); + + +} + +/** + * Implements hook_node_view(). + */ +function devshop_support_network_client_node_view($node, $view_mode, $langcode) { + if ($node->type == 'server') { + if ($view_mode != 'teaser') { + // @todo : turn it into x minutes ago + $node->content['info']['last_cron'] = array( + '#type' => 'item', + '#title' => t('Cron run'), + '#weight' => 20, + '#markup' => hosting_format_interval($node->last_cron), + ); + } + } +} + +/** + * Implements hook_form_alter(). + */ +function devshop_support_network_client_form_user_login_alter(&$form, &$form_state, $form_id) +{ + + if (devshop_cloud_license_active()) { + + drupal_set_title(t('Welcome to @hostname!', array( + '@hostname' => $_SERVER['HTTP_HOST'], + ))); + + + $form['logo'] = array( + '#markup' => '', + ); + + $form['note'] = array( + '#prefix' => '

    ', + '#suffix' => '

    ', + '#markup' => t('Welcome to !link!', + array( + '!link' => l(variable_get('site_name', $_SERVER['HTTP_HOST']), '/'), + '!support_link' => l(t('DevShop.Support'), variable_get('devshop_support_url', 'devshop.support')), + ) + ), + ); + $form['note2'] = array( + '#prefix' => '

    ', + '#suffix' => '

    ', + '#markup' => t('Please sign in using one of the following options:'), + ); + $t_devshop = t('Sign in with DevShop.Cloud'); + $t_github = t('Sign in with GitHub'); + $form['cloud_login'] = array( + '#markup' => ' ' . $t_devshop . '', + ); + + $github_login_url = 'https://' . variable_get('devshop_support_url', 'devshop.support') . '/hybridauth/window/GitHub?destination=server/' . $_SERVER['HTTP_HOST'] . '/go&destination_error=cas/login'; + $form['github_login'] = array( + '#markup' => ' ' . $t_github . '', + ); + + $text = t('Sign in with @local', array( + '@local' => variable_get('site_title', $_SERVER['HTTP_HOST']), + )); + + if (variable_get('devshop_support_allow_local_login', TRUE)) { + + $items[] = array( + 'data' => '' . $text . '', + 'class' => 'uncas-link', + ); + + $text = t('Cancel'); + $items[] = array( + 'data' => '' . $text . '', + 'class' => 'cas-link', + ); + + $form['cas_links']['#items'] = $items; + } + else { + $form['cas_links']['#items'] = array(); + } + + // Hack to get cas.module to hide the login button. + $form['actions']['#attributes']['class'][] = 'form-item-name'; + + $form['name']['#weight'] = 1; + $form['pass']['#weight'] = 2; + $form['actions']['#weight'] = 3; + $form['cas_links']['#weight'] = 4; + + $form['name']['#prefix'] = '
    '; + $form['pass']['#suffix'] = '
    '; + + // Turn off "CAS" on user form submit, so it will work with automated testing, + // We actually don't need cas_identifier to be set to 1 because we used links instead of the form. + if ($_SERVER['HTTP_HOST'] == 'devshop.local.computer') { + $form['cas_identifier']['#default_value'] = 0; + } + } +} + + +/** + * Implements hook_form_alter(). + * + * Overrides specific from settings based on user policy. + */ +function devshop_support_network_client_form_alter(&$form, &$form_state, $form_id) { + + if ($form_id == 'user_login') { + } + +} + +/** + * Implements hook_cas_user_alter(). + */ +function devshop_support_network_client_cas_user_alter(&$cas_user) +{ + devshop_support_network_client_post_data(); +} \ No newline at end of file diff --git a/modules/devshop/devshop_support/license.inc b/modules/devshop/devshop_support/license.inc new file mode 100644 index 000000000..0b6bea33a --- /dev/null +++ b/modules/devshop/devshop_support/license.inc @@ -0,0 +1,142 @@ + 'POST'); + $options['data'] = http_build_query(devshop_support_network_client_client_data()); + $options['headers']['Content-Type'] = 'application/x-www-form-urlencoded'; + + $devshop_hostname = $_SERVER['HTTP_HOST']; + $response = drupal_http_request($get_support_url . '/network/server/' . $devshop_hostname . "/" . $license_key, $options); + $response_data = json_decode($response->data); +// drupal_set_message($get_support_url . '/network/server/' . $devshop_hostname . "/" . $license_key); + + watchdog('devshop_support_api', 'DevShop Support License Request ' . ($response->code == 200? 'Success: ': 'Failure: ') . print_r($response_data, 1 )); + + + // Save raw data to a variable. + variable_set('devshop_support_license_raw_data', (array) $response_data); + + // Set all arbitrary variables + $variables = devshop_support_network_default_variables(); + + if (!empty($response_data->_VARIABLES)) { + $variables = array_merge($variables, (array) $response_data->_VARIABLES); + } + + if (!empty($variables)) { + foreach ($variables as $name => $value) { + + if (is_object($value)) { + $value = (array) $value; + } + variable_set($name, $value); + } + } + + // Enable needed modules. + module_enable(array( + 'cas', + 'intercomio', + 'devshop_permissions', + 'composer_manager', + 'hosting_statsd', + )); + + // Enable HTTPS on the hostmaster site. + $node = hosting_context_load('hostmaster'); + if (module_exists('hosting_https') && $node->https_enabled != HOSTING_HTTPS_REQUIRED) { + watchdog('devshop_support_api', 'Hostmaster HTTPS was not enabled. Enabling.'); + $node->https_enabled = variable_get('DEVSHOP_SUPPORT_DEFAULT_HOSTING_HTTPS_ENABLED', HOSTING_HTTPS_REQUIRED); + node_save($node); + } + + drupal_flush_all_caches(); +// +// // Don't do any more for certain hostnames. +// if ($devshop_hostname == 'devshop.local.computer') { +// return $response; +// } + + // Create all CAS users. + module_load_include('batch.inc', 'cas'); + + // Always keep uid 1 active. + $active_local_uids = array(1); + $mapped_accounts = array(); + $mapped_account_names = array(); + + // Payload comes back an array of CAS usernames. + // @TODO: We need to retrieve email here as well to ensure intercom sync. + // @TODO: Here's where we could sync SSH keys. + foreach ($response_data->_USERS as $cas_user) { + + // Let's prevent admin from being set here. + if ($cas_user == 'admin') { + continue; + } + + // Try to create a CAS account. Load if not created. + // @TODO: cas_user_register() returns false if user already exists. + $account = cas_user_register($cas_user); + + if (empty($account)) { + $account = cas_user_load_by_name($cas_user, FALSE, TRUE); + } + + // if there is an account found, with a UID, save to $active_local_uids. + if ($account && is_numeric($account->uid)){ + $active_local_uids[] = $account->uid; + $mapped_accounts[] = $account; + $mapped_account_names[] = $account->name; + } + } + + // Disable all users not included in the license payload. + db_update('users') + ->fields(array( + 'status' => 0 + )) + ->condition('uid', $active_local_uids, 'NOT IN') + ->execute(); + + // Disable all users not included in the license payload. + db_update('users') + ->fields(array( + 'status' => 1 + )) + ->condition('uid', $active_local_uids, 'IN') + ->execute(); + + watchdog('devshop_support_api', 'Loaded !count devshop.support users: !users', array( + '!count' => count($active_local_uids), + '!users' => implode(' ', $mapped_account_names), + )); + + return $response; +} diff --git a/modules/devshop/devshop_testing/ansi-to-html/LICENSE b/modules/devshop/devshop_testing/ansi-to-html/LICENSE new file mode 100644 index 000000000..674bb2a6e --- /dev/null +++ b/modules/devshop/devshop_testing/ansi-to-html/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2013 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/modules/devshop/devshop_testing/ansi-to-html/README.md b/modules/devshop/devshop_testing/ansi-to-html/README.md new file mode 100644 index 000000000..90a03c7e2 --- /dev/null +++ b/modules/devshop/devshop_testing/ansi-to-html/README.md @@ -0,0 +1,109 @@ +ANSI to HTML5 Converter +======================= + +This small library only does one thing: converting a text containing ANSI +codes to an HTML5 fragment: + +```php +require_once __DIR__.'/../vendor/autoload.php'; + +use SensioLabs\AnsiConverter\AnsiToHtmlConverter; + +$converter = new AnsiToHtmlConverter(); + +$html = $converter->convert($ansi); +``` + +The `$ansi` variable should contain a text with ANSI codes, and `$html` will +contain the converted HTML5 version of it. + +You can then output the HTML5 fragment in any HTML document: + +```html+php + + +
    + + +``` + +The converter supports different color themes: + +```php +use SensioLabs\AnsiConverter\Theme\SolarizedTheme; + +$theme = new SolarizedTheme(); +$converter = new AnsiToHtmlConverter($theme); +``` + +By default, the colors are inlined into the HTML, but you can also use classes +by turning off style inlining: + +```php +$converter = new AnsiToHtmlConverter($theme, false); +``` + +And the `asCss()` method of the theme object let you retrieve the theme styles +as a CSS snippet: + +```php +$styles = $theme->asCss(); +``` + +which you can then use in your HTML document: + +```html+php + + + + + +
    + + +``` + +Twig Integration +---------------- + +Register the extension: + +```php +use SensioLabs\AnsiConverter\Bridge\Twig\AnsiExtension; + +$twig->addExtension(AnsiExtension()); +``` + +It's possible to use a custom ``AnsiToHtmlConverter``: + +```php +use SensioLabs\AnsiConverter\Bridge\Twig\AnsiExtension; +use SensioLabs\AnsiConverter\Theme\SolarizedTheme; + +$theme = new SolarizedTheme(); +$converter = new AnsiToHtmlConverter($theme, false); + +$twig->addExtension(AnsiExtension($converter)); +``` + +Then: + +```jinja + + + + + + {{ some_ansi_code|ansi_to_html }} + + +``` + diff --git a/modules/devshop/devshop_testing/ansi-to-html/SensioLabs/AnsiConverter/AnsiToHtmlConverter.php b/modules/devshop/devshop_testing/ansi-to-html/SensioLabs/AnsiConverter/AnsiToHtmlConverter.php new file mode 100644 index 000000000..f9a358f46 --- /dev/null +++ b/modules/devshop/devshop_testing/ansi-to-html/SensioLabs/AnsiConverter/AnsiToHtmlConverter.php @@ -0,0 +1,152 @@ +theme = null === $theme ? new Theme() : $theme; + $this->inlineStyles = $inlineStyles; + $this->charset = $charset; + $this->inlineColors = $this->theme->asArray(); + $this->colorNames = array( + 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', + '', '', + 'brblack', 'brred', 'brgreen', 'bryellow', 'brblue', 'brmagenta', 'brcyan', 'brwhite', + ); + } + + public function convert($text) + { + // remove cursor movement sequences + $text = preg_replace('#\e\[(K|s|u|2J|2K|\d+(A|B|C|D|E|F|G|J|K|S|T)|\d+;\d+(H|f))#', '', $text); + $text = htmlspecialchars($text, ENT_QUOTES, $this->charset); + + // carriage return + $text = preg_replace('#^.*\r(?!\n)#m', '', $text); + + $tokens = $this->tokenize($text); + + // a backspace remove the previous character but only from a text token + foreach ($tokens as $i => $token) { + if ('backspace' == $token[0]) { + $j = $i; + while (--$j >= 0) { + if ('text' == $tokens[$j][0] && strlen($tokens[$j][1]) > 0) { + $tokens[$j][1] = substr($tokens[$j][1], 0, -1); + + break; + } + } + } + } + + $html = ''; + foreach ($tokens as $token) { + if ('text' == $token[0]) { + $html .= $token[1]; + } elseif ('color' == $token[0]) { + $html .= $this->convertAnsiToColor($token[1]); + } + } + + if ($this->inlineStyles) { + $html = sprintf('%s', $this->inlineColors['black'], $this->inlineColors['white'], $html); + } else { + $html = sprintf('%s', $html); + } + + // remove empty span + $html = preg_replace('#]*>
    #', '', $html); + + return $html; + } + + public function getTheme() + { + return $this->theme; + } + + protected function convertAnsiToColor($ansi) + { + $bg = 0; + $fg = 7; + $as = ''; + if ('0' != $ansi && '' != $ansi) { + $options = explode(';', $ansi); + + foreach ($options as $option) { + if ($option >= 30 && $option < 38) { + $fg = $option - 30; + } elseif ($option >= 40 && $option < 48) { + $bg = $option - 40; + } elseif (39 == $option) { + $fg = 7; + } elseif (49 == $option) { + $bg = 0; + } + } + + // options: bold => 1, underscore => 4, blink => 5, reverse => 7, conceal => 8 + if (in_array(1, $options)) { + $fg += 10; + $bg += 10; + } + + if (in_array(4, $options)) { + $as = '; text-decoration: underline'; + } + + if (in_array(7, $options)) { + $tmp = $fg; $fg = $bg; $bg = $tmp; + } + } + + if ($this->inlineStyles) { + return sprintf('', $this->inlineColors[$this->colorNames[$bg]], $this->inlineColors[$this->colorNames[$fg]], $as); + } else { + return sprintf('', $this->colorNames[$bg], $this->colorNames[$fg]); + } + } + + protected function tokenize($text) + { + $tokens = array(); + preg_match_all("/(?:\e\[(.*?)m|(\x08))/", $text, $matches, PREG_OFFSET_CAPTURE); + + $offset = 0; + foreach ($matches[0] as $i => $match) { + if ($match[1] - $offset > 0) { + $tokens[] = array('text', substr($text, $offset, $match[1] - $offset)); + } + $tokens[] = array("\x08" == $match[0] ? 'backspace' : 'color', $matches[1][$i][0]); + $offset = $match[1] + strlen($match[0]); + } + if ($offset < strlen($text)) { + $tokens[] = array('text', substr($text, $offset)); + } + + return $tokens; + } +} diff --git a/modules/devshop/devshop_testing/ansi-to-html/SensioLabs/AnsiConverter/Bridge/Twig/AnsiExtension.php b/modules/devshop/devshop_testing/ansi-to-html/SensioLabs/AnsiConverter/Bridge/Twig/AnsiExtension.php new file mode 100644 index 000000000..a109bc459 --- /dev/null +++ b/modules/devshop/devshop_testing/ansi-to-html/SensioLabs/AnsiConverter/Bridge/Twig/AnsiExtension.php @@ -0,0 +1,44 @@ +converter = $converter ?: new AnsiToHtmlConverter(); + } + + public function getFilters() + { + return array( + new \Twig_SimpleFilter('ansi_to_html', array($this, 'ansiToHtml'), array('is_safe' => array('html'))), + ); + } + + public function getFunctions() + { + return array( + new \Twig_SimpleFunction('ansi_css', array($this, 'css'), array('is_safe' => array('css'))), + ); + } + + public function ansiToHtml($string) + { + return $this->converter->convert($string); + } + + public function css() + { + return $this->converter->getTheme()->asCss(); + } + + public function getName() + { + return 'sensiolabs_ansi'; + } +} diff --git a/modules/devshop/devshop_testing/ansi-to-html/SensioLabs/AnsiConverter/Tests/AnsiToHtmlConverterTest.php b/modules/devshop/devshop_testing/ansi-to-html/SensioLabs/AnsiConverter/Tests/AnsiToHtmlConverterTest.php new file mode 100644 index 000000000..04d9f2f43 --- /dev/null +++ b/modules/devshop/devshop_testing/ansi-to-html/SensioLabs/AnsiConverter/Tests/AnsiToHtmlConverterTest.php @@ -0,0 +1,56 @@ +assertEquals($expected, $converter->convert($input)); + } + + public function getConvertData() + { + return array( + // text is escaped + array('foo <br />', 'foo
    '), + + // newlines are preserved + array("foo\nbar", "foo\nbar"), + + // backspaces + array("foo ", "foobar\x08\x08\x08 "), + array("foo ", "foob\e[31;41ma\e[0mr\x08\x08\x08 "), + + // color + array("foo", "\e[31;41mfoo\e[0m"), + + // color with [m as a termination (equivalent to [0m]) + array("foo", "\e[31;41mfoo\e[m"), + + // bright color + array("foo", "\e[31;41;1mfoo\e[0m"), + + // carriage returns + array("foobar", "foo\rbar\rfoobar"), + + // underline + array("foo", "\e[4mfoo\e[0m"), + ); + } +} diff --git a/modules/devshop/devshop_testing/ansi-to-html/SensioLabs/AnsiConverter/Theme/SolarizedTheme.php b/modules/devshop/devshop_testing/ansi-to-html/SensioLabs/AnsiConverter/Theme/SolarizedTheme.php new file mode 100644 index 000000000..f362e6f5d --- /dev/null +++ b/modules/devshop/devshop_testing/ansi-to-html/SensioLabs/AnsiConverter/Theme/SolarizedTheme.php @@ -0,0 +1,45 @@ + '#073642', + 'red' => '#dc322f', + 'green' => '#859900', + 'yellow' => '#b58900', + 'blue' => '#268bd2', + 'magenta' => '#d33682', + 'cyan' => '#2aa198', + 'white' => '#eee8d5', + + // bright + 'brblack' => '#002b36', + 'brred' => '#cb4b16', + 'brgreen' => '#586e75', + 'bryellow' => '#657b83', + 'brblue' => '#839496', + 'brmagenta' => '#6c71c4', + 'brcyan' => '#93a1a1', + 'brwhite' => '#fdf6e3', + ); + } +} diff --git a/modules/devshop/devshop_testing/ansi-to-html/SensioLabs/AnsiConverter/Theme/SolarizedXTermTheme.php b/modules/devshop/devshop_testing/ansi-to-html/SensioLabs/AnsiConverter/Theme/SolarizedXTermTheme.php new file mode 100644 index 000000000..a5a9bd5fb --- /dev/null +++ b/modules/devshop/devshop_testing/ansi-to-html/SensioLabs/AnsiConverter/Theme/SolarizedXTermTheme.php @@ -0,0 +1,45 @@ + '#262626', + 'red' => '#d70000', + 'green' => '#5f8700', + 'yellow' => '#af8700', + 'blue' => '#0087ff', + 'magenta' => '#af005f', + 'cyan' => '#00afaf', + 'white' => '#e4e4e4', + + // bright + 'brblack' => '#1c1c1c', + 'brred' => '#d75f00', + 'brgreen' => '#585858', + 'bryellow' => '#626262', + 'brblue' => '#808080', + 'brmagenta' => '#5f5faf', + 'brcyan' => '#8a8a8a', + 'brwhite' => '#ffffd7', + ); + } +} diff --git a/modules/devshop/devshop_testing/ansi-to-html/SensioLabs/AnsiConverter/Theme/Theme.php b/modules/devshop/devshop_testing/ansi-to-html/SensioLabs/AnsiConverter/Theme/Theme.php new file mode 100644 index 000000000..2e14b15ef --- /dev/null +++ b/modules/devshop/devshop_testing/ansi-to-html/SensioLabs/AnsiConverter/Theme/Theme.php @@ -0,0 +1,52 @@ +asArray() as $name => $color) { + $css[] = sprintf('.%s_fg_%s { color: %s }', $prefix, $name, $color); + $css[] = sprintf('.%s_bg_%s { background-color: %s }', $prefix, $name, $color); + } + + return implode("\n", $css); + } + + public function asArray() + { + return array( + 'black' => 'black', + 'red' => 'darkred', + 'green' => 'green', + 'yellow' => 'yellow', + 'blue' => 'blue', + 'magenta' => 'darkmagenta', + 'cyan' => 'cyan', + 'white' => 'white', + + 'brblack' => 'black', + 'brred' => 'red', + 'brgreen' => 'lightgreen', + 'bryellow' => 'lightyellow', + 'brblue' => 'lightblue', + 'brmagenta' => 'magenta', + 'brcyan' => 'lightcyan', + 'brwhite' => 'white', + ); + } +} diff --git a/modules/devshop/devshop_testing/ansi-to-html/composer.json b/modules/devshop/devshop_testing/ansi-to-html/composer.json new file mode 100644 index 000000000..8ad0c4e16 --- /dev/null +++ b/modules/devshop/devshop_testing/ansi-to-html/composer.json @@ -0,0 +1,26 @@ +{ + "name": "sensiolabs/ansi-to-html", + "type": "library", + "description": "A library to convert a text with ANSI codes to HTML", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "require": { + "php": ">=5.3.0" + }, + "suggest": { + "twig/twig": "Provides nice templating features" + }, + "autoload": { + "psr-0": { "SensioLabs\\AnsiConverter": "." } + }, + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + } +} diff --git a/modules/devshop/devshop_testing/ansi-to-html/phpunit.xml.dist b/modules/devshop/devshop_testing/ansi-to-html/phpunit.xml.dist new file mode 100644 index 000000000..8014b0a69 --- /dev/null +++ b/modules/devshop/devshop_testing/ansi-to-html/phpunit.xml.dist @@ -0,0 +1,19 @@ + + + + + + ./SensioLabs/AnsiConverter/Tests + + + diff --git a/modules/devshop/devshop_testing/ansi-to-html/vendor/autoload.php b/modules/devshop/devshop_testing/ansi-to-html/vendor/autoload.php new file mode 100644 index 000000000..aca5d9a31 --- /dev/null +++ b/modules/devshop/devshop_testing/ansi-to-html/vendor/autoload.php @@ -0,0 +1,7 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0 class loader + * + * See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + */ +class ClassLoader +{ + // PSR-4 + private $prefixLengthsPsr4 = array(); + private $prefixDirsPsr4 = array(); + private $fallbackDirsPsr4 = array(); + + // PSR-0 + private $prefixesPsr0 = array(); + private $fallbackDirsPsr0 = array(); + + private $useIncludePath = false; + private $classMap = array(); + + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', $this->prefixesPsr0); + } + + return array(); + } + + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-0 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 base directories + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + } + + /** + * Unregisters this instance as an autoloader. + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return bool|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + includeFile($file); + + return true; + } + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731 + if ('\\' == $class[0]) { + $class = substr($class, 1); + } + + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if ($file === null && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if ($file === null) { + // Remember that this class does not exist. + return $this->classMap[$class] = false; + } + + return $file; + } + + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) { + if (0 === strpos($class, $prefix)) { + foreach ($this->prefixDirsPsr4[$prefix] as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + */ +function includeFile($file) +{ + include $file; +} diff --git a/modules/devshop/devshop_testing/ansi-to-html/vendor/composer/autoload_classmap.php b/modules/devshop/devshop_testing/ansi-to-html/vendor/composer/autoload_classmap.php new file mode 100644 index 000000000..7a91153b0 --- /dev/null +++ b/modules/devshop/devshop_testing/ansi-to-html/vendor/composer/autoload_classmap.php @@ -0,0 +1,9 @@ + array($baseDir . '/'), +); diff --git a/modules/devshop/devshop_testing/ansi-to-html/vendor/composer/autoload_psr4.php b/modules/devshop/devshop_testing/ansi-to-html/vendor/composer/autoload_psr4.php new file mode 100644 index 000000000..b265c64a2 --- /dev/null +++ b/modules/devshop/devshop_testing/ansi-to-html/vendor/composer/autoload_psr4.php @@ -0,0 +1,9 @@ + $path) { + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + + $loader->register(true); + + return $loader; + } +} + +function composerRequire97a8e3812a089e13a240206631a2cb81($file) +{ + require $file; +} diff --git a/modules/devshop/devshop_testing/devshop_testing.drush.inc b/modules/devshop/devshop_testing/devshop_testing.drush.inc new file mode 100644 index 000000000..e08fb250f --- /dev/null +++ b/modules/devshop/devshop_testing/devshop_testing.drush.inc @@ -0,0 +1,39 @@ +ref->environment->settings->deploy['test']) { + $node = hosting_add_task($task->ref->nid, 'test'); + drush_log(l(t('A Test task has been queued.'), "node/{$node->nid}"), 'p_log'); + } +} + +/** + * Implementation of hook_post_hosting_TASK_TYPE_task() for IMPORT tasks + * + * Ensures that cloned sites trigger a test run if their environment on first deploy. + * This is used when a Pull Request environment is created. + */ +function devshop_testing_post_hosting_import_task($task, $data) +{ + // Only work with site import. + if ($task->ref->type != 'site') { + return; + } + + // Load environment info from platform, because site node doesn't have info yet. + $platform = node_load($task->ref->platform); + + // If environment settings say to run tests on deploy... + if ($platform->environment->settings->deploy['test']) { + drush_log('[DEVSHOP] Triggering test run...', 'ok'); + hosting_add_task($task->ref->nid, 'test'); + } + else { + drush_log('[DEVSHOP] Skipping test run...', 'ok'); + } +} diff --git a/modules/devshop/devshop_testing/devshop_testing.info b/modules/devshop/devshop_testing/devshop_testing.info new file mode 100644 index 000000000..516ce0461 --- /dev/null +++ b/modules/devshop/devshop_testing/devshop_testing.info @@ -0,0 +1,6 @@ +name = DevShop Testing +description = Allow simpletests to be run easily on your DevShop projects. +core = 7.x +package = DevShop +files[] = pages.inc +dependencies[] = devshop_projects diff --git a/modules/devshop/devshop_testing/devshop_testing.install b/modules/devshop/devshop_testing/devshop_testing.install new file mode 100644 index 000000000..ad7efaef5 --- /dev/null +++ b/modules/devshop/devshop_testing/devshop_testing.install @@ -0,0 +1,63 @@ +fields(array( + 'weight' => 1 + )) + ->condition('name', 'devshop_testing') + ->execute(); +} + +/** + * Implements hook_enable() + */ +function devshop_testing_enable() { + drupal_set_message('The DevShop testing system has been enabled. Visit your projects settings pages to enable testing.'); +} + +/** + * Implementation of hook_schema(). + */ +function devshop_testing_schema() { + $schema['hosting_devshop_project_testing'] = array( + 'fields' => array( + 'project_nid' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'description' => 'Project Node ID.', + ), + 'tests_to_run' => array( + 'type' => 'text', + 'not null' => TRUE, + 'size' => 'big', + 'description' => 'A list of tests to run', + ), + ), + 'primary key' => array('project_nid'), + ); + + return $schema; +} + +/** + * Set a weight higher than devshop_project so our form altering can access the project settings. + */ +function devshop_testing_update_7000() { + db_update('system') + ->fields(array( + 'weight' => 2 + )) + ->condition('name', 'devshop_testing') + ->execute(); +} \ No newline at end of file diff --git a/modules/devshop/devshop_testing/devshop_testing.module b/modules/devshop/devshop_testing/devshop_testing.module new file mode 100644 index 000000000..c7c56ad9c --- /dev/null +++ b/modules/devshop/devshop_testing/devshop_testing.module @@ -0,0 +1,316 @@ +type == 'task' && $node->task_type == 'test' && user_access('access test results')) { + return TRUE; + } + else { + return node_access($op, $node, $account); + } +} + +/** + * Replacement access check for node/%/revision/%/view pages. + * + * We do this so we can check a secondary permission for Task nodes of type + * "test" + * + * @param $op + * @param $node + * @param null $account + */ +function devshop_testing_node_revision_access_alter($node, $op = 'view') { + if ($node->type == 'task' && $node->task_type == 'test' && user_access('access test results')) { + return TRUE; + } + else { + return _node_revision_access($node, $op); + } +} + +/** + * Implementation of hook_perm() + */ +function devshop_testing_permission() { + return array( + 'create test task' => array( + 'title' => t('Create test task'), + 'description' => t('Run tests on a site.'), + ), + 'access test results' => array( + 'title' => t('Access test results on task node pages.'), + 'description' => t('This permission grants special access to task nodes of type "test". Use this is you wish to grant outside developers (such as anonymous) access to test results.'), + ), + ); +} + +/** + * Implementation of hook_hosting_tasks() + */ +function devshop_testing_hosting_tasks() { + $tasks = array(); + + $tasks['site']['test'] = array( + 'title' => t('Run Tests'), + 'description' => t('Run tests on the site.'), + 'dialog' => TRUE, + 'task_permitted' => TRUE, + 'access callback' => 'devshop_hosting_task_menu_access', + 'icon' => 'pencil-square-o', + ); + + return $tasks; +} + +/** + * Implements hook_devshop_environment_menu(). + * + * Defines the list of tasks that appear under the gear icon. + */ +// I'm getting rid of this for now for testing. I want this button at the top. +//function devshop_testing_devshop_environment_menu($environment) { +// if ($environment->site && $environment->site_status == HOSTING_SITE_ENABLED) { +// return array( +// 'test' +// ); +// } +//} + +/** + * Implementation of hook_hosting_task_TASK_TYPE_form(). + * + * Our parameters are added in devshop_testing_form_alter() + */ +function hosting_task_test_form($node) { +// $form = devshop_testing_form_elements($node->project, $node->environment->name); + $form = array(); + return $form; +} + +/** + * Implements hook_form_alter(). + */ +function devshop_testing_form_alter(&$form, &$form_state, $form_id) { + + // If it's the project form, it's "defaults" for environments. + if ($form_id == 'project_node_form') { + $form['project']['settings']['testing'] = array_merge(array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#group' => 'project_settings', + '#title' => t('Testing'), + '#description' => t('Configure how testing is handled for this project.'), + ), devshop_testing_form_elements($form['#node']->project) + ); + } + + // If it's the site (environment) form, it is the override. + elseif ($form_id == 'site_node_form') { + if (!isset($form['#node']->project)) { + return; + } + $form['environment']['settings']['testing'] = array( + '#type' => 'fieldset', + '#title' => t('Testing'), + '#description' => t('Set the testing settings for this environment.'), + '#weight' => 100, + ); + $form['environment']['settings']['testing'] = devshop_testing_form_elements($form['#node']->project, $form['#node']->environment->name); + } + // Run Tests Form: add extra submit handler to stringify the test options. + elseif ($form_id == 'hosting_task_confirm_form' && $form['task']['#value'] == 'test') { +// array_unshift($form['#submit'], 'devshop_testing_task_submit'); + } +} + +/** + * Helper that returns the common form elements for running tests. + */ +function devshop_testing_form_elements($project, $environment_name = NULL) { + + + // Load environment object and settings, if we need it. + if ($environment_name) { + $environment = (object) $project->environments[$environment_name]; + $settings = $environment->settings; + } + else { + // If these settings are not for a specific environment, load up live or the first one. + if ($project->settings->live['live_environment']) { + $environment = (object) $project->environments[$project->settings->live['live_environment']]; + } + else { + $environment = array_shift($project->environments); + } + $settings = $project->settings; + } + + // If this is an environment form, and we are missing testing settings, inherit the project settings. + if (isset($environment) && !isset($settings->testing)) { + $settings->testing = $project->settings->testing; + } + // If a project and we are missing testing settings, load defaults. + // @TODO: Move this to the node API? + elseif (!isset($settings->testing)) { + $settings->testing = array( + 'test_type' => 0, + 'tests_to_run' => '', + 'behat_folder_path' => '', + 'behat_bin' => 'bin/behat', + ); + } + + // build this group of fields. + $testing_form = array(); + $testing_form['test_type'] = array( + '#title' => t('Test Type'), + '#options' => array( + '0' => 'none', + 'simpletest' => t('Simpletest'), + 'behat' => t('Behat'), + ), + '#type' => 'radios', + '#default_value' => $settings->testing['test_type'], + ); + + // Look for behat feature files. + // @TODO: Extract this to a hook to make tests types extensible. + + // @TODO: Figure out a better way to handle lists of tests per environment. +// $tests = array(); +// +// // Final check for repo root. +// if (empty($environment->repo_path)){ +// $tests[] = 'Something is wrong. Environment not found.'; +// } +// else { +// $directory = $environment->repo_path . '/' . $settings->testing['behat_folder_path'] . '/features'; +// +// $files = file_scan_directory($directory, '.feature', array('.', '..')); +// +// $tests = array(); +// foreach ($files as $file) { +// $value = str_replace($directory, '', $file->filename); +// $tests[$value] = $file->basename; +// } +// } +// +// $testing_form['tests_to_run'] = array( +// '#type' => 'checkboxes', +// '#options' => $tests, +// '#title' => t('Tests to Run'), +// '#default_value' => $settings->testing['tests_to_run'], +// '#description' => t('Select the tests you wish to run. Leave all unchecked to run all of them.'), +// ); +// +// if (empty($environment_name)) { +// $testing_form['tests_to_run']['#description'] = t('Select the tests you wish to run by default. These tests were detected from your environment %live.', array('%live' => $environment->name)); +// } + + $testing_form['behat_folder_path'] = array( + '#type' => 'textfield', + '#title' => t('Behat folder path'), + '#description' => t('Enter the path to your behat tests folder, relative to your git repo root.'), + '#default_value' => $settings->testing['behat_folder_path'], + ); + + // Lock fields in environment settings. + if ($environment_name) { + $locked = array( + 'test_type', + 'behat_folder_path', + ); + foreach ($locked as $field){ + $testing_form[$field]['#value'] = $testing_form[$field]['#default_value']; + $testing_form[$field]['#type'] = 'value'; + } + } + return $testing_form; +} + +/** + * Helper to stringify the tests to run. + */ +function devshop_testing_task_submit(&$form, &$form_state) { + + // Implode tests to run +// $value = implode(',', array_filter($form_state['values']['parameters']['tests_to_run'])); +// form_set_value($form['parameters']['tests_to_run'], $value, $form_state); +} + +/** + * Implements hook_devshop_deploy_hooks_form_elements_alter(). + */ +function devshop_testing_devshop_deploy_hooks_form_elements_alter(&$form, $is_environment_form) { + // Add to deploy hooks. + + // If this is the environment form and the project allows environment specific deploy hooks... + if ($is_environment_form && $form['#project']->settings->deploy['allow_environment_deploy_config']) { + $default_value = $form['#environment']->settings->deploy['test']; + } + // If this is env form and project does not allow env specific deploy hooks... + elseif ($is_environment_form && !$form['#project']->settings->deploy['allow_environment_deploy_config']) { + $default_value = $form['#project']->settings->deploy['default_hooks']['test']; + } + // If this is a project form... + elseif (!$is_environment_form) { + $default_value = $form['#project']->settings->deploy['default_hooks']['test']; + } + else { + $default_value = 0; + } + + $form['test'] = array( + '#type' => 'checkbox', + '#title' => t('Run Tests.'), + '#description' => t('Run tests after every code deploy.'), + '#weight' => 100, + '#default_value' => $default_value, + ); +} + +/** + * Implements hook_hosting_slack_message_alter() + */ +function devshop_testing_hosting_slack_message_alter(&$message_options) { + + + if ($message_options->task->task_type == 'test') { + + $url = url('node/' . $message_options->task->nid . '/revisions/' . $message_options->task->vid . '/view', array('absolute' => TRUE)); + + $attachment = new stdClass(); + $attachment->fallback = t('Results: ') . $url; + $attachment->title = t('Results: '); + $attachment->text = $url; + $attachment->title_link = $url; + + $message_options->attachments[] = $attachment; + + } +} diff --git a/modules/devshop/devshop_testing/pages.inc b/modules/devshop/devshop_testing/pages.inc new file mode 100644 index 000000000..e69de29bb diff --git a/modules/devshop/devshop_testing/test-reload.js b/modules/devshop/devshop_testing/test-reload.js new file mode 100644 index 000000000..a716697bb --- /dev/null +++ b/modules/devshop/devshop_testing/test-reload.js @@ -0,0 +1,30 @@ + + + setInterval(function(){ + if (Drupal.settings.test_running == 'FALSE') { + return; + } + + $.get(Drupal.settings.test_status_url, function( running ) { + if (running == 'TRUE') { + + console.log('Still running...'); + $('.results-wrapper').load(Drupal.settings.test_url, function () { + + if ($('#follow').prop('checked')) { + console.log('checked!'); + var position = $('footer').position(); + window.scrollTo(position.bottom,document.body.scrollHeight); + } + console.log('not checked!'); + + }); + } + else { + console.log('Test Ended!'); + Drupal.settings.test_running = 'FALSE'; + $('.follow-checkbox ').hide(); + } + }); + }, 1000); + diff --git a/modules/devshop/devshop_testing/tests_example/.gitignore b/modules/devshop/devshop_testing/tests_example/.gitignore new file mode 100644 index 000000000..b8735463e --- /dev/null +++ b/modules/devshop/devshop_testing/tests_example/.gitignore @@ -0,0 +1,2 @@ +vendor +bin diff --git a/modules/devshop/devshop_testing/tests_example/README.md b/modules/devshop/devshop_testing/tests_example/README.md new file mode 100644 index 000000000..9e0756e52 --- /dev/null +++ b/modules/devshop/devshop_testing/tests_example/README.md @@ -0,0 +1,6 @@ +# Drupal Behat Testing Template Folder + +The files in this folder are all you need to get started testing with devshop. + +Copy these files into a folder in your git repo, then configure your project with the path to that folder. + diff --git a/modules/devshop/devshop_testing/tests_example/behat.yml b/modules/devshop/devshop_testing/tests_example/behat.yml new file mode 100644 index 000000000..b2bf23987 --- /dev/null +++ b/modules/devshop/devshop_testing/tests_example/behat.yml @@ -0,0 +1,16 @@ +default: + suites: + default: + contexts: + - FeatureContext + - Drupal\DrupalExtension\Context\DrupalContext + - Drupal\DrupalExtension\Context\MinkContext + - Drupal\DrupalExtension\Context\MessageContext + - Drupal\DrupalExtension\Context\DrushContext + extensions: + Behat\MinkExtension: + goutte: ~ + selenium2: ~ + base_url: http://seven.l + Drupal\DrupalExtension: + blackbox: ~ diff --git a/modules/devshop/devshop_testing/tests_example/composer.json b/modules/devshop/devshop_testing/tests_example/composer.json new file mode 100644 index 000000000..7843f4154 --- /dev/null +++ b/modules/devshop/devshop_testing/tests_example/composer.json @@ -0,0 +1,8 @@ +{ + "require": { + "drupal/drupal-extension": "~3.0" +}, + "config": { + "bin-dir": "bin/" + } +} diff --git a/modules/devshop/devshop_testing/tests_example/composer.lock b/modules/devshop/devshop_testing/tests_example/composer.lock new file mode 100644 index 000000000..f42a4b24d --- /dev/null +++ b/modules/devshop/devshop_testing/tests_example/composer.lock @@ -0,0 +1,1643 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "hash": "dbdf3b74a1303f621119474c5c850b4d", + "content-hash": "44e5da7b296c0097945e907dccf96387", + "packages": [ + { + "name": "behat/behat", + "version": "v3.1.0", + "source": { + "type": "git", + "url": "https://github.com/Behat/Behat.git", + "reference": "359d987b3064d78f2d3a6ba3a355277f3b09b47f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/Behat/zipball/359d987b3064d78f2d3a6ba3a355277f3b09b47f", + "reference": "359d987b3064d78f2d3a6ba3a355277f3b09b47f", + "shasum": "" + }, + "require": { + "behat/gherkin": "~4.4", + "behat/transliterator": "~1.0", + "ext-mbstring": "*", + "php": ">=5.3.3", + "symfony/class-loader": "~2.1|~3.0", + "symfony/config": "~2.3|~3.0", + "symfony/console": "~2.1|~3.0", + "symfony/dependency-injection": "~2.1|~3.0", + "symfony/event-dispatcher": "~2.1|~3.0", + "symfony/translation": "~2.3|~3.0", + "symfony/yaml": "~2.1|~3.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.5", + "symfony/process": "~2.1|~3.0" + }, + "suggest": { + "behat/mink-extension": "for integration with Mink testing framework", + "behat/symfony2-extension": "for integration with Symfony2 web framework", + "behat/yii-extension": "for integration with Yii web framework" + }, + "bin": [ + "bin/behat" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1.x-dev" + } + }, + "autoload": { + "psr-0": { + "Behat\\Behat": "src/", + "Behat\\Testwork": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Scenario-oriented BDD framework for PHP 5.3", + "homepage": "http://behat.org/", + "keywords": [ + "Agile", + "BDD", + "ScenarioBDD", + "Scrum", + "StoryBDD", + "User story", + "business", + "development", + "documentation", + "examples", + "symfony", + "testing" + ], + "time": "2016-03-28 07:04:45" + }, + { + "name": "behat/gherkin", + "version": "v4.4.1", + "source": { + "type": "git", + "url": "https://github.com/Behat/Gherkin.git", + "reference": "1576b485c0f92ef6d27da9c4bbfc57ee30cf6911" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/Gherkin/zipball/1576b485c0f92ef6d27da9c4bbfc57ee30cf6911", + "reference": "1576b485c0f92ef6d27da9c4bbfc57ee30cf6911", + "shasum": "" + }, + "require": { + "php": ">=5.3.1" + }, + "require-dev": { + "phpunit/phpunit": "~4.0", + "symfony/yaml": "~2.1" + }, + "suggest": { + "symfony/yaml": "If you want to parse features, represented in YAML files" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.4-dev" + } + }, + "autoload": { + "psr-0": { + "Behat\\Gherkin": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Gherkin DSL parser for PHP 5.3", + "homepage": "http://behat.org/", + "keywords": [ + "BDD", + "Behat", + "Cucumber", + "DSL", + "gherkin", + "parser" + ], + "time": "2015-12-30 14:47:00" + }, + { + "name": "behat/mink", + "version": "v1.7.1", + "source": { + "type": "git", + "url": "https://github.com/minkphp/Mink.git", + "reference": "e6930b9c74693dff7f4e58577e1b1743399f3ff9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/minkphp/Mink/zipball/e6930b9c74693dff7f4e58577e1b1743399f3ff9", + "reference": "e6930b9c74693dff7f4e58577e1b1743399f3ff9", + "shasum": "" + }, + "require": { + "php": ">=5.3.1", + "symfony/css-selector": "~2.1|~3.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "~2.7|~3.0" + }, + "suggest": { + "behat/mink-browserkit-driver": "extremely fast headless driver for Symfony\\Kernel-based apps (Sf2, Silex)", + "behat/mink-goutte-driver": "fast headless driver for any app without JS emulation", + "behat/mink-selenium2-driver": "slow, but JS-enabled driver for any app (requires Selenium2)", + "behat/mink-zombie-driver": "fast and JS-enabled headless driver for any app (requires node.js)" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7.x-dev" + } + }, + "autoload": { + "psr-4": { + "Behat\\Mink\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Browser controller/emulator abstraction for PHP", + "homepage": "http://mink.behat.org/", + "keywords": [ + "browser", + "testing", + "web" + ], + "time": "2016-03-05 08:26:18" + }, + { + "name": "behat/mink-browserkit-driver", + "version": "v1.3.2", + "source": { + "type": "git", + "url": "https://github.com/minkphp/MinkBrowserKitDriver.git", + "reference": "10e67fb4a295efcd62ea0bf16025a85ea19534fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/minkphp/MinkBrowserKitDriver/zipball/10e67fb4a295efcd62ea0bf16025a85ea19534fb", + "reference": "10e67fb4a295efcd62ea0bf16025a85ea19534fb", + "shasum": "" + }, + "require": { + "behat/mink": "^1.7.1@dev", + "php": ">=5.3.6", + "symfony/browser-kit": "~2.3|~3.0", + "symfony/dom-crawler": "~2.3|~3.0" + }, + "require-dev": { + "silex/silex": "~1.2", + "symfony/phpunit-bridge": "~2.7|~3.0" + }, + "type": "mink-driver", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Behat\\Mink\\Driver\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Symfony2 BrowserKit driver for Mink framework", + "homepage": "http://mink.behat.org/", + "keywords": [ + "Mink", + "Symfony2", + "browser", + "testing" + ], + "time": "2016-03-05 08:59:47" + }, + { + "name": "behat/mink-extension", + "version": "v2.2", + "source": { + "type": "git", + "url": "https://github.com/Behat/MinkExtension.git", + "reference": "5b4bda64ff456104564317e212c823e45cad9d59" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/MinkExtension/zipball/5b4bda64ff456104564317e212c823e45cad9d59", + "reference": "5b4bda64ff456104564317e212c823e45cad9d59", + "shasum": "" + }, + "require": { + "behat/behat": "~3.0,>=3.0.5", + "behat/mink": "~1.5", + "php": ">=5.3.2", + "symfony/config": "~2.2|~3.0" + }, + "require-dev": { + "behat/mink-goutte-driver": "~1.1", + "phpspec/phpspec": "~2.0" + }, + "type": "behat-extension", + "extra": { + "branch-alias": { + "dev-master": "2.1.x-dev" + } + }, + "autoload": { + "psr-0": { + "Behat\\MinkExtension": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christophe Coevoet", + "email": "stof@notk.org" + }, + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com" + } + ], + "description": "Mink extension for Behat", + "homepage": "http://extensions.behat.org/mink", + "keywords": [ + "browser", + "gui", + "test", + "web" + ], + "time": "2016-02-15 07:55:18" + }, + { + "name": "behat/mink-goutte-driver", + "version": "v1.2.1", + "source": { + "type": "git", + "url": "https://github.com/minkphp/MinkGoutteDriver.git", + "reference": "8b9ad6d2d95bc70b840d15323365f52fcdaea6ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/minkphp/MinkGoutteDriver/zipball/8b9ad6d2d95bc70b840d15323365f52fcdaea6ca", + "reference": "8b9ad6d2d95bc70b840d15323365f52fcdaea6ca", + "shasum": "" + }, + "require": { + "behat/mink": "~1.6@dev", + "behat/mink-browserkit-driver": "~1.2@dev", + "fabpot/goutte": "~1.0.4|~2.0|~3.1", + "php": ">=5.3.1" + }, + "require-dev": { + "symfony/phpunit-bridge": "~2.7|~3.0" + }, + "type": "mink-driver", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Behat\\Mink\\Driver\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Goutte driver for Mink framework", + "homepage": "http://mink.behat.org/", + "keywords": [ + "browser", + "goutte", + "headless", + "testing" + ], + "time": "2016-03-05 09:04:22" + }, + { + "name": "behat/mink-selenium2-driver", + "version": "v1.3.1", + "source": { + "type": "git", + "url": "https://github.com/minkphp/MinkSelenium2Driver.git", + "reference": "473a9f3ebe0c134ee1e623ce8a9c852832020288" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/minkphp/MinkSelenium2Driver/zipball/473a9f3ebe0c134ee1e623ce8a9c852832020288", + "reference": "473a9f3ebe0c134ee1e623ce8a9c852832020288", + "shasum": "" + }, + "require": { + "behat/mink": "~1.7@dev", + "instaclick/php-webdriver": "~1.1", + "php": ">=5.3.1" + }, + "require-dev": { + "symfony/phpunit-bridge": "~2.7" + }, + "type": "mink-driver", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Behat\\Mink\\Driver\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Pete Otaqui", + "email": "pete@otaqui.com", + "homepage": "https://github.com/pete-otaqui" + } + ], + "description": "Selenium2 (WebDriver) driver for Mink framework", + "homepage": "http://mink.behat.org/", + "keywords": [ + "ajax", + "browser", + "javascript", + "selenium", + "testing", + "webdriver" + ], + "time": "2016-03-05 09:10:18" + }, + { + "name": "behat/transliterator", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/Behat/Transliterator.git", + "reference": "868e05be3a9f25ba6424c2dd4849567f50715003" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/Transliterator/zipball/868e05be3a9f25ba6424c2dd4849567f50715003", + "reference": "868e05be3a9f25ba6424c2dd4849567f50715003", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-0": { + "Behat\\Transliterator": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Artistic-1.0" + ], + "description": "String transliterator", + "keywords": [ + "i18n", + "slug", + "transliterator" + ], + "time": "2015-09-28 16:26:35" + }, + { + "name": "drupal/drupal-driver", + "version": "v1.1.6", + "source": { + "type": "git", + "url": "https://github.com/jhedstrom/DrupalDriver.git", + "reference": "c4c1f4826fd7fdb24733834b8a4e1b52ee87ceaa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jhedstrom/DrupalDriver/zipball/c4c1f4826fd7fdb24733834b8a4e1b52ee87ceaa", + "reference": "c4c1f4826fd7fdb24733834b8a4e1b52ee87ceaa", + "shasum": "" + }, + "require": { + "symfony/dependency-injection": "~2.6|~3.0", + "symfony/process": "~2.5|~3.0" + }, + "require-dev": { + "drupal/coder": "~8.2.0", + "mockery/mockery": "0.9.4", + "phpspec/phpspec": "~2.0", + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "Drupal\\Component": "src/", + "Drupal\\Driver": "src/", + "Drupal\\Tests\\Driver": "tests/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0+" + ], + "authors": [ + { + "name": "Jonathan Hedstrom", + "email": "jhedstrom@gmail.com" + } + ], + "description": "A collection of reusable Drupal drivers", + "homepage": "http://github.com/jhedstrom/DrupalDriver", + "keywords": [ + "drupal", + "test", + "web" + ], + "time": "2016-04-08 22:11:45" + }, + { + "name": "drupal/drupal-extension", + "version": "v3.1.5", + "source": { + "type": "git", + "url": "https://github.com/jhedstrom/drupalextension.git", + "reference": "6bfff4967d0efacff51e2ff51a306021012396d8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jhedstrom/drupalextension/zipball/6bfff4967d0efacff51e2ff51a306021012396d8", + "reference": "6bfff4967d0efacff51e2ff51a306021012396d8", + "shasum": "" + }, + "require": { + "behat/behat": "~3.0,>=3.0.5", + "behat/mink": "~1.5", + "behat/mink-extension": "~2.0", + "behat/mink-goutte-driver": "~1.0", + "behat/mink-selenium2-driver": "~1.1", + "drupal/drupal-driver": "~1.1" + }, + "require-dev": { + "behat/mink-zombie-driver": "^1.2", + "phpspec/phpspec": "~2.0", + "phpunit/phpunit": "3.7.*" + }, + "type": "behat-extension", + "autoload": { + "psr-0": { + "Drupal\\Drupal": "src/", + "Drupal\\Exception": "src/", + "Drupal\\DrupalExtension": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0+" + ], + "authors": [ + { + "name": "Jonathan Hedstrom", + "email": "jhedstrom@gmail.com" + } + ], + "description": "Drupal extension for Behat", + "homepage": "http://drupal.org/project/drupalextension", + "keywords": [ + "drupal", + "test", + "web" + ], + "time": "2015-12-22 20:10:14" + }, + { + "name": "fabpot/goutte", + "version": "v3.1.2", + "source": { + "type": "git", + "url": "https://github.com/FriendsOfPHP/Goutte.git", + "reference": "3cbc6ed222422a28400e470050f14928a153207e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FriendsOfPHP/Goutte/zipball/3cbc6ed222422a28400e470050f14928a153207e", + "reference": "3cbc6ed222422a28400e470050f14928a153207e", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^6.0", + "php": ">=5.5.0", + "symfony/browser-kit": "~2.1|~3.0", + "symfony/css-selector": "~2.1|~3.0", + "symfony/dom-crawler": "~2.1|~3.0" + }, + "type": "application", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "psr-4": { + "Goutte\\": "Goutte" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "A simple PHP Web Scraper", + "homepage": "https://github.com/FriendsOfPHP/Goutte", + "keywords": [ + "scraper" + ], + "time": "2015-11-05 12:58:44" + }, + { + "name": "guzzlehttp/guzzle", + "version": "6.2.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "d094e337976dff9d8e2424e8485872194e768662" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d094e337976dff9d8e2424e8485872194e768662", + "reference": "d094e337976dff9d8e2424e8485872194e768662", + "shasum": "" + }, + "require": { + "guzzlehttp/promises": "~1.0", + "guzzlehttp/psr7": "~1.1", + "php": ">=5.5.0" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "~4.0", + "psr/log": "~1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.2-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "time": "2016-03-21 20:02:09" + }, + { + "name": "guzzlehttp/promises", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "c10d860e2a9595f8883527fa0021c7da9e65f579" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/c10d860e2a9595f8883527fa0021c7da9e65f579", + "reference": "c10d860e2a9595f8883527fa0021c7da9e65f579", + "shasum": "" + }, + "require": { + "php": ">=5.5.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "time": "2016-05-18 16:56:05" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "31382fef2889136415751badebbd1cb022a4ed72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/31382fef2889136415751badebbd1cb022a4ed72", + "reference": "31382fef2889136415751badebbd1cb022a4ed72", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "PSR-7 message implementation", + "keywords": [ + "http", + "message", + "stream", + "uri" + ], + "time": "2016-04-13 19:56:01" + }, + { + "name": "instaclick/php-webdriver", + "version": "1.4.3", + "source": { + "type": "git", + "url": "https://github.com/instaclick/php-webdriver.git", + "reference": "0c20707dcf30a32728fd6bdeeab996c887fdb2fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/instaclick/php-webdriver/zipball/0c20707dcf30a32728fd6bdeeab996c887fdb2fb", + "reference": "0c20707dcf30a32728fd6bdeeab996c887fdb2fb", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": ">=5.3.2" + }, + "require-dev": { + "satooshi/php-coveralls": "dev-master" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "psr-0": { + "WebDriver": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Justin Bishop", + "email": "jubishop@gmail.com", + "role": "Developer" + }, + { + "name": "Anthon Pang", + "email": "apang@softwaredevelopment.ca", + "role": "Fork Maintainer" + } + ], + "description": "PHP WebDriver for Selenium 2", + "homepage": "http://instaclick.com/", + "keywords": [ + "browser", + "selenium", + "webdriver", + "webtest" + ], + "time": "2015-06-15 20:19:33" + }, + { + "name": "psr/http-message", + "version": "1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "85d63699f0dbedb190bbd4b0d2b9dc707ea4c298" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/85d63699f0dbedb190bbd4b0d2b9dc707ea4c298", + "reference": "85d63699f0dbedb190bbd4b0d2b9dc707ea4c298", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "time": "2015-05-04 20:22:00" + }, + { + "name": "symfony/browser-kit", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/browser-kit.git", + "reference": "b645a9b23d6c0eeba5ac823fa87bf010db9aff22" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/b645a9b23d6c0eeba5ac823fa87bf010db9aff22", + "reference": "b645a9b23d6c0eeba5ac823fa87bf010db9aff22", + "shasum": "" + }, + "require": { + "php": ">=5.5.9", + "symfony/dom-crawler": "~2.8|~3.0" + }, + "require-dev": { + "symfony/css-selector": "~2.8|~3.0", + "symfony/process": "~2.8|~3.0" + }, + "suggest": { + "symfony/process": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\BrowserKit\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony BrowserKit Component", + "homepage": "https://symfony.com", + "time": "2016-03-04 07:56:56" + }, + { + "name": "symfony/class-loader", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/class-loader.git", + "reference": "6ebc60f69a6df4b3cf5ad6f260ba4edf5957ea05" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/class-loader/zipball/6ebc60f69a6df4b3cf5ad6f260ba4edf5957ea05", + "reference": "6ebc60f69a6df4b3cf5ad6f260ba4edf5957ea05", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "require-dev": { + "symfony/finder": "~2.8|~3.0", + "symfony/polyfill-apcu": "~1.1" + }, + "suggest": { + "symfony/polyfill-apcu": "For using ApcClassLoader on HHVM" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\ClassLoader\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony ClassLoader Component", + "homepage": "https://symfony.com", + "time": "2016-03-30 10:41:47" + }, + { + "name": "symfony/config", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "048dc47e07f92333203c3b7045868bbc864fc40e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/048dc47e07f92333203c3b7045868bbc864fc40e", + "reference": "048dc47e07f92333203c3b7045868bbc864fc40e", + "shasum": "" + }, + "require": { + "php": ">=5.5.9", + "symfony/filesystem": "~2.8|~3.0" + }, + "suggest": { + "symfony/yaml": "To use the yaml reference dumper" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Config Component", + "homepage": "https://symfony.com", + "time": "2016-05-20 11:48:17" + }, + { + "name": "symfony/console", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "64a4d43b045f07055bb197650159769604cb2a92" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/64a4d43b045f07055bb197650159769604cb2a92", + "reference": "64a4d43b045f07055bb197650159769604cb2a92", + "shasum": "" + }, + "require": { + "php": ">=5.5.9", + "symfony/polyfill-mbstring": "~1.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/event-dispatcher": "~2.8|~3.0", + "symfony/process": "~2.8|~3.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/process": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Console Component", + "homepage": "https://symfony.com", + "time": "2016-06-14 11:18:07" + }, + { + "name": "symfony/css-selector", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "c526d7b3cb4fe1673c6a34e13be2ff63f519df99" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/c526d7b3cb4fe1673c6a34e13be2ff63f519df99", + "reference": "c526d7b3cb4fe1673c6a34e13be2ff63f519df99", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony CssSelector Component", + "homepage": "https://symfony.com", + "time": "2016-06-06 11:42:41" + }, + { + "name": "symfony/dependency-injection", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "4b9645f5870e0b4ab35cc5b528422049946e8bec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/4b9645f5870e0b4ab35cc5b528422049946e8bec", + "reference": "4b9645f5870e0b4ab35cc5b528422049946e8bec", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "require-dev": { + "symfony/config": "~2.8|~3.0", + "symfony/expression-language": "~2.8|~3.0", + "symfony/yaml": "~2.8|~3.0" + }, + "suggest": { + "symfony/config": "", + "symfony/expression-language": "For using expressions in service container configuration", + "symfony/proxy-manager-bridge": "Generate service proxies to lazy load them", + "symfony/yaml": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\DependencyInjection\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony DependencyInjection Component", + "homepage": "https://symfony.com", + "time": "2016-06-14 11:18:07" + }, + { + "name": "symfony/dom-crawler", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/dom-crawler.git", + "reference": "12aa63fd41b060d2bee9a34623d29eda70bc8fe3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/12aa63fd41b060d2bee9a34623d29eda70bc8fe3", + "reference": "12aa63fd41b060d2bee9a34623d29eda70bc8fe3", + "shasum": "" + }, + "require": { + "php": ">=5.5.9", + "symfony/polyfill-mbstring": "~1.0" + }, + "require-dev": { + "symfony/css-selector": "~2.8|~3.0" + }, + "suggest": { + "symfony/css-selector": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\DomCrawler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony DomCrawler Component", + "homepage": "https://symfony.com", + "time": "2016-05-13 15:49:09" + }, + { + "name": "symfony/event-dispatcher", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "f5b7563f67779c6d3d5370e23448e707c858df3e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/f5b7563f67779c6d3d5370e23448e707c858df3e", + "reference": "f5b7563f67779c6d3d5370e23448e707c858df3e", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~2.8|~3.0", + "symfony/dependency-injection": "~2.8|~3.0", + "symfony/expression-language": "~2.8|~3.0", + "symfony/stopwatch": "~2.8|~3.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony EventDispatcher Component", + "homepage": "https://symfony.com", + "time": "2016-06-06 11:42:41" + }, + { + "name": "symfony/filesystem", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "5751e80d6f94b7c018f338a4a7be0b700d6f3058" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/5751e80d6f94b7c018f338a4a7be0b700d6f3058", + "reference": "5751e80d6f94b7c018f338a4a7be0b700d6f3058", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Filesystem Component", + "homepage": "https://symfony.com", + "time": "2016-04-12 18:27:47" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "dff51f72b0706335131b00a7f49606168c582594" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/dff51f72b0706335131b00a7f49606168c582594", + "reference": "dff51f72b0706335131b00a7f49606168c582594", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "time": "2016-05-18 14:26:46" + }, + { + "name": "symfony/process", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "6350e63ed9c232da50e00f00a7e0330f066387a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/6350e63ed9c232da50e00f00a7e0330f066387a2", + "reference": "6350e63ed9c232da50e00f00a7e0330f066387a2", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Process Component", + "homepage": "https://symfony.com", + "time": "2016-06-06 11:42:41" + }, + { + "name": "symfony/translation", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "b36e79d7bbbfa4a7e9708335082b3eba2263d356" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/b36e79d7bbbfa4a7e9708335082b3eba2263d356", + "reference": "b36e79d7bbbfa4a7e9708335082b3eba2263d356", + "shasum": "" + }, + "require": { + "php": ">=5.5.9", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/config": "<2.8" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~2.8|~3.0", + "symfony/intl": "~2.8|~3.0", + "symfony/yaml": "~2.8|~3.0" + }, + "suggest": { + "psr/log": "To use logging capability in translator", + "symfony/config": "", + "symfony/yaml": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Translation Component", + "homepage": "https://symfony.com", + "time": "2016-06-14 11:18:07" + }, + { + "name": "symfony/yaml", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "c5a7e7fc273c758b92b85dcb9c46149ccda89623" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/c5a7e7fc273c758b92b85dcb9c46149ccda89623", + "reference": "c5a7e7fc273c758b92b85dcb9c46149ccda89623", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Yaml Component", + "homepage": "https://symfony.com", + "time": "2016-06-14 11:18:07" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} diff --git a/modules/devshop/devshop_testing/tests_example/features/bootstrap/FeatureContext.php b/modules/devshop/devshop_testing/tests_example/features/bootstrap/FeatureContext.php new file mode 100644 index 000000000..da963c0ae --- /dev/null +++ b/modules/devshop/devshop_testing/tests_example/features/bootstrap/FeatureContext.php @@ -0,0 +1,23 @@ + t('DevShop'), + 'description' => t('DevShop Hosting Platform'), + 'status' => HOSTING_FEATURE_REQUIRED, + 'module' => 'devshop_projects', + 'group' => 'advanced', + ); + $features['devshop_testing'] = array( + 'title' => t('DevShop Testing'), + 'description' => t('Allows running of tests on sites.'), + 'status' => HOSTING_FEATURE_DISABLED, + 'module' => 'devshop_testing', + 'group' => 'advanced', + ); + $features['devshop_dothooks'] = array( + 'title' => t('DevShop .Hooks'), + 'description' => t('Allows projects to define their deploy hooks in a .hooks.yml file.'), + 'status' => HOSTING_FEATURE_DISABLED, + 'module' => 'devshop_dothooks', + 'group' => 'advanced', + ); + $features['devshop_acquia'] = array( + 'title' => t('DevShop Acquia'), + 'description' => t('Allows environments to use Acquia Cloud Hooks for Deployment Hooks.'), + 'status' => HOSTING_FEATURE_DISABLED, + 'module' => 'devshop_acquia', + 'group' => 'advanced', + ); + $features['devshop_remotes'] = array( + 'title' => t('DevShop Remote Aliases'), + 'description' => t('Allows projects to add remote drush aliases as data sources.'), + 'status' => HOSTING_FEATURE_DISABLED, + 'module' => 'devshop_remotes', + 'group' => 'advanced', + ); + return $features; +} diff --git a/modules/devshop/logo-aegir.png b/modules/devshop/logo-aegir.png new file mode 100644 index 000000000..2aa6f5e98 Binary files /dev/null and b/modules/devshop/logo-aegir.png differ diff --git a/modules/devshop/logo-td.png b/modules/devshop/logo-td.png new file mode 100644 index 000000000..7b1b00cd9 Binary files /dev/null and b/modules/devshop/logo-td.png differ diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 000000000..43d08e839 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1,2 @@ +bin +vendor diff --git a/tests/behat.yml b/tests/behat.yml new file mode 100644 index 000000000..19b7a6b9d --- /dev/null +++ b/tests/behat.yml @@ -0,0 +1,42 @@ +default: + suites: + default: + contexts: + - FeatureContext + - Drupal\DrupalExtension\Context\DrupalContext + - Drupal\DrupalExtension\Context\DrushContext + - Drupal\DrupalExtension\Context\MinkContext + extensions: + Behat\MinkExtension: + goutte: ~ + selenium2: + wd_host: http://selenium:4444/wd/hub + # We do not include base_url here because then we cannot override it on the command line. + # base_url: http://devshop.site + show_cmd: firefox %s + Drupal\DrupalExtension: + blackbox: ~ + api_driver: 'drupal' + drupal: + # We do not include root here because then we cannot override it on the command line. + # drupal_root: /var/aegir/devmaster-1.x + drush: + alias: 'hostmaster' + # We do not include root here because then we cannot override it on the command line. + # root: /var/aegir/devmaster-0.x + region_map: + main: "#block-system-main" + +# Use this profile when running bin/behat directly. +# Usage: +# bin/behat --profile=devmaster +devmaster: + extensions: + Behat\MinkExtension: + base_url: http://devshop.local.computer + Drupal\DrupalExtension: + drupal: + drupal_root: /var/aegir/devmaster-1.x + drush: + alias: 'hostmaster' + root: /var/aegir/devmaster-1.x \ No newline at end of file diff --git a/tests/composer.json b/tests/composer.json new file mode 100644 index 000000000..d1621df39 --- /dev/null +++ b/tests/composer.json @@ -0,0 +1,8 @@ +{ + "require": { + "drupal/drupal-extension": "~3.1" +}, + "config": { + "bin-dir": "bin/" + } +} diff --git a/tests/composer.lock b/tests/composer.lock new file mode 100644 index 000000000..75e4e6ea2 --- /dev/null +++ b/tests/composer.lock @@ -0,0 +1,1787 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "content-hash": "e19bd2dcfa9714b62f57ec3281177ca2", + "packages": [ + { + "name": "behat/behat", + "version": "v3.1.0", + "source": { + "type": "git", + "url": "https://github.com/Behat/Behat.git", + "reference": "359d987b3064d78f2d3a6ba3a355277f3b09b47f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/Behat/zipball/359d987b3064d78f2d3a6ba3a355277f3b09b47f", + "reference": "359d987b3064d78f2d3a6ba3a355277f3b09b47f", + "shasum": "" + }, + "require": { + "behat/gherkin": "~4.4", + "behat/transliterator": "~1.0", + "ext-mbstring": "*", + "php": ">=5.3.3", + "symfony/class-loader": "~2.1|~3.0", + "symfony/config": "~2.3|~3.0", + "symfony/console": "~2.1|~3.0", + "symfony/dependency-injection": "~2.1|~3.0", + "symfony/event-dispatcher": "~2.1|~3.0", + "symfony/translation": "~2.3|~3.0", + "symfony/yaml": "~2.1|~3.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.5", + "symfony/process": "~2.1|~3.0" + }, + "suggest": { + "behat/mink-extension": "for integration with Mink testing framework", + "behat/symfony2-extension": "for integration with Symfony2 web framework", + "behat/yii-extension": "for integration with Yii web framework" + }, + "bin": [ + "bin/behat" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1.x-dev" + } + }, + "autoload": { + "psr-0": { + "Behat\\Behat": "src/", + "Behat\\Testwork": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Scenario-oriented BDD framework for PHP 5.3", + "homepage": "http://behat.org/", + "keywords": [ + "Agile", + "BDD", + "ScenarioBDD", + "Scrum", + "StoryBDD", + "User story", + "business", + "development", + "documentation", + "examples", + "symfony", + "testing" + ], + "time": "2016-03-28T07:04:45+00:00" + }, + { + "name": "behat/gherkin", + "version": "v4.4.5", + "source": { + "type": "git", + "url": "https://github.com/Behat/Gherkin.git", + "reference": "5c14cff4f955b17d20d088dec1bde61c0539ec74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/Gherkin/zipball/5c14cff4f955b17d20d088dec1bde61c0539ec74", + "reference": "5c14cff4f955b17d20d088dec1bde61c0539ec74", + "shasum": "" + }, + "require": { + "php": ">=5.3.1" + }, + "require-dev": { + "phpunit/phpunit": "~4.5|~5", + "symfony/phpunit-bridge": "~2.7|~3", + "symfony/yaml": "~2.3|~3" + }, + "suggest": { + "symfony/yaml": "If you want to parse features, represented in YAML files" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.4-dev" + } + }, + "autoload": { + "psr-0": { + "Behat\\Gherkin": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Gherkin DSL parser for PHP 5.3", + "homepage": "http://behat.org/", + "keywords": [ + "BDD", + "Behat", + "Cucumber", + "DSL", + "gherkin", + "parser" + ], + "time": "2016-10-30T11:50:56+00:00" + }, + { + "name": "behat/mink", + "version": "v1.7.1", + "source": { + "type": "git", + "url": "https://github.com/minkphp/Mink.git", + "reference": "e6930b9c74693dff7f4e58577e1b1743399f3ff9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/minkphp/Mink/zipball/e6930b9c74693dff7f4e58577e1b1743399f3ff9", + "reference": "e6930b9c74693dff7f4e58577e1b1743399f3ff9", + "shasum": "" + }, + "require": { + "php": ">=5.3.1", + "symfony/css-selector": "~2.1|~3.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "~2.7|~3.0" + }, + "suggest": { + "behat/mink-browserkit-driver": "extremely fast headless driver for Symfony\\Kernel-based apps (Sf2, Silex)", + "behat/mink-goutte-driver": "fast headless driver for any app without JS emulation", + "behat/mink-selenium2-driver": "slow, but JS-enabled driver for any app (requires Selenium2)", + "behat/mink-zombie-driver": "fast and JS-enabled headless driver for any app (requires node.js)" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7.x-dev" + } + }, + "autoload": { + "psr-4": { + "Behat\\Mink\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Browser controller/emulator abstraction for PHP", + "homepage": "http://mink.behat.org/", + "keywords": [ + "browser", + "testing", + "web" + ], + "time": "2016-03-05T08:26:18+00:00" + }, + { + "name": "behat/mink-browserkit-driver", + "version": "v1.3.2", + "source": { + "type": "git", + "url": "https://github.com/minkphp/MinkBrowserKitDriver.git", + "reference": "10e67fb4a295efcd62ea0bf16025a85ea19534fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/minkphp/MinkBrowserKitDriver/zipball/10e67fb4a295efcd62ea0bf16025a85ea19534fb", + "reference": "10e67fb4a295efcd62ea0bf16025a85ea19534fb", + "shasum": "" + }, + "require": { + "behat/mink": "^1.7.1@dev", + "php": ">=5.3.6", + "symfony/browser-kit": "~2.3|~3.0", + "symfony/dom-crawler": "~2.3|~3.0" + }, + "require-dev": { + "silex/silex": "~1.2", + "symfony/phpunit-bridge": "~2.7|~3.0" + }, + "type": "mink-driver", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Behat\\Mink\\Driver\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Symfony2 BrowserKit driver for Mink framework", + "homepage": "http://mink.behat.org/", + "keywords": [ + "Mink", + "Symfony2", + "browser", + "testing" + ], + "time": "2016-03-05T08:59:47+00:00" + }, + { + "name": "behat/mink-extension", + "version": "v2.2", + "source": { + "type": "git", + "url": "https://github.com/Behat/MinkExtension.git", + "reference": "5b4bda64ff456104564317e212c823e45cad9d59" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/MinkExtension/zipball/5b4bda64ff456104564317e212c823e45cad9d59", + "reference": "5b4bda64ff456104564317e212c823e45cad9d59", + "shasum": "" + }, + "require": { + "behat/behat": "~3.0,>=3.0.5", + "behat/mink": "~1.5", + "php": ">=5.3.2", + "symfony/config": "~2.2|~3.0" + }, + "require-dev": { + "behat/mink-goutte-driver": "~1.1", + "phpspec/phpspec": "~2.0" + }, + "type": "behat-extension", + "extra": { + "branch-alias": { + "dev-master": "2.1.x-dev" + } + }, + "autoload": { + "psr-0": { + "Behat\\MinkExtension": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christophe Coevoet", + "email": "stof@notk.org" + }, + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com" + } + ], + "description": "Mink extension for Behat", + "homepage": "http://extensions.behat.org/mink", + "keywords": [ + "browser", + "gui", + "test", + "web" + ], + "time": "2016-02-15T07:55:18+00:00" + }, + { + "name": "behat/mink-goutte-driver", + "version": "v1.2.1", + "source": { + "type": "git", + "url": "https://github.com/minkphp/MinkGoutteDriver.git", + "reference": "8b9ad6d2d95bc70b840d15323365f52fcdaea6ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/minkphp/MinkGoutteDriver/zipball/8b9ad6d2d95bc70b840d15323365f52fcdaea6ca", + "reference": "8b9ad6d2d95bc70b840d15323365f52fcdaea6ca", + "shasum": "" + }, + "require": { + "behat/mink": "~1.6@dev", + "behat/mink-browserkit-driver": "~1.2@dev", + "fabpot/goutte": "~1.0.4|~2.0|~3.1", + "php": ">=5.3.1" + }, + "require-dev": { + "symfony/phpunit-bridge": "~2.7|~3.0" + }, + "type": "mink-driver", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Behat\\Mink\\Driver\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Goutte driver for Mink framework", + "homepage": "http://mink.behat.org/", + "keywords": [ + "browser", + "goutte", + "headless", + "testing" + ], + "time": "2016-03-05T09:04:22+00:00" + }, + { + "name": "behat/mink-selenium2-driver", + "version": "v1.3.1", + "source": { + "type": "git", + "url": "https://github.com/minkphp/MinkSelenium2Driver.git", + "reference": "473a9f3ebe0c134ee1e623ce8a9c852832020288" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/minkphp/MinkSelenium2Driver/zipball/473a9f3ebe0c134ee1e623ce8a9c852832020288", + "reference": "473a9f3ebe0c134ee1e623ce8a9c852832020288", + "shasum": "" + }, + "require": { + "behat/mink": "~1.7@dev", + "instaclick/php-webdriver": "~1.1", + "php": ">=5.3.1" + }, + "require-dev": { + "symfony/phpunit-bridge": "~2.7" + }, + "type": "mink-driver", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Behat\\Mink\\Driver\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Pete Otaqui", + "email": "pete@otaqui.com", + "homepage": "https://github.com/pete-otaqui" + } + ], + "description": "Selenium2 (WebDriver) driver for Mink framework", + "homepage": "http://mink.behat.org/", + "keywords": [ + "ajax", + "browser", + "javascript", + "selenium", + "testing", + "webdriver" + ], + "time": "2016-03-05T09:10:18+00:00" + }, + { + "name": "behat/transliterator", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/Behat/Transliterator.git", + "reference": "826ce7e9c2a6664c0d1f381cbb38b1fb80a7ee2c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/Transliterator/zipball/826ce7e9c2a6664c0d1f381cbb38b1fb80a7ee2c", + "reference": "826ce7e9c2a6664c0d1f381cbb38b1fb80a7ee2c", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "chuyskywalker/rolling-curl": "^3.1", + "php-yaoi/php-yaoi": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "psr-0": { + "Behat\\Transliterator": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Artistic-1.0" + ], + "description": "String transliterator", + "keywords": [ + "i18n", + "slug", + "transliterator" + ], + "time": "2017-04-04T11:38:05+00:00" + }, + { + "name": "drupal/drupal-driver", + "version": "v1.2.1", + "source": { + "type": "git", + "url": "https://github.com/jhedstrom/DrupalDriver.git", + "reference": "125d39918c97f7a08e3110d456a0a1db864dae46" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jhedstrom/DrupalDriver/zipball/125d39918c97f7a08e3110d456a0a1db864dae46", + "reference": "125d39918c97f7a08e3110d456a0a1db864dae46", + "shasum": "" + }, + "require": { + "symfony/dependency-injection": "~2.6|~3.0", + "symfony/process": "~2.5|~3.0" + }, + "require-dev": { + "drupal/coder": "~8.2.0", + "drush-ops/behat-drush-endpoint": "*", + "mockery/mockery": "0.9.4", + "phpspec/phpspec": "~2.0", + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "psr-0": { + "Drupal\\Component": "src/", + "Drupal\\Driver": "src/", + "Drupal\\Tests\\Driver": "tests/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0+" + ], + "authors": [ + { + "name": "Jonathan Hedstrom", + "email": "jhedstrom@gmail.com" + } + ], + "description": "A collection of reusable Drupal drivers", + "homepage": "http://github.com/jhedstrom/DrupalDriver", + "keywords": [ + "drupal", + "test", + "web" + ], + "time": "2016-06-20T16:29:51+00:00" + }, + { + "name": "drupal/drupal-extension", + "version": "v3.2.2", + "source": { + "type": "git", + "url": "https://github.com/jhedstrom/drupalextension.git", + "reference": "abe3a33abd94382ab62423dd972fa820c63962e3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jhedstrom/drupalextension/zipball/abe3a33abd94382ab62423dd972fa820c63962e3", + "reference": "abe3a33abd94382ab62423dd972fa820c63962e3", + "shasum": "" + }, + "require": { + "behat/behat": "~3.1.0-rc2", + "behat/mink": "~1.5", + "behat/mink-extension": "~2.0", + "behat/mink-goutte-driver": "~1.0", + "behat/mink-selenium2-driver": "~1.1", + "drupal/drupal-driver": "~1.2", + "symfony/dependency-injection": "~2.7", + "symfony/event-dispatcher": "~2.7" + }, + "require-dev": { + "behat/mink-zombie-driver": "^1.2", + "phpspec/phpspec": "~2.0", + "phpunit/phpunit": "3.7.*" + }, + "type": "behat-extension", + "extra": { + "branch-alias": { + "dev-master": "3.2.x-dev" + } + }, + "autoload": { + "psr-0": { + "Drupal\\Drupal": "src/", + "Drupal\\Exception": "src/", + "Drupal\\DrupalExtension": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0+" + ], + "authors": [ + { + "name": "Jonathan Hedstrom", + "email": "jhedstrom@gmail.com" + } + ], + "description": "Drupal extension for Behat", + "homepage": "http://drupal.org/project/drupalextension", + "keywords": [ + "drupal", + "test", + "web" + ], + "time": "2016-06-30T21:12:18+00:00" + }, + { + "name": "fabpot/goutte", + "version": "v3.2.1", + "source": { + "type": "git", + "url": "https://github.com/FriendsOfPHP/Goutte.git", + "reference": "db5c28f4a010b4161d507d5304e28a7ebf211638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FriendsOfPHP/Goutte/zipball/db5c28f4a010b4161d507d5304e28a7ebf211638", + "reference": "db5c28f4a010b4161d507d5304e28a7ebf211638", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^6.0", + "php": ">=5.5.0", + "symfony/browser-kit": "~2.1|~3.0", + "symfony/css-selector": "~2.1|~3.0", + "symfony/dom-crawler": "~2.1|~3.0" + }, + "type": "application", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Goutte\\": "Goutte" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "A simple PHP Web Scraper", + "homepage": "https://github.com/FriendsOfPHP/Goutte", + "keywords": [ + "scraper" + ], + "time": "2017-01-03T13:21:43+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "6.2.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "8d6c6cc55186db87b7dc5009827429ba4e9dc006" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/8d6c6cc55186db87b7dc5009827429ba4e9dc006", + "reference": "8d6c6cc55186db87b7dc5009827429ba4e9dc006", + "shasum": "" + }, + "require": { + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.4", + "php": ">=5.5" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "^4.0", + "psr/log": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.2-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "time": "2017-02-28T22:50:30+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "v1.3.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "shasum": "" + }, + "require": { + "php": ">=5.5.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "time": "2016-12-20T10:07:11+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.4.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/f5b8a8512e2b58b0071a7280e39f14f72e05d87c", + "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "request", + "response", + "stream", + "uri", + "url" + ], + "time": "2017-03-20T17:10:46+00:00" + }, + { + "name": "instaclick/php-webdriver", + "version": "1.4.3", + "source": { + "type": "git", + "url": "https://github.com/instaclick/php-webdriver.git", + "reference": "0c20707dcf30a32728fd6bdeeab996c887fdb2fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/instaclick/php-webdriver/zipball/0c20707dcf30a32728fd6bdeeab996c887fdb2fb", + "reference": "0c20707dcf30a32728fd6bdeeab996c887fdb2fb", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": ">=5.3.2" + }, + "require-dev": { + "satooshi/php-coveralls": "dev-master" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "psr-0": { + "WebDriver": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Justin Bishop", + "email": "jubishop@gmail.com", + "role": "Developer" + }, + { + "name": "Anthon Pang", + "email": "apang@softwaredevelopment.ca", + "role": "Fork Maintainer" + } + ], + "description": "PHP WebDriver for Selenium 2", + "homepage": "http://instaclick.com/", + "keywords": [ + "browser", + "selenium", + "webdriver", + "webtest" + ], + "time": "2015-06-15T20:19:33+00:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "psr/log", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2016-10-10T12:19:37+00:00" + }, + { + "name": "symfony/browser-kit", + "version": "v3.2.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/browser-kit.git", + "reference": "9fab1ab6f77b77f3df5fc5250fc6956811699b57" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/9fab1ab6f77b77f3df5fc5250fc6956811699b57", + "reference": "9fab1ab6f77b77f3df5fc5250fc6956811699b57", + "shasum": "" + }, + "require": { + "php": ">=5.5.9", + "symfony/dom-crawler": "~2.8|~3.0" + }, + "require-dev": { + "symfony/css-selector": "~2.8|~3.0", + "symfony/process": "~2.8|~3.0" + }, + "suggest": { + "symfony/process": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\BrowserKit\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony BrowserKit Component", + "homepage": "https://symfony.com", + "time": "2017-04-12T14:13:17+00:00" + }, + { + "name": "symfony/class-loader", + "version": "v3.2.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/class-loader.git", + "reference": "fc4c04bfd17130a9dccfded9578353f311967da7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/class-loader/zipball/fc4c04bfd17130a9dccfded9578353f311967da7", + "reference": "fc4c04bfd17130a9dccfded9578353f311967da7", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "require-dev": { + "symfony/finder": "~2.8|~3.0", + "symfony/polyfill-apcu": "~1.1" + }, + "suggest": { + "symfony/polyfill-apcu": "For using ApcClassLoader on HHVM" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\ClassLoader\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony ClassLoader Component", + "homepage": "https://symfony.com", + "time": "2017-04-12T14:13:17+00:00" + }, + { + "name": "symfony/config", + "version": "v3.2.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "e5533fcc0b3dd377626153b2852707878f363728" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/e5533fcc0b3dd377626153b2852707878f363728", + "reference": "e5533fcc0b3dd377626153b2852707878f363728", + "shasum": "" + }, + "require": { + "php": ">=5.5.9", + "symfony/filesystem": "~2.8|~3.0" + }, + "require-dev": { + "symfony/yaml": "~3.0" + }, + "suggest": { + "symfony/yaml": "To use the yaml reference dumper" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Config Component", + "homepage": "https://symfony.com", + "time": "2017-04-12T14:13:17+00:00" + }, + { + "name": "symfony/console", + "version": "v3.2.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "a7a17e0c6c3c4d70a211f80782e4b90ddadeaa38" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/a7a17e0c6c3c4d70a211f80782e4b90ddadeaa38", + "reference": "a7a17e0c6c3c4d70a211f80782e4b90ddadeaa38", + "shasum": "" + }, + "require": { + "php": ">=5.5.9", + "symfony/debug": "~2.8|~3.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/event-dispatcher": "~2.8|~3.0", + "symfony/filesystem": "~2.8|~3.0", + "symfony/process": "~2.8|~3.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/filesystem": "", + "symfony/process": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Console Component", + "homepage": "https://symfony.com", + "time": "2017-04-26T01:39:17+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v3.2.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "02983c144038e697c959e6b06ef6666de759ccbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/02983c144038e697c959e6b06ef6666de759ccbc", + "reference": "02983c144038e697c959e6b06ef6666de759ccbc", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony CssSelector Component", + "homepage": "https://symfony.com", + "time": "2017-05-01T14:55:58+00:00" + }, + { + "name": "symfony/debug", + "version": "v3.2.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/debug.git", + "reference": "fd6eeee656a5a7b384d56f1072243fe1c0e81686" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/debug/zipball/fd6eeee656a5a7b384d56f1072243fe1c0e81686", + "reference": "fd6eeee656a5a7b384d56f1072243fe1c0e81686", + "shasum": "" + }, + "require": { + "php": ">=5.5.9", + "psr/log": "~1.0" + }, + "conflict": { + "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2" + }, + "require-dev": { + "symfony/class-loader": "~2.8|~3.0", + "symfony/http-kernel": "~2.8|~3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Debug\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Debug Component", + "homepage": "https://symfony.com", + "time": "2017-04-19T20:17:50+00:00" + }, + { + "name": "symfony/dependency-injection", + "version": "v2.8.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "e1c722dfe4dd04453aeb6b7a6deefb400c878394" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/e1c722dfe4dd04453aeb6b7a6deefb400c878394", + "reference": "e1c722dfe4dd04453aeb6b7a6deefb400c878394", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "conflict": { + "symfony/expression-language": "<2.6" + }, + "require-dev": { + "symfony/config": "~2.2|~3.0.0", + "symfony/expression-language": "~2.6|~3.0.0", + "symfony/yaml": "~2.3.42|~2.7.14|~2.8.7|~3.0.7" + }, + "suggest": { + "symfony/config": "", + "symfony/expression-language": "For using expressions in service container configuration", + "symfony/proxy-manager-bridge": "Generate service proxies to lazy load them", + "symfony/yaml": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\DependencyInjection\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony DependencyInjection Component", + "homepage": "https://symfony.com", + "time": "2017-04-26T01:38:53+00:00" + }, + { + "name": "symfony/dom-crawler", + "version": "v3.2.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/dom-crawler.git", + "reference": "f1ad34e8af09ed17570e027cf0c58a12eddec286" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/f1ad34e8af09ed17570e027cf0c58a12eddec286", + "reference": "f1ad34e8af09ed17570e027cf0c58a12eddec286", + "shasum": "" + }, + "require": { + "php": ">=5.5.9", + "symfony/polyfill-mbstring": "~1.0" + }, + "require-dev": { + "symfony/css-selector": "~2.8|~3.0" + }, + "suggest": { + "symfony/css-selector": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\DomCrawler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony DomCrawler Component", + "homepage": "https://symfony.com", + "time": "2017-04-12T14:13:17+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v2.8.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "7fc8e2b4118ff316550596357325dfd92a51f531" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/7fc8e2b4118ff316550596357325dfd92a51f531", + "reference": "7fc8e2b4118ff316550596357325dfd92a51f531", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "^2.0.5|~3.0.0", + "symfony/dependency-injection": "~2.6|~3.0.0", + "symfony/expression-language": "~2.6|~3.0.0", + "symfony/stopwatch": "~2.3|~3.0.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony EventDispatcher Component", + "homepage": "https://symfony.com", + "time": "2017-04-26T16:56:54+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v3.2.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "040651db13cf061827a460cc10f6e36a445c45b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/040651db13cf061827a460cc10f6e36a445c45b4", + "reference": "040651db13cf061827a460cc10f6e36a445c45b4", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Filesystem Component", + "homepage": "https://symfony.com", + "time": "2017-04-12T14:13:17+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/e79d363049d1c2128f133a2667e4f4190904f7f4", + "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "time": "2016-11-14T01:06:16+00:00" + }, + { + "name": "symfony/process", + "version": "v3.2.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "999c2cf5061e627e6cd551dc9ebf90dd1d11d9f0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/999c2cf5061e627e6cd551dc9ebf90dd1d11d9f0", + "reference": "999c2cf5061e627e6cd551dc9ebf90dd1d11d9f0", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Process Component", + "homepage": "https://symfony.com", + "time": "2017-04-12T14:13:17+00:00" + }, + { + "name": "symfony/translation", + "version": "v3.2.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "f4a04d2df710f81515df576b2de06bdeee518b83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/f4a04d2df710f81515df576b2de06bdeee518b83", + "reference": "f4a04d2df710f81515df576b2de06bdeee518b83", + "shasum": "" + }, + "require": { + "php": ">=5.5.9", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/config": "<2.8" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~2.8|~3.0", + "symfony/intl": "^2.8.18|^3.2.5", + "symfony/yaml": "~2.8|~3.0" + }, + "suggest": { + "psr/log": "To use logging capability in translator", + "symfony/config": "", + "symfony/yaml": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Translation Component", + "homepage": "https://symfony.com", + "time": "2017-04-12T14:13:17+00:00" + }, + { + "name": "symfony/yaml", + "version": "v3.2.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "acec26fcf7f3031e094e910b94b002fa53d4e4d6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/acec26fcf7f3031e094e910b94b002fa53d4e4d6", + "reference": "acec26fcf7f3031e094e910b94b002fa53d4e4d6", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "require-dev": { + "symfony/console": "~2.8|~3.0" + }, + "suggest": { + "symfony/console": "For validating YAML files using the lint command" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Yaml Component", + "homepage": "https://symfony.com", + "time": "2017-05-01T14:55:58+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} diff --git a/tests/devshop-tests.sh b/tests/devshop-tests.sh new file mode 100755 index 000000000..de42ee898 --- /dev/null +++ b/tests/devshop-tests.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +set -e + +# Run remaining tasks from install process. +echo ">> Running remaining tasks: drush @hostmaster hosting-tasks --fork=0 --strict=0" +drush @hostmaster hosting-tasks --fork=0 --strict=0 + +echo ">> Running remaining tasks: Complete!" + +if [[ $* == *--upgrade* ]]; then + echo ">> Triggering Upgrade: Running drush @hostmaster hostmaster-migrate $HOSTNAME $AEGIR_HOSTMASTER_ROOT_TARGET -y" + + # Force all tasks to appear as completed.' + drush @hostmaster sql-query "UPDATE hosting_task SET task_status = 1;" + + drush @hostmaster hostmaster-migrate $HOSTNAME $AEGIR_HOSTMASTER_ROOT_TARGET -y + + echo ">> Upgrade Complete." +fi + +# Pause the task queue. +drush @hostmaster vset hosting_queued_paused 1 + +# Run the test suite. +/usr/share/devshop/bin/devshop devmaster:test +#drush @hostmaster provision-test --behat-folder-path=profiles/devmaster/tests --test-type=behat + +# Unpause the task queue. +drush @hostmaster vset hosting_queued_paused 0 \ No newline at end of file diff --git a/tests/features/assets/pull-request-opened.json b/tests/features/assets/pull-request-opened.json new file mode 100644 index 000000000..fe74ec332 --- /dev/null +++ b/tests/features/assets/pull-request-opened.json @@ -0,0 +1,467 @@ +{ + "action": "opened", + "number": 18, + "pull_request": { + "url": "https://api.github.com/repos/opendevshop/drupal_docroot/pulls/18", + "id": 184068838, + "html_url": "https://github.com/opendevshop/drupal_docroot/pull/18", + "diff_url": "https://github.com/opendevshop/drupal_docroot/pull/18.diff", + "patch_url": "https://github.com/opendevshop/drupal_docroot/pull/18.patch", + "issue_url": "https://api.github.com/repos/opendevshop/drupal_docroot/issues/18", + "number": 18, + "state": "open", + "locked": false, + "title": "Update README.md", + "user": { + "login": "jonpugh", + "id": 106420, + "avatar_url": "https://avatars2.githubusercontent.com/u/106420?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/jonpugh", + "html_url": "https://github.com/jonpugh", + "followers_url": "https://api.github.com/users/jonpugh/followers", + "following_url": "https://api.github.com/users/jonpugh/following{/other_user}", + "gists_url": "https://api.github.com/users/jonpugh/gists{/gist_id}", + "starred_url": "https://api.github.com/users/jonpugh/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/jonpugh/subscriptions", + "organizations_url": "https://api.github.com/users/jonpugh/orgs", + "repos_url": "https://api.github.com/users/jonpugh/repos", + "events_url": "https://api.github.com/users/jonpugh/events{/privacy}", + "received_events_url": "https://api.github.com/users/jonpugh/received_events", + "type": "User", + "site_admin": false + }, + "body": "", + "created_at": "2018-04-25T15:07:45Z", + "updated_at": "2018-04-25T15:07:45Z", + "closed_at": null, + "merged_at": null, + "merge_commit_sha": null, + "assignee": null, + "assignees": [ + + ], + "requested_reviewers": [ + + ], + "requested_teams": [ + + ], + "labels": [ + + ], + "milestone": null, + "commits_url": "https://api.github.com/repos/opendevshop/drupal_docroot/pulls/18/commits", + "review_comments_url": "https://api.github.com/repos/opendevshop/drupal_docroot/pulls/18/comments", + "review_comment_url": "https://api.github.com/repos/opendevshop/drupal_docroot/pulls/comments{/number}", + "comments_url": "https://api.github.com/repos/opendevshop/drupal_docroot/issues/18/comments", + "statuses_url": "https://api.github.com/repos/opendevshop/drupal_docroot/statuses/aac2d4f28e23764dbc0106f6d8e448e083365cd6", + "head": { + "label": "thinkdrop:new-branch", + "ref": "new-branch", + "sha": "aac2d4f28e23764dbc0106f6d8e448e083365cd6", + "user": { + "login": "thinkdrop", + "id": 202621, + "avatar_url": "https://avatars2.githubusercontent.com/u/202621?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/thinkdrop", + "html_url": "https://github.com/thinkdrop", + "followers_url": "https://api.github.com/users/thinkdrop/followers", + "following_url": "https://api.github.com/users/thinkdrop/following{/other_user}", + "gists_url": "https://api.github.com/users/thinkdrop/gists{/gist_id}", + "starred_url": "https://api.github.com/users/thinkdrop/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/thinkdrop/subscriptions", + "organizations_url": "https://api.github.com/users/thinkdrop/orgs", + "repos_url": "https://api.github.com/users/thinkdrop/repos", + "events_url": "https://api.github.com/users/thinkdrop/events{/privacy}", + "received_events_url": "https://api.github.com/users/thinkdrop/received_events", + "type": "Organization", + "site_admin": false + }, + "repo": { + "id": 7487501, + "name": "drupal_docroot", + "full_name": "opendevshop/drupal_docroot.net", + "owner": { + "login": "opendevshop", + "id": 202621, + "avatar_url": "https://avatars2.githubusercontent.com/u/202621?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/opendevshop", + "html_url": "https://github.com/opendevshop", + "followers_url": "https://api.github.com/users/opendevshop/followers", + "following_url": "https://api.github.com/users/opendevshop/following{/other_user}", + "gists_url": "https://api.github.com/users/opendevshop/gists{/gist_id}", + "starred_url": "https://api.github.com/users/opendevshop/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/opendevshop/subscriptions", + "organizations_url": "https://api.github.com/users/opendevshop/orgs", + "repos_url": "https://api.github.com/users/opendevshop/repos", + "events_url": "https://api.github.com/users/opendevshop/events{/privacy}", + "received_events_url": "https://api.github.com/users/opendevshop/received_events", + "type": "Organization", + "site_admin": false + }, + "private": true, + "html_url": "https://github.com/opendevshop/drupal_docroot.net", + "description": "A repo with Drupal in the docroot.", + "fork": false, + "url": "https://api.github.com/repos/opendevshop/drupal_docroot", + "forks_url": "https://api.github.com/repos/opendevshop/drupal_docroot/forks", + "keys_url": "https://api.github.com/repos/opendevshop/drupal_docroot/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/opendevshop/drupal_docroot/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/opendevshop/drupal_docroot/teams", + "hooks_url": "https://api.github.com/repos/opendevshop/drupal_docroot/hooks", + "issue_events_url": "https://api.github.com/repos/opendevshop/drupal_docroot/issues/events{/number}", + "events_url": "https://api.github.com/repos/opendevshop/drupal_docroot/events", + "assignees_url": "https://api.github.com/repos/opendevshop/drupal_docroot/assignees{/user}", + "branches_url": "https://api.github.com/repos/opendevshop/drupal_docroot/branches{/branch}", + "tags_url": "https://api.github.com/repos/opendevshop/drupal_docroot/tags", + "blobs_url": "https://api.github.com/repos/opendevshop/drupal_docroot/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/opendevshop/drupal_docroot/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/opendevshop/drupal_docroot/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/opendevshop/drupal_docroot/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/opendevshop/drupal_docroot/statuses/{sha}", + "languages_url": "https://api.github.com/repos/opendevshop/drupal_docroot/languages", + "stargazers_url": "https://api.github.com/repos/opendevshop/drupal_docroot/stargazers", + "contributors_url": "https://api.github.com/repos/opendevshop/drupal_docroot/contributors", + "subscribers_url": "https://api.github.com/repos/opendevshop/drupal_docroot/subscribers", + "subscription_url": "https://api.github.com/repos/opendevshop/drupal_docroot/subscription", + "commits_url": "https://api.github.com/repos/opendevshop/drupal_docroot/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/opendevshop/drupal_docroot/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/opendevshop/drupal_docroot/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/opendevshop/drupal_docroot/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/opendevshop/drupal_docroot/contents/{+path}", + "compare_url": "https://api.github.com/repos/opendevshop/drupal_docroot/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/opendevshop/drupal_docroot/merges", + "archive_url": "https://api.github.com/repos/opendevshop/drupal_docroot/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/opendevshop/drupal_docroot/downloads", + "issues_url": "https://api.github.com/repos/opendevshop/drupal_docroot/issues{/number}", + "pulls_url": "https://api.github.com/repos/opendevshop/drupal_docroot/pulls{/number}", + "milestones_url": "https://api.github.com/repos/opendevshop/drupal_docroot/milestones{/number}", + "notifications_url": "https://api.github.com/repos/opendevshop/drupal_docroot/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/opendevshop/drupal_docroot/labels{/name}", + "releases_url": "https://api.github.com/repos/opendevshop/drupal_docroot/releases{/id}", + "deployments_url": "https://api.github.com/repos/opendevshop/drupal_docroot/deployments", + "created_at": "2013-01-07T18:33:00Z", + "updated_at": "2018-03-28T20:18:11Z", + "pushed_at": "2018-04-25T15:07:42Z", + "git_url": "git://github.com/opendevshop/drupal_docroot.git", + "ssh_url": "git@github.com:opendevshop/drupal_docroot.git", + "clone_url": "https://github.com/opendevshop/drupal_docroot.git", + "svn_url": "https://github.com/opendevshop/drupal_docroot", + "homepage": null, + "size": 56754, + "stargazers_count": 0, + "watchers_count": 0, + "language": "PHP", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "open_issues_count": 3, + "license": { + "key": "gpl-2.0", + "name": "GNU General Public License v2.0", + "spdx_id": "GPL-2.0", + "url": "https://api.github.com/licenses/gpl-2.0" + }, + "forks": 0, + "open_issues": 3, + "watchers": 0, + "default_branch": "8.x" + } + }, + "base": { + "label": "thinkdrop:8.x", + "ref": "8.x", + "sha": "10b1f4bf14cf38c675ad71a24ecba91aa3f8698e", + "user": { + "login": "thinkdrop", + "id": 202621, + "avatar_url": "https://avatars2.githubusercontent.com/u/202621?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/thinkdrop", + "html_url": "https://github.com/thinkdrop", + "followers_url": "https://api.github.com/users/thinkdrop/followers", + "following_url": "https://api.github.com/users/thinkdrop/following{/other_user}", + "gists_url": "https://api.github.com/users/thinkdrop/gists{/gist_id}", + "starred_url": "https://api.github.com/users/thinkdrop/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/thinkdrop/subscriptions", + "organizations_url": "https://api.github.com/users/thinkdrop/orgs", + "repos_url": "https://api.github.com/users/thinkdrop/repos", + "events_url": "https://api.github.com/users/thinkdrop/events{/privacy}", + "received_events_url": "https://api.github.com/users/thinkdrop/received_events", + "type": "Organization", + "site_admin": false + }, + "repo": { + "id": 7487501, + "name": "thinkdrop.net", + "full_name": "opendevshop/drupal_docroot", + "owner": { + "login": "thinkdrop", + "id": 202621, + "avatar_url": "https://avatars2.githubusercontent.com/u/202621?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/thinkdrop", + "html_url": "https://github.com/thinkdrop", + "followers_url": "https://api.github.com/users/thinkdrop/followers", + "following_url": "https://api.github.com/users/thinkdrop/following{/other_user}", + "gists_url": "https://api.github.com/users/thinkdrop/gists{/gist_id}", + "starred_url": "https://api.github.com/users/thinkdrop/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/thinkdrop/subscriptions", + "organizations_url": "https://api.github.com/users/thinkdrop/orgs", + "repos_url": "https://api.github.com/users/thinkdrop/repos", + "events_url": "https://api.github.com/users/thinkdrop/events{/privacy}", + "received_events_url": "https://api.github.com/users/thinkdrop/received_events", + "type": "Organization", + "site_admin": false + }, + "private": true, + "html_url": "https://github.com/opendevshop/drupal_docroot", + "description": "thinkdrop.net", + "fork": false, + "url": "https://api.github.com/repos/opendevshop/drupal_docroot", + "forks_url": "https://api.github.com/repos/opendevshop/drupal_docroot/forks", + "keys_url": "https://api.github.com/repos/opendevshop/drupal_docroot/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/opendevshop/drupal_docroot/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/opendevshop/drupal_docroot/teams", + "hooks_url": "https://api.github.com/repos/opendevshop/drupal_docroot/hooks", + "issue_events_url": "https://api.github.com/repos/opendevshop/drupal_docroot/issues/events{/number}", + "events_url": "https://api.github.com/repos/opendevshop/drupal_docroot/events", + "assignees_url": "https://api.github.com/repos/opendevshop/drupal_docroot/assignees{/user}", + "branches_url": "https://api.github.com/repos/opendevshop/drupal_docroot/branches{/branch}", + "tags_url": "https://api.github.com/repos/opendevshop/drupal_docroot/tags", + "blobs_url": "https://api.github.com/repos/opendevshop/drupal_docroot/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/opendevshop/drupal_docroot/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/opendevshop/drupal_docroot/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/opendevshop/drupal_docroot/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/opendevshop/drupal_docroot/statuses/{sha}", + "languages_url": "https://api.github.com/repos/opendevshop/drupal_docroot/languages", + "stargazers_url": "https://api.github.com/repos/opendevshop/drupal_docroot/stargazers", + "contributors_url": "https://api.github.com/repos/opendevshop/drupal_docroot/contributors", + "subscribers_url": "https://api.github.com/repos/opendevshop/drupal_docroot/subscribers", + "subscription_url": "https://api.github.com/repos/opendevshop/drupal_docroot/subscription", + "commits_url": "https://api.github.com/repos/opendevshop/drupal_docroot/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/opendevshop/drupal_docroot/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/opendevshop/drupal_docroot/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/opendevshop/drupal_docroot/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/opendevshop/drupal_docroot/contents/{+path}", + "compare_url": "https://api.github.com/repos/opendevshop/drupal_docroot/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/opendevshop/drupal_docroot/merges", + "archive_url": "https://api.github.com/repos/opendevshop/drupal_docroot/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/opendevshop/drupal_docroot/downloads", + "issues_url": "https://api.github.com/repos/opendevshop/drupal_docroot/issues{/number}", + "pulls_url": "https://api.github.com/repos/opendevshop/drupal_docroot/pulls{/number}", + "milestones_url": "https://api.github.com/repos/opendevshop/drupal_docroot/milestones{/number}", + "notifications_url": "https://api.github.com/repos/opendevshop/drupal_docroot/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/opendevshop/drupal_docroot/labels{/name}", + "releases_url": "https://api.github.com/repos/opendevshop/drupal_docroot/releases{/id}", + "deployments_url": "https://api.github.com/repos/opendevshop/drupal_docroot/deployments", + "created_at": "2013-01-07T18:33:00Z", + "updated_at": "2018-03-28T20:18:11Z", + "pushed_at": "2018-04-25T15:07:42Z", + "git_url": "git://github.com/opendevshop/drupal_docroot.git", + "ssh_url": "git@github.com:opendevshop/drupal_docroot.git", + "clone_url": "https://github.com/opendevshop/drupal_docroot.git", + "svn_url": "https://github.com/opendevshop/drupal_docroot", + "homepage": null, + "size": 56754, + "stargazers_count": 0, + "watchers_count": 0, + "language": "PHP", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "open_issues_count": 3, + "license": { + "key": "gpl-2.0", + "name": "GNU General Public License v2.0", + "spdx_id": "GPL-2.0", + "url": "https://api.github.com/licenses/gpl-2.0" + }, + "forks": 0, + "open_issues": 3, + "watchers": 0, + "default_branch": "8.x" + } + }, + "_links": { + "self": { + "href": "https://api.github.com/repos/opendevshop/drupal_docroot/pulls/18" + }, + "html": { + "href": "https://github.com/opendevshop/drupal_docroot/pull/18" + }, + "issue": { + "href": "https://api.github.com/repos/opendevshop/drupal_docroot/issues/18" + }, + "comments": { + "href": "https://api.github.com/repos/opendevshop/drupal_docroot/issues/18/comments" + }, + "review_comments": { + "href": "https://api.github.com/repos/opendevshop/drupal_docroot/pulls/18/comments" + }, + "review_comment": { + "href": "https://api.github.com/repos/opendevshop/drupal_docroot/pulls/comments{/number}" + }, + "commits": { + "href": "https://api.github.com/repos/opendevshop/drupal_docroot/pulls/18/commits" + }, + "statuses": { + "href": "https://api.github.com/repos/opendevshop/drupal_docroot/statuses/aac2d4f28e23764dbc0106f6d8e448e083365cd6" + } + }, + "author_association": "OWNER", + "merged": false, + "mergeable": null, + "rebaseable": null, + "mergeable_state": "unknown", + "merged_by": null, + "comments": 0, + "review_comments": 0, + "maintainer_can_modify": false, + "commits": 1, + "additions": 0, + "deletions": 4, + "changed_files": 1 + }, + "repository": { + "id": 7487501, + "name": "thinkdrop.net", + "full_name": "opendevshop/drupal_docroot", + "owner": { + "login": "thinkdrop", + "id": 202621, + "avatar_url": "https://avatars2.githubusercontent.com/u/202621?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/thinkdrop", + "html_url": "https://github.com/thinkdrop", + "followers_url": "https://api.github.com/users/thinkdrop/followers", + "following_url": "https://api.github.com/users/thinkdrop/following{/other_user}", + "gists_url": "https://api.github.com/users/thinkdrop/gists{/gist_id}", + "starred_url": "https://api.github.com/users/thinkdrop/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/thinkdrop/subscriptions", + "organizations_url": "https://api.github.com/users/thinkdrop/orgs", + "repos_url": "https://api.github.com/users/thinkdrop/repos", + "events_url": "https://api.github.com/users/thinkdrop/events{/privacy}", + "received_events_url": "https://api.github.com/users/thinkdrop/received_events", + "type": "Organization", + "site_admin": false + }, + "private": true, + "html_url": "https://github.com/opendevshop/drupal_docroot", + "description": "thinkdrop.net", + "fork": false, + "url": "https://api.github.com/repos/opendevshop/drupal_docroot", + "forks_url": "https://api.github.com/repos/opendevshop/drupal_docroot/forks", + "keys_url": "https://api.github.com/repos/opendevshop/drupal_docroot/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/opendevshop/drupal_docroot/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/opendevshop/drupal_docroot/teams", + "hooks_url": "https://api.github.com/repos/opendevshop/drupal_docroot/hooks", + "issue_events_url": "https://api.github.com/repos/opendevshop/drupal_docroot/issues/events{/number}", + "events_url": "https://api.github.com/repos/opendevshop/drupal_docroot/events", + "assignees_url": "https://api.github.com/repos/opendevshop/drupal_docroot/assignees{/user}", + "branches_url": "https://api.github.com/repos/opendevshop/drupal_docroot/branches{/branch}", + "tags_url": "https://api.github.com/repos/opendevshop/drupal_docroot/tags", + "blobs_url": "https://api.github.com/repos/opendevshop/drupal_docroot/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/opendevshop/drupal_docroot/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/opendevshop/drupal_docroot/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/opendevshop/drupal_docroot/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/opendevshop/drupal_docroot/statuses/{sha}", + "languages_url": "https://api.github.com/repos/opendevshop/drupal_docroot/languages", + "stargazers_url": "https://api.github.com/repos/opendevshop/drupal_docroot/stargazers", + "contributors_url": "https://api.github.com/repos/opendevshop/drupal_docroot/contributors", + "subscribers_url": "https://api.github.com/repos/opendevshop/drupal_docroot/subscribers", + "subscription_url": "https://api.github.com/repos/opendevshop/drupal_docroot/subscription", + "commits_url": "https://api.github.com/repos/opendevshop/drupal_docroot/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/opendevshop/drupal_docroot/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/opendevshop/drupal_docroot/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/opendevshop/drupal_docroot/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/opendevshop/drupal_docroot/contents/{+path}", + "compare_url": "https://api.github.com/repos/opendevshop/drupal_docroot/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/opendevshop/drupal_docroot/merges", + "archive_url": "https://api.github.com/repos/opendevshop/drupal_docroot/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/opendevshop/drupal_docroot/downloads", + "issues_url": "https://api.github.com/repos/opendevshop/drupal_docroot/issues{/number}", + "pulls_url": "https://api.github.com/repos/opendevshop/drupal_docroot/pulls{/number}", + "milestones_url": "https://api.github.com/repos/opendevshop/drupal_docroot/milestones{/number}", + "notifications_url": "https://api.github.com/repos/opendevshop/drupal_docroot/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/opendevshop/drupal_docroot/labels{/name}", + "releases_url": "https://api.github.com/repos/opendevshop/drupal_docroot/releases{/id}", + "deployments_url": "https://api.github.com/repos/opendevshop/drupal_docroot/deployments", + "created_at": "2013-01-07T18:33:00Z", + "updated_at": "2018-03-28T20:18:11Z", + "pushed_at": "2018-04-25T15:07:42Z", + "git_url": "git://github.com/opendevshop/drupal_docroot.git", + "ssh_url": "git@github.com:opendevshop/drupal_docroot.git", + "clone_url": "https://github.com/opendevshop/drupal_docroot.git", + "svn_url": "https://github.com/opendevshop/drupal_docroot", + "homepage": null, + "size": 56754, + "stargazers_count": 0, + "watchers_count": 0, + "language": "PHP", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "open_issues_count": 3, + "license": { + "key": "gpl-2.0", + "name": "GNU General Public License v2.0", + "spdx_id": "GPL-2.0", + "url": "https://api.github.com/licenses/gpl-2.0" + }, + "forks": 0, + "open_issues": 3, + "watchers": 0, + "default_branch": "8.x" + }, + "organization": { + "login": "thinkdrop", + "id": 202621, + "url": "https://api.github.com/orgs/thinkdrop", + "repos_url": "https://api.github.com/orgs/thinkdrop/repos", + "events_url": "https://api.github.com/orgs/thinkdrop/events", + "hooks_url": "https://api.github.com/orgs/thinkdrop/hooks", + "issues_url": "https://api.github.com/orgs/thinkdrop/issues", + "members_url": "https://api.github.com/orgs/thinkdrop/members{/member}", + "public_members_url": "https://api.github.com/orgs/thinkdrop/public_members{/member}", + "avatar_url": "https://avatars2.githubusercontent.com/u/202621?v=4", + "description": null + }, + "sender": { + "login": "jonpugh", + "id": 106420, + "avatar_url": "https://avatars2.githubusercontent.com/u/106420?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/jonpugh", + "html_url": "https://github.com/jonpugh", + "followers_url": "https://api.github.com/users/jonpugh/followers", + "following_url": "https://api.github.com/users/jonpugh/following{/other_user}", + "gists_url": "https://api.github.com/users/jonpugh/gists{/gist_id}", + "starred_url": "https://api.github.com/users/jonpugh/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/jonpugh/subscriptions", + "organizations_url": "https://api.github.com/users/jonpugh/orgs", + "repos_url": "https://api.github.com/users/jonpugh/repos", + "events_url": "https://api.github.com/users/jonpugh/events{/privacy}", + "received_events_url": "https://api.github.com/users/jonpugh/received_events", + "type": "User", + "site_admin": false + } +} \ No newline at end of file diff --git a/tests/features/bootstrap/FeatureContext.php b/tests/features/bootstrap/FeatureContext.php new file mode 100644 index 000000000..b9b9c0b1a --- /dev/null +++ b/tests/features/bootstrap/FeatureContext.php @@ -0,0 +1,232 @@ +getEnvironment(); + $this->minkContext = $environment->getContext('Drupal\DrupalExtension\Context\MinkContext'); + $this->drupalContext = $environment->getContext('Drupal\DrupalExtension\Context\DrupalContext'); + $this->drushContext = $environment->getContext('Drupal\DrupalExtension\Context\DrushContext'); + } + + /** + * Log output and watchdog logs after any failed step. + * @AfterStep + */ + public function logAfterFailedStep($event) + { + if ($event->getTestResult()->getResultCode() === \Behat\Testwork\Tester\Result\TestResult::FAILED) { + + $base_url = $this->getMinkParameter('base_url'); + $drush_config = $this->drupalContext->getDrupalParameter('drush'); + $alias = $drush_config['alias']; + + // Lookup file_directory_path + $cmd = "drush @$alias vget file_public_path --format=string"; + $files_path = trim(shell_exec($cmd)); + + // Check for various problems. + if (empty($files_path)) { + throw new \Exception("Results of command '$cmd' was empty. Check your settings and try again.'"); + } + elseif (!file_exists($files_path)) { + throw new \Exception("Unable to find directory at 'files_path' Drupal parameter '$files_path'"); + } + elseif (!is_writable($files_path)) { + throw new \Exception("Files path '$files_path' is not writable. Check permissions and try again."); + } + + $output_directory = $files_path .'/test_failures'; + $output_path = $files_path .'/test_failures/failure-' . time() . '.html'; + + // Print Current URL and Last reponse after any step failure. + echo "Step Failed. \n"; + echo "Site: $alias \n"; + echo "Current URL: " . $this->getSession()->getCurrentUrl() . "\n"; + + if (!file_exists($output_directory)) { + mkdir($output_directory); + } + $wrote = file_put_contents($output_path, $this->getSession()->getPage()->getContent()); + + + if (isset($_SERVER['TRAVIS'])) { + echo "\nLasts Response:\n"; + $this->minkContext->printLastResponse(); + } + else { + + if ($wrote) { + echo "Last Page Output: $base_url/$output_path \n"; + } + else { + throw new \Exception("Something failed when writing output to $base_url/$output_path ... \n"); + } + } + + echo "\nWatchdog Errors:\n"; + $this->drushContext->assertDrushCommand('wd-show'); + $this->drushContext->printLastDrushOutput(); + } + } + + /** + * Initializes context. + * + * Every scenario gets its own context instance. + * You can also pass arbitrary arguments to the + * context constructor through behat.yml. + */ + public function __construct() { + } + + /** + * @Then I wait :seconds seconds + */ + public function iWaitSeconds($seconds) + { + sleep($seconds); + } + + /** + * @Then save last response + */ + public function saveLastResponse() + { +// +// $path = '/var/aegir/devmaster-0.x/sites/devshop.site/files/test-output.html'; +// +// $file = file_save_data($this->getSession()->getPage()->getContent(), $path); +// +// $link = str_replace('/var/aegir/devmaster-0.x/sites/devshop.site/files/', 'http://devshop.site/sites/devshop.site/files/', $file); +// echo "Saved output to $link"; + } + + /** + * Creates a project. + * + * @Given I am viewing a project named :title with the git url :git_url + */ + public function createProject($title, $git_url) { + $node = (object) array( + 'title' => $title, + 'type' => 'project', + 'project' => (object) array( + 'git_url' => $git_url, + 'install_profile' => 'standard', + 'settings' => (object) array( + 'git' => array(), + ), + ), + ); + $saved = $this->nodeCreate($node); + + // Set internal page on the new node. + $this->getSession()->visit($this->locatePath('/node/' . $saved->nid)); + } + + /** + * @Then the field :field should have the value :value + */ + public function theFieldShouldHaveTheValue($field, $value) + { + $field = $this->fixStepArgument($field); + $value = $this->fixStepArgument($value); + + $field_object = $this->getSession()->getPage()->findField($field); + + if (null === $field_object) { + throw new \Exception('No field found with id|name|label|value ' . $field); + } + + if ($field_object->getAttribute('value') != $value) { + $current_value = $field_object->getAttribute('value'); + throw new \Exception("The field '$field' has the value '$current_value', not '$value'."); + } + } + + /** + * Returns fixed step argument (with \\" replaced back to "). + * + * A copy from MinkContext + * + * @param string $argument + * + * @return string + */ + protected function fixStepArgument($argument) + { + return str_replace('\\"', '"', $argument); + } + + /** + * @AfterSuite + */ + public static function deleteProjectCode(AfterSuiteScope $scope) + { + print "Deleting /var/aegir/projects/drpl8"; + print shell_exec('rm -rf /var/aegir/projects/drpl8'); + print shell_exec('rm -rf /var/aegir/config/server_master/apache/platform.d/platform_drpl8_*.conf'); + } + + /** + * + * @When I submit a pull-request + */ + public function iSubmitAPullRequest() + { + $url = $this->minkContext->getSession()->getPage()->findField('webhook-url')->getAttribute('value'); + $postdata = file_get_contents(dirname(dirname(__FILE__)) . '/assets/pull-request-opened.json'); + + if (empty($url)) { + throw new \Exception('Unable to find webhook URL.'); + } + + print "Found WebHook URL: $url"; + + // This one works, let's test for a URL first. + $client = $this->getSession()->getDriver('drush')->getClient(); + + /** @var Symfony\Component\DomCrawler\Crawler $response */ + $response = $client->request('POST', $url, [], [], [], $postdata); + + print_r($response->html()); + + + } +} diff --git a/tests/features/devshop.feature b/tests/features/devshop.feature new file mode 100644 index 000000000..49b2a78c1 --- /dev/null +++ b/tests/features/devshop.feature @@ -0,0 +1,44 @@ +Feature: Anonymous Homepage + + Scenario: The homepage works + + Given I am on the homepage + Then I should see "Sign In" + And I should see "Username" + And I should see "Password" + When I fill in "wrong" for "Username" + And I press "Log in" + Then I should see "Password field is required." + And I should see "Sorry, unrecognized username or password." + + When I click "Forgot your Password?" + Then I should see "Forgot your password?" + And I fill in "wrong" for "Username or e-mail address" + And I press "E-mail new password" + Then I should see "Sorry, wrong is not recognized as a user name or an e-mail address." + + @api + Scenario: The homepage works when devshop support is enabled. + Given I am on the homepage + Then I should see "Sign In" + And I should see "Username" + And I should see "Password" + And I should not see "Please sign in using one of the following options:" + And I should not see the link "Sign in with DevShop.Cloud" + + When I am logged in as a user with the "authenticated user" role + Then I should see "Your DevShop server is currently unsupported." + + When I run drush "vset devshop_support_license_key automated_testing_license_key" + Then print last drush output + When I run drush "vset devshop_support_license_key_status active" + Then print last drush output + + Then I am on the homepage + Then I should not see "Your DevShop server is currently unsupported." + Then I should see "License Status: Active" + + When I am logged in as a user with the "administrator" role + Given I am at "admin/devshop/support" + Then I should see "DevShop.Support" + And the "DevShop Support License Key" field should contain "automated_testing_license_key" \ No newline at end of file diff --git a/tests/features/environment.settings.feature b/tests/features/environment.settings.feature new file mode 100644 index 000000000..25d15706e --- /dev/null +++ b/tests/features/environment.settings.feature @@ -0,0 +1,37 @@ +#Feature: Create an environment +# +# @api +# Scenario: Get a project +# Given I am logged in as a user with the "administrator" role +# Given I am viewing a project named "demo" with the git url "http://github.com/jonpugh/drupal.git" +# Then I should see "Project Name" +# And I should see "demo" +# And I should see "http://github.com/jonpugh/drupal.git" +# +# Then break + + +# @TODO: Commented out until the drush user-create bug is fixed. +#Feature: Environment settings save. +# @TODO: Only works if you have a project named "drupal". Coming Soon. +# Scenario: Check environment settings. +# Given I am on the homepage +# And I fill in "jon" for "Username" +# And I fill in "jon" for "Password" +# Then I press "Log in" +# +# When I am at "project/drupal" +# Then I click "Environment Settings" +# When I fill in "NewUsername" for "Username" +# And I fill in "NewPassword" for "Password" +# Then I press "Save" +## Then I should see "Environment settings saved for dev in project drupal" +# +# When I click "Environment Settings" +# Then the field "Username" should have the value "NewUsername" +# When I fill in "" for "Username" +# And I fill in "" for "Password" +# Then I press "Save" +# +# When I click "Environment Settings" +# Then the field "Username" should have the value "" diff --git a/tests/features/project.create.feature b/tests/features/project.create.feature new file mode 100644 index 000000000..0986fa29a --- /dev/null +++ b/tests/features/project.create.feature @@ -0,0 +1,142 @@ +@api +Feature: Create a project and check settings + In order to start developing a drupal site + As a project admin + I need to create a new project + + Scenario: Create a new drupal 8 project + + Given I am logged in as a user with the "administrator" role + And I am on the homepage + When I click "Projects" + And I click "Start a new Project" + Then I should see "Step 1" + Then I fill in "drpl8" for "Project Code Name" + And I fill in "http://github.com/opendevshop/drupal_docroot.git" for "Git Repository URL" + When I press "Next" + + # Step 2 + Then I should see "drpl8" + And I should see "http://github.com/opendevshop/drupal_docroot.git" + Then I should see "Please wait while we connect and analyze your repository." + When I run drush "hosting-tasks --force --fork=0 --strict=0" + Then print last drush output + And I reload the page + + Then I fill in "docroot" for "Document Root" + When I press "Next" + And I should see "DOCUMENT ROOT docroot" + + When I run drush "hosting-tasks --force --fork=0 --strict=0" + Then print last drush output + When I run drush "hosting-tasks --force --fork=0 --strict=0" + Then print last drush output + And I reload the page + And I reload the page + + Then I should see "Create as many new environments as you would like." + When I fill in "dev" for "project[environments][NEW][name]" + And I select "master" from "project[environments][NEW][git_ref]" + + And I press "Add environment" + And I fill in "live" for "project[environments][NEW][name]" + And I select "master" from "project[environments][NEW][git_ref]" + And I press "Add environment" + Then I press "Next" + + # Step 4 + And I should see "dev" + And I should see "live" + And I should see "master" + + When I run drush "hosting-tasks --force --fork=0 --strict=0" + Then print last drush output + And I reload the page + + Then I should see "dev" + And I should see "live" + And I should see "master" + + And I should see "master" + And I reload the page +# When I click "Process Failed" + Then I should see "8." + Then I should not see "Platform verification failed" + When I select "standard" from "install_profile" + +# Then I break + + And I press "Create Project & Environments" + + # FINISH! + Then I should see "Your project has been created. Your sites are being installed." + And I should see "Dashboard" + And I should see "Settings" + And I should see "Logs" + And I should see "standard" +# And I should see "http://github.com/opendevshop/drupal" + And I should see the link "dev" + And I should see the link "live" + +# Then I break + And I should see the link "http://drpl8.dev.devshop.local.computer" + And I should see the link "Aegir Site" + + When I run drush "hosting-tasks --force --fork=0 --strict=0" + Then print last drush output + Then drush output should not contain "This task is already running, use --force" + + And I reload the page + Then I should see the link "dev" + Then I should see the link "live" +# Given I go to "http://dev.drpl8.devshop.travis" +# When I click "Visit Environment" + +# @TODO: Fix our site installation. +# Then I should see "No front page content has been created yet." + + When I click "Create New Environment" + And I fill in "testenv" for "Environment Name" + And I select "master" from "Branch or Tag" + And I select the radio button "Drupal Profile" + Then I select the radio button "Standard Install with commonly used features pre-configured." + + #@TODO: Check lots of settings + + When I fill in "testuser" for "Username" + And I fill in "testpassword" for "Password" + And I fill in "What's the password?" for "Message" + + Then I press "Create New Environment" + Then I should see "Environment testenv created in project drpl8." + + When I run drush "hosting-tasks --force --fork=0 --strict=0" + Then print last drush output + When I run drush "hosting-tasks --force --fork=0 --strict=0" + Then print last drush output + When I run drush "hosting-tasks --force --fork=0 --strict=0" + Then print last drush output + + When I click "testenv" in the "main" region + Then I should see "Environment Dashboard" + And I should see "Environment Settings" + + And I click "Environment Settings" + Then the field "Username" should have the value "testuser" + Then the field "Password" should have the value "testpassword" + Then the field "Message" should have the value "What's the password?" + + When I click "Project Settings" + Then I select "testenv" from "Primary Environment" + And I press "Save" + + Then I should see "DevShop Project drpl8 has been updated." + And I should see an ".environment-link .fa-bolt" element + + # When I click "Visit Site" + Given I am on "http://drpl8.testenv.devshop.local.computer" +# TODO: Figure out how to test this in travis! +# Then the response status code should be 401 + + Given I am on "http://testuser:testpassword@drpl8.testenv.devshop.local.computer" + Then I should see "Welcome to drpl8.testenv" diff --git a/tests/features/project.docroot.feature b/tests/features/project.docroot.feature new file mode 100644 index 000000000..b401b0e47 --- /dev/null +++ b/tests/features/project.docroot.feature @@ -0,0 +1,86 @@ +@api +Feature: Create a project with Drupal in the docroot. + + In order to start developing a drupal site + As a project admin + I need to create a new project + + Scenario: Create a new drupal 8 project with drupal in docroot folder + + When I run drush "en dblog -y" + Given I am logged in as a user with the "administrator" role + And I am on the homepage + When I click "Projects" + And I click "Start a new Project" + Then I should see "Step 1" + Then I fill in "rootproject" for "Project Code Name" + And I fill in "http://github.com/opendevshop/drupal_docroot" for "Git Repository URL" + When I press "Next" + + # Step 2 + Then I should see "rootproject" + And I should not see "The name rootproject is in use. Please try again." + And I should see "http://github.com/opendevshop/drupal_docroot" + + When I run drush "hosting-tasks --force --fork=0 --strict=0" + Then print last drush output + And I reload the page + + When I fill in "docroot" for "Document Root" + + # Step 3 + When I press "Next" + # Users no longer see this, we wait for verify before showing step 2. +# Then I should see "Please wait while we connect to your repository and determine any branches." + And I should see "Document Root" + And I should see "rootproject" + + When I run drush "hosting-tasks --force --fork=0 --strict=0" + Then print last drush output + And I reload the page + And I reload the page + + Then I should see "Create as many new environments as you would like." + When I fill in "dev" for "project[environments][NEW][name]" + And I select "master" from "project[environments][NEW][git_ref]" + + Then I press "Next" + # Step 4 + And I should see "dev" + And I should see "master" + + When I run drush "hosting-tasks --force --fork=0 --strict=0" + Then print last drush output + And I reload the page + + Then I should see "dev" + And I should see "master" + And I reload the page +# When I click "Process Failed" + Then I should see "8." + Then I should not see "Platform verification failed" + When I select "standard" from "install_profile" + And I press "Create Project & Environments" + + # FINISH! + Then print current URL + When I run drush "wd-show" + Then print last drush output + + Then I should see "Your project has been created. Your sites are being installed." + And I should see "Dashboard" + And I should see "Settings" + And I should see "Logs" + And I should see "standard" +# And I should see "http://github.com/opendevshop/drupal" + And I should see the link "dev" + + When I run drush "hosting-tasks --force --fork=0 --strict=0" + Then print last drush output + Then drush output should not contain "This task is already running, use --force" + + And I reload the page + Then I should see the link "dev" + +# When I click "Visit Site" +# Then I should see "Welcome to" diff --git a/tests/features/project.list.feature b/tests/features/project.list.feature new file mode 100644 index 000000000..b6bb850a1 --- /dev/null +++ b/tests/features/project.list.feature @@ -0,0 +1,11 @@ +# @TODO: Commented out until the drush user-create bug is fixed. +#@api +Feature: List all projects +# In order to view all of the projects +# As a front-end user +# I need to visit the projects page. +# +# Scenario: Project list home page +# Given I am logged in as a user with the "administrator" role +# When I click "Projects" +# Then I should see "All Projects" \ No newline at end of file diff --git a/tests/features/project.settings.feature b/tests/features/project.settings.feature new file mode 100644 index 000000000..e69de29bb diff --git a/tests/features/pull_requests.feature b/tests/features/pull_requests.feature new file mode 100644 index 000000000..ba065fc88 --- /dev/null +++ b/tests/features/pull_requests.feature @@ -0,0 +1,73 @@ +#@api +#Feature: Pull Request Environments +# In order to review a pull request +# As a project admin +# I need to have a copy of the site running on the code from the PR +# +# @api +# Scenario: Create a new drupal 8 project +# +# Given I am logged in as a user with the "administrator" role +# And I am on the homepage +# When I click "Projects" +# And I click "Start a new Project" +# Then I should see "Step 1" +# Then I fill in "prproject" for "Project Code Name" +# And I fill in "http://github.com/opendevshop/drupal_docroot.git" for "Git URL" +# When I press "Next" +# +# # Step 2 +# Then I should see "prproject" +# And I should see "http://github.com/opendevshop/drupal_docroot.git" +# When I fill in "docroot" for "Path to Drupal" +# And I check the box "Create Environments for Pull Requests" +# And I check the box "Delete Pull Request Environments" +# And I check the box "Reinstall Pull Request Environments on every git push." +# +# # Step 3 +# When I press "Next" +# Then I should see "Please wait while we connect to your repository and determine any branches." +## And I should see "Path to Drupal: docroot" +# +# When I run drush "hosting-tasks --force --fork=0 --strict=0" +# Then print last drush output +# And I reload the page +# And I reload the page +# +# Then I should see "Create as many new environments as you would like." +# When I fill in "mirror" for "project[environments][NEW][name]" +# And I select "master" from "project[environments][NEW][git_ref]" +# Then I press "Next" +# +# # Step 4 +# When I run drush "hosting-tasks --force --fork=0 --strict=0" +# Then print last drush output +# And I reload the page +# +# And I reload the page +## When I click "Process Failed" +# Then I should see "8." +# Then I should not see "Platform verification failed" +# When I select "standard" from "install_profile" +# +## Then I break +# +# And I press "Create Project & Environments" +# +# # FINISH! +# Then I should see "Your project has been created. Your sites are being installed." +# +# When I run drush "hosting-tasks --force --fork=0 --strict=0" +# Then print last drush output +# Then drush output should not contain "This task is already running, use --force" +# +# Then I click "Project Settings" +# And I select "mirror" from "Pull Request Environment Creation Method" +# +# Then I press "Save" +# Then I should see "DevShop Project drupal has been updated." +# +# +# +# # @TODO: Deliver demo PR payloads +## When I submit a pull-request diff --git a/tests/features/servers.feature b/tests/features/servers.feature new file mode 100644 index 000000000..688199411 --- /dev/null +++ b/tests/features/servers.feature @@ -0,0 +1,27 @@ +Feature: DevShop Servers have LetsEncrypt enabled out of the box. + In order to have a safe and secure website + As devshop user + I need to enable HTTPS as easily as possible + + @api + Scenario: Server node has Certificate and HTTPS services enabled. + + Given I am logged in as a user with the "administrator" role + When I am at "hosting/c/server_master" + And I click "Edit" + Then I select the radio button with the label "LetsEncrypt" + Then I select the radio button with the label "Staging" +# Then I select the radio button with the label "Apache HTTPS" + And I press "Save" + When I run drush "hosting-tasks --force --fork=0 --strict=0" + Then print last drush output + And I click "Servers" + And I should see "Certificate" + And I should see "LetsEncrypt" +# And I should see "Apache HTTPS" + + @api + Scenario: Server node has Certificate and HTTPS services enabled. + Given I am logged in as a user with the "administrator" role + When I am at "hosting/c/hostmaster" + Then I should see "Encryption Disabled" \ No newline at end of file diff --git a/tests/features/support.feature b/tests/features/support.feature new file mode 100644 index 000000000..460de550c --- /dev/null +++ b/tests/features/support.feature @@ -0,0 +1,18 @@ +Feature: Anonymous Homepage + + Scenario: The homepage works + + Given I am on the homepage + Then I should see "Login" + And I should see "Username" + And I should see "Password" + When I fill in "wrong" for "Username" + And I press "Log in" + Then I should see "Password field is required." + And I should see "Sorry, unrecognized username or password." + + When I click "Forgot your Password?" + Then I should see "Forgot your password?" + And I fill in "wrong" for "Username or e-mail address" + And I press "E-mail new password" + Then I should see "Sorry, wrong is not recognized as a user name or an e-mail address." \ No newline at end of file diff --git a/themes/boots/boots.css b/themes/boots/boots.css new file mode 100644 index 000000000..bbfba0e7e --- /dev/null +++ b/themes/boots/boots.css @@ -0,0 +1,1338 @@ +html, +body { + height: 100%; +} + +body.admin-menu .navbar-static-top { + margin-top: 20px !important; + z-index: 3; +} + +.environment-wrapper { + min-width: 330px; +} + +.environment .list-group-item { + border: none; +} + +.environment .list-group-item:first-child, +.environment .list-group-item:last-child { + border-top-left-radius: 0px; + border-top-right-radius: 0px; + border-bottom-left-radius: 0px; + border-bottom-right-radius: 0px; +} + +.environment .list-group-item .content { + font-size: .7em; + text-transform: uppercase; + color: grey; +} + +.environment-main { + border-left: 1px #ededed solid; + border-right: 1px #ededed solid; + border-collapse: collapse; +} +.environment-messages .list-group-item-info { + background-color: whitesmoke; + color: #777; + + border-bottom: 1px solid whitesmoke; + border-top: 1px solid whitesmoke; + border-collapse: collapse; +} +.environment-messages .list-group-item-default { + color: #777; + border-bottom: 1px solid whitesmoke; + border-top: 1px solid whitesmoke; + border-collapse: collapse; +} + +.environment-messages .list-group-item { + padding: 0px; + + border-bottom: 1px solid whitesmoke; + border-top: 1px solid whitesmoke; + border-collapse: collapse; +} +.environment-messages .list-group-item .text { + padding: 5px 10px; + +} + +.environment.disabled .list-group-item-disabled { + background-color: #e5e5e5 !important; +} +.environment.disabled .list-group-item-info { + background-color: whitesmoke !important; +} + +.table-controls { + border: 1px solid #ddd; + margin: 0px; +} + +.btn-env-buttons { + min-width: 92px; +} +#navbar .task-list.btn-group { + padding: .5em; +} +#navbar .active-task { + color: #ffffaa; +} + +.environment-git-ref { + max-width: 77%; + overflow: hidden; + text-overflow: ellipsis; +} + +.environment-meta-data { + margin: 0px; + padding: 3px 6px; + color: #777; + font-size: .7em; + white-space: nowrap; +} + +.environment.live-environment .environment-meta-data { + color: #fff; +} + +.environment-indicators { + display: inline; +} +.environment-meta-data i.fa { + margin-right: .5em; +} + +.environment-meta-data.environment-info i.fa { + margin-right: 0; +} +.environment-meta-data.environment-info { + text-transform: uppercase; + z-index: 1; + position: relative; + display: inline-block; + vertical-align: middle; + margin-right: -5px; +} + +.environment-status { + display: inline-block; + white-space: nowrap; +} + +.environment-link { + font-weight: bold; + font-size: 1.1em; +} + +.environment-link:hover { + text-decoration: none; +} + +.environment-link:hover span { + text-decoration: underline; +} + + +.environment-link strong { + font-style: inherit; + color: inherit; + text-transform: inherit; +} + +.environment-header { + text-overflow: ellipsis; + overflow: hidden; + padding-right: 30px; + border-radius: 5px 5px 0 0; +} + +.environment-header.list-group-item-disabled { + border: 1px #ededed solid; + background: whitesmoke; +} + +.environment-menu button.environment-menu-button { + z-index: 1; + padding: .3em; +} + +.environment-dropdowns { + position: absolute; + right: 18px; + top: 7px; +} + +.environment-domains { + position: relative; + padding: 0px; + white-space: nowrap; +} + +.environment-domains .url-wrapper .btn{ + padding: 3px; +} + +.environment-domains.login-available { + padding-right: 4em; +} + +.environment.live-environment .environment-header { + background: #4E6DDB; + } + +.environment.live-environment .environment-header a, +.environment.live-environment .environment-header a.btn-text, +.environment.live-environment .environment-header button.btn-link, +.environment.live-environment .environment-header span, +.environment.live-environment .environment-header button, +.environment.live-environment .environment-menu, +.environment.live-environment .environment-menu a, +.environment.live-environment .environment-menu a.btn-text, +.environment.live-environment .environment-menu button.btn-link +{ + color: white; +} + +.environment.live-environment .environment-menu .dropdown-menu a, +.environment.live-environment .environment-menu .dropdown-menu { + color: black; +} + + +.environment.live-environment .environment-menu button.btn-link:hover { + text-decoration: none; +} + +#project-info { + padding-bottom: .5em; +} + +.add-project-button .btn { + height: 9.5em; + padding: 3em 0em; + background: white; + color: #4cae4c; + width: 100%; +} +.add-project-button .btn:hover, +.add-project-button .btn:active { + background: inherit; + color: inherit; + + color: white; + background: #4cae4c; +} + +.add-project-button .fa { + font-size: 2em; + margin-bottom: .25em; +} + +.navbar-nav { + margin: 0; +} +.navbar-nav li { + text-align: right; +} + +ul.secondary { + float: right; +} + +.navbar-project > .container-fluid { + padding-left: .5em; + padding-right: .5em; +} +.main-project-nav { + margin: .5em 0; +} + +/*.main-project-nav .dropdown a.active {*/ + /*background: #333;*/ + /*color: white;*/ +/*}*/ + +.navbar-project .nav-pills a.active { + background: whitesmoke; + border: 1px solid whitesmoke; +} + +.navbar-project .main-project-nav > ul.nav-pills > li > a.active { + background: white; + +} + + + +.project-stuff { + position: absolute; + right: 0px; + top: -45px; +} + +.project-stuff a { + color: #777; +} + +/*.environment .tasks-button {*/ + /*position: absolute;*/ + /*top: 1px;*/ + /*right: 15px;*/ + /*z-index: 2;*/ +/*}*/ + + +/*.environment .tasks-button > a {*/ + /*padding: 10px 5px;*/ + /*height: 3.2em;*/ +/*}*/ + +/*.environment .tasks-button > a:hover,*/ +/*.environment .tasks-button.open > a {*/ + /*background: #428bca;*/ + /*color: white;*/ + /*text-decoration: none;*/ + /*border-radius: 4px;*/ +/*}*/ + +/*.environment.live-environment .tasks-button > a {*/ + /*color: white;*/ +/*}*/ + +/*.environment.live-environment .tasks-button > a:hover,*/ +/*.environment.live-environment .tasks-button.open > a {*/ + /*text-decoration: underline;*/ +/*}*/ + +.environment-main > .list-group-item.btn-group { + padding: 0px; +} +.environment-main > .list-group-item.btn-group a.btn { + margin: 0px 3px; + background: transparent !important; +} + +.environment-main > .list-group-item.btn-group .btn, +.environment-main > .list-group-item.btn-group > .btn-group { + font-size: 1.0em; + color: #777; + border: none !important; + border-radius: 0px; + background: transparent !important; +} + + +.environment-main > .list-group-item.btn-group > a:hover { + color: #111 !important; + background-color: whitesmoke; +} + +.environment-domains button { + display: inline-block; +} + + +.environment-domains.btn-group-justified>.btn-group .btn { + width: auto; +} + +.environment-domains .btn-group-smaller > a, +.environment-domains .btn-group-smaller > button { + margin: 0px 5px; + padding: 3px; + color: #777; + font-size: .8em !important; +} +.environment-domains a:hover, +.environment-domains button:hover, +.environment-meta-data:hover, +.environment-meta-data button:hover { + text-decoration: underline; +} + +.url-wrapper { + text-overflow:ellipsis; + overflow: hidden; + display: inline; + max-width: 75%; +} + +.environment .last-commit { + display: block; + color: #777; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + + +.environment .last-commit span { + font-style: italic; + font-size: .8em; + color: #999999; +} + + + +.btn.settings { + margin: -5px 0px; + border:none; +} + +.dropdown-menu>li>a { + white-space: normal; +} + +input.form-control.webhook-url { + display: inline; + width: 36em; + +} + +@media (max-width: 640px) { + body.admin-menu { + margin-top: 40px !important; + } +} +.environment-deploy, +.environment-tests { + padding: 0px; +} + +.environment-deploy.list-group-item { + white-space: nowrap; +} + +.dropdown-menu li label, +.environment label +/* Keeping... not sure if this affects other UI elements. +.well label, +.well dt + */ +{ + font-size: .7em; + text-transform: uppercase; + color: grey; + font-weight: bold; + margin-right: 10px; + margin-left: 10px; +} + + +.task-arguments.well { + min-height: inherit; + margin-bottom: 0px; +} +.task-arguments.well dl { + margin-bottom: 0px; +} +.task-arguments.well .dl-horizontal dt { + width: 120px; + padding-top: .5em; + padding-right: 2em; + margin-bottom: 0px; +} + +.task-arguments.well .dl-horizontal dd { + margin-left: 120px; +} + +.environment-tests.list-group-item > label { + font-size: .7em; + color: grey; + text-transform: uppercase; + font-weight: bold; + margin-right: 10px; + margin-left: 10px; +} + +.environment-tests button, +.environment-tests a { + color: #777; + background: none; + border: none; + font-size: .9em; +} + +.btn-deploy-code li a { + white-space: nowrap; +} + +.btn-deploy-code li.create-git-ref a { + float: none; +} + +.alert-spacing { + margin-top: 15px; +} + +.deploy-db-indicator { + position: absolute; + top: 8px; + left: 8px; +} + +.btn-deploy-database a { + position: relative; +} + +.devshop-tasks { + min-width: 340px; + z-index:20; + max-width: 27em; +} + +.environment-menu .devshop-tasks .tasks, +.environment-menu .devshop-tasks .actions { + float: left; + width: 50%; +} + +.devshop-tasks.dropdown-menu { + padding-bottom: 0px; + top: 91%; +} +.devshop-tasks .actions .list-group, +.devshop-tasks .tasks .list-group +{ + margin-bottom: 0px; +} +.devshop-tasks .actions .list-group-item:first-child, +.devshop-tasks .tasks .list-group-item:first-child +{ + border-top-left-radius: 0px; + border-top-right-radius: 0px; + margin-top: -10px; +} +.devshop-tasks .actions .list-group-item:last-child, +.devshop-tasks .tasks .list-group-item:last-child +{ + border-bottom-left-radius: 0px; + border-bottom-right-radius: 0px; + border-bottom: none; +} + +.task-list.btn-group.open button { + background: white; + border: 1px solid rgba(0,0,0,.15); + border-radius: 4px !important; + -webkit-box-shadow: 0 6px 12px rgba(0,0,0,.175); + box-shadow: 0 6px 12px rgba(0,0,0,.175); +} + +.task-list.btn-group.open button i { + color: #777; +} + +#navbar .task-list.btn-group.open button, +#navbar .task-list.btn-group.open button .active-task { + color: #777; +} + +.devshop-stack li.inline { + position: relative; +} +.devshop-stack li.inline a { + + display: inline-block; + height: 3.5em; +} + +.devshop-stack li.inline a:first-child { + width: 80%; +} +.devshop-stack li.inline a:last-child { + width: 15%; + padding: 0px; + position: absolute; + top: 0px; +} + +.devshop-stack li.inline a:last-child i.fa { + position: absolute; + top: 25%; + left: 31%; + font-size: 1.5em; +} + +.modal-results.modal-lg { + min-width: 900px; + width: auto; + max-width: 90%; +} + +/* Bootstrap breaks user/permissions table */ +th.checkbox, td.checkbox { + display: table-cell; +} + +/* shows hidden task logs **/ +.hosting-task-full { + display: inherit; +} + +.hosting-task-summary { + display: none; +} + + +.padded-top { + padding-top: 1em; +} + +ul.dropdown-menu li.text { + padding: .25em 2em; + font-size: .8em; +} + +.node-type-site .environment { + position: relative; +} + +.node-type-site .environment-dropdowns { + right: 0px; +} + +.environment.pull-request img.environment-avatar { + position: absolute; + left: 10px; + top: 10px; +} +.environment.pull-request .environment-header { + padding-left: 48px !important; +} + +.environment.pull-request .environment-header a.pull-request i.fa-github { + font-size: 1.5em; + vertical-align: middle; + white-space: nowrap; + text-overflow: ellipsis; +} + +.environment-main > .list-group-item, +.environment-task-logs.list-group-item { + padding: 0px; + clear: both; +} + +.environment-main > .list-group-item pre { + white-space: pre; +} + +.environment-main > .environment-header.list-group-item { + white-space: normal; +} + +.environment-main > .list-group-item.text, +.environment-menu > ul.dropdown-menu li .text +{ + padding: 10px 15px; + white-space: normal; +} + +.environment-main > .list-group-item.text .btn-group { + margin-top: -10px; + margin-right: -15px; +} + +.environment-main > .list-group-item.environment-header { + padding: 10px 80px 10px 10px; +} + +.environment-main > .list-group-item .btn-group > .btn, +.environment-main > .list-group-item .btn-group > button +{ + font-size: .8em; + text-transform: uppercase; + font-weight: normal; + border: none !important; + border-radius: 0px; + background: transparent !important; + color: gray; + /*padding-left: 0.75em;*/ + /*padding-right: 0.75em;*/ + +} + +.environment-main .list-group-item .btn-group > button.btn-danger { + color: #a94442; +} +.environment-main .list-group-item .btn-group > button.btn-warning { + color: #f0ad4e; +} +.environment-main .list-group-item .btn-group > button.btn-info { + color: #428bca; +} +.environment-main .list-group-item .btn-group > button.btn-success { + color: #40a823; +} + +.environment-main .list-group-item-git > .dropdown-menu { + max-width: 350px; +} +.environment-main .list-group-item .btn-group > a.alert-link { + color: inherit; + font-size: inherit; +} +.environment-main .list-group-item > .btn-group > button:hover { + color: #333; +} + +.github-button { + font-size: 1.5em; +} + +#hosting-site-info, #hosting-task-list { + float: left; + margin: 0 30px 15px 0; + width: 25%; +} + +form > div > fieldset { + /*padding: 20px 0px;*/ +} +form > div > fieldset > legend { + /*padding: 20px 0px 0px;*/ + +} + +html.js fieldset.collapsed { + height: 2em; +} + +.btn-urls { + max-width: 63%; + text-overflow: ellipsis; + overflow: hidden; +} + + +.btn-urls-single { + max-width: 75%; + text-overflow: ellipsis; + overflow: hidden; +} + +.environment-menu .dropdown-menu a { + white-space: nowrap; +} + +.environment-task-logs .text { + text-transform: uppercase; + font-size: .8em; +} +.environment-tasks-alert { + position:relative; +} +.environment-tasks-alert > .text { + z-index: 2; +} +.environment-tasks-alert .alert-link { + display: block; +} +.node-type-site .environment-tasks-alert .time, +.dropdown-menu .environment-tasks-alert .time { + float:right; +} +.environment-task-logs .btn-logs { + z-index: 3; +} + +.environment .progress { + border-radius: 0px; + border: none; + margin: 0; + padding: 0px; + background: transparent; + box-shadow: none; + clear: both; + position: absolute; + bottom: 0px; + width: 100%; + left: 0px; + z-index: 1; + height: 1.8em; + display: none; +} + +.task-text { + padding: 0; + text-transform: uppercase; + font-size: .8em; + z-index: 2; + position: relative; + white-space: nowrap; +} + +.task-text a { + padding: 5px 10px; +} + +.environment .progress-bar { + float: none; + background-color: #bbb;; + width: 100%; +} +.environment-tasks-alert.alert-processing .progress { + display: block; + width: 100%; +} + +.btn-logs > button { + color: grey; + padding: 3px 8px; + margin: 0px; + background-color: transparent !important; + border: none; + border-radius: initial; +} +.btn-logs > button:focus, +.btn-logs > button:hover +{ + background-color: transparent !important; +} + +.btn-logs > .dropdown-menu { + padding: 0px; + border: none; + -webkit-border-radius: none; + -moz-border-radius: none; + border-radius: none; +} + +.btn-logs .list-group-item { + white-space: nowrap; + min-width: 22em; +} + +.environment-task-logs > .alert-default { + background-color: transparent; + position: relative; + z-index: 2; +} + +.environment-menu i.fa { + width: 20px; +} + +.environment-menu .dropdown-menu .divider { + margin: 3px 0px; +} + +.nav-pills.environment-tasks-nav a { + color: #333; +} + +form.retry.btn.btn-default { + padding: 0; +} +form.retry.btn.btn-default > div > button.btn-default { + margin: 0; + border: none; +} + +.hosting-task-retry, .hosting-task-retry div, .hosting-task-retry form { + display: block; +} +div.hosting-task-retry form, div.hosting-task-retry form input { + padding: 0.25em 0.5em !important; +} + +.alert-status { + background: #ccc;color: #444; +} + +.dropdown-settings.dropdown-menu { + width: 255px; + padding: 0px; +} + +.alert-queued { + background: #ddd; +} + +.alert-queued .alert-link { + color: #777; +} +.alert-processing .alert-link { + color: #333; +} + +.processing { + z-index: 1; +} +/*.environment-task-logs > div {*/ + /*position: relative;*/ + /*z-index: 2;*/ +/*}*/ + +.project-environments a.alert-queued, +.devshop-tasks a.list-group-item-queued { + color: #777; +} +.devshop-tasks a.list-group-item-queued { + color: #777; + background-color: #ddd; +} +.devshop-tasks a.list-group-item-queued:hover { + color: #333; + background-color: #dedede; +} +.project-environments a.alert-processing, +.devshop-tasks a.list-group-item-processing { + color: #333; + background-color: #ddd; +} +.project-environments a.alert-processing:hover, +.devshop-tasks a.list-group-item-processing:hover { + color: #000; + background-color: #ccc; +} + +.refresh-link { + font-size: 0.8em; + color: #444; + white-space: nowrap; +} + +#project-add-status { + margin-top: 15px; + overflow-x: hidden; +} + +#project-add-status.panel label +{ + font-size: .7em; + text-transform: uppercase; + color: grey; + font-weight: bold; + margin-right: 10px; + margin-left: 0px; +} + +.panel.devshop-command .panel-heading { + font-family: Menlo,Monaco,Consolas,"Courier New",monospace; +} +.panel.devshop-command pre { + margin: 0; + border: none; + border-radius: 0; + background: none; +} + +.environment-dothooks pre { + margin: 0 1em 1em; + font-size: .8em; + overflow-x: scroll; + word-wrap: normal; +} + + +.task-details { + margin-top: 3em; +} + +.running-indicator { + display: none; +} + +.running-indicator.active { + display: block; +} +.task-badge { + margin-right: 5px; +} +.task-arg { + padding-right: 2em; + white-space: nowrap; +} + +.task-arguments-list { + display: inline-block; + +} + +.ansi_color_bg_black { + background: #262626; +} + +#hosting-task-confirm-form .form-checkboxes .form-item span { + width: 6em; + display: inline-block; + font-size: 0.8em; + text-transform: uppercase; + text-align: right; + padding-right: 1em; +} + +#hosting-task-confirm-form .form-checkboxes .form-item code { + background: transparent; +} + +.panel-ansii.panel-body { + background: #262626; + padding: 0; +} + +.panel-type-error pre { + white-space: normal; +} + +pre.ansi_box { + padding: 1.25em; + white-space: pre-wrap; + line-height: 120%; +} + +.errors-table td > div { + color: rgb(51, 51, 51); + display: block; + font-family: Menlo, Monaco, Consolas, 'Courier New', monospace; + font-size: 13px; +} + +#project-settings-table .form-control, +#hosting-task-confirm-form .form-control#edit-parameters-environment-name { + width: auto; + display:inline; + text-align: center; + font-size: 1.5em; + font-weight: bold; +} +.form-control#edit-environment-name { + width: auto; + text-align: center; + display:inline; + font-size: 1.5em; + font-weight: bold; +} +.input-group-addon-url { + width: 100%; + text-align: left; +} + +#devshop-project-create-step-git #edit-cancel { + float: right; +} + +.project-add-environment-button { + margin: 0px auto; + text-align: center; +} +#edit-add-environment { + background: #999; +} +p.wait { + text-align: center; + padding: 32px 0px 52px; + background: bottom center no-repeat url(images/loading.gif); +} + +p.error { + text-align: center; + padding: 32px 0px 52px; +} + +#hosting-site-info, #hosting-task-list { + width: 100%; + float: none; + margin: 0; +} +.node-type-server .panel-body .form-item { + margin-right: 2em; +} + +a.logo img { + max-height: 3em; +} + +.project-link { + font-size: 1.5em; + font-weight: bold; + display: block; +} + +.btn-group-links { + white-space: nowrap; +} + +.btn-group-links .btn-hooks { + white-space: normal; +} + +.btn-hooks { + float: none !important; + display: inline-block; +} + +#devshop-project-create-step-sites #edit-cancel, +#devshop-project-create-step-environments #edit-cancel, +#project-node-form #edit-cancel { + float: right; +} + +li.text.code { + white-space: nowrap; +} + +.list-group-item-info p { + color: #0f0f0f; +} + +.panel-drush-access section { + padding-bottom: 1em; +} +.panel-drush-access label { + width: 25%; +} +.panel-drush-access input { + width: 70%; + display: inline-block; + vertical-align: middle; +} + +.list-group-item.clone-info { + padding: .5em 0 .3em; +} + + + +.modal-info label { + width: 19%; +} + +.modal-info input.form-control { + display: inline-block; + width: 60%; +} + +.add-environment-container { + padding-left: 0px; + padding-right: 0px; +} + +.install-method-wrapper { + display: inline-block; + margin-right: 1em; +} + +.install-method-wrapper.radio input[type="radio"] { + margin-left: -16px; +} + +#edit-install-method > .form-item > label { + width: 100%; + white-space: normal; +} + +#edit-install-method { + clear: left; +} +.form-item-environment-name { + padding-left: 0 !important; +} +.form-item-git-ref { + padding-right: 0 !important; +} +#edit-install-method-clone-source label span { + min-width: 6em; + display: inline-block; +} +#edit-install-method-clone-source label small.git-ref { + min-width: 6em; + display: inline-block; + margin-left: 1em; +} +#edit-install-method-clone-source label:hover { + background-color: whitesmoke; +} +.no-wrap { + white-space: nowrap; +} +.inline { + display: inline-block; + vertical-align: middle; +} + +#block-menu-devshop-footer-links > ul > li { + display: inline-block; +} + +#block-menu-devshop-footer-links > h2.block-title { + display: none; +} + +.devshop-support-block h3 { + font-size: 1.0em; + color: gray; + text-transform: uppercase; + padding: 1em; + padding-top:3em; +} + + +.region-footer .devshop-support-block h3 { + padding-top:1em; +} + +.region-footer .devshop-support-block h3, +.region-footer .devshop-support-block ul.nav, +.region-footer .devshop-support-block ul.nav li +{ + display: inline-block; +} + +.table-responsive { + overflow: visible; +} + +.task-info { + width: 100%; + z-index: 100; + position: relative; +} + +.task-info-fixed { + width: 73%; + top: 20px; + position: fixed; +} + +.project-environment-links-fixed { + position: fixed; + top: 30px; + z-index: 1; + background: white; + width: 25%; +} + + + +@media (max-width: 767px) { + + .task-info-fixed { + width: 96%; + top: 40px; + } + + .project-environment-links-fixed { + top: 172px; + min-width: 50%; + } +} + +.follow-checkbox { + position: absolute; + right: 10px; + z-index: 10; +} +ul.cas-links { + padding-left: 0; +} + +img.login-images { + max-width:150px; +} + +.row.logos { + + width: 300px; + margin: 0px auto; +} + +footer.anonymous-page { + padding-top: 5em; +} + +#hosting-site-edit-info label { + position: relative; + left: 0; +} + +.form-item-git-url-select > .chosen-container { + display: block !important; + width: 100% !important; +} + +#edit-devshop-github-token-button > a.btn { + clear: both; +} + +#devshop-github-settings-form .form-item { + margin-top: 2em; +} + +.form-item-github-organization, +.form-item-github-repository-name { + display: inline-block; +} + + +.alert-project-welcome { + margin: 2em 0em; + padding: 1em 2em; +} +.alert-project-welcome h2 { + margin-top: 0px; +} + +#edit-github-create label, +.form-item-github-repos label +{ + display: block; +} + +#edit-settings-github-repository-source-composer-project-suggestions, +#edit-settings-github-repository-source-composer-project { + width: 25em; +} + +.form-item-github-create-github-organization, +.form-item-github-create-github-repository-name { + display: inline-block; +} + +form .well .form-type-radios label, +form .well .form-type-radios .form-type-radio +{ + display: inline-block; + margin-right: 1em; +} + +.form-item-github-create-github-repository-name { + width: 14em; +} + +#edit-settings-github-repository-source-populate-choice > div.form-type-radio > label { + margin-right: 2em; +} + +#edit-github-create-github-organization { + min-width: 14em; +} + +#edit-github-create-github-repository-public { + padding-bottom: 1em; +} + +.form-control.chosen-container { + padding: 0px; + background-color: transparent; + border: none; + box-shadow: none; + + height: 30px; +} +.form-control.chosen-container > a > span { + padding: 6px 0px; +} +.form-control.chosen-container.chosen-container-single .chosen-single { + height: 34px; +} \ No newline at end of file diff --git a/themes/boots/boots.info b/themes/boots/boots.info new file mode 100644 index 000000000..8c691b087 --- /dev/null +++ b/themes/boots/boots.info @@ -0,0 +1,29 @@ +name = Boots +description = Bootstrap-powered theme for devshop. +core = 7.x +engine = phptemplate + +base theme = bootstrap + +stylesheets[all][] = boots.css +stylesheets[all][] = fontawesome/css/font-awesome.min.css + +scripts[] = js/jquery.matchHeight.js +scripts[] = js/jquery.timeago.js +scripts[] = js/boots.js + +; Settings +settings[toggle_logo] = 1 +settings[bootstrap_fluid_container] = 1 +settings[bootstrap_navbar_position] = 'static-top' +settings[bootstrap_navbar_inverse] = 1 +settings[bootstrap_region_well-sidebar_first] = 0 +settings[bootstrap_cdn_provider] = custom +settings[bootstrap_cdn_custom_css] = /profiles/devmaster/themes/boots/bootstrap/css/bootstrap.css +settings[bootstrap_cdn_custom_css_min] = /profiles/devmaster/themes/boots/bootstrap/css/bootstrap.min.css +settings[bootstrap_cdn_custom_js] = /profiles/devmaster/themes/boots/bootstrap/js/bootstrap.js +settings[bootstrap_cdn_custom_js_min] = /profiles/devmaster/themes/boots/bootstrap/js/bootstrap.min.js + +; "Smart form descriptions" maximum character limit +; (Form element descriptions shorter than this will be turned into bootstrap tooltips.) +settings[bootstrap_forms_smart_descriptions_limit] = 40 diff --git a/themes/boots/bootstrap/css/bootstrap-theme.css b/themes/boots/bootstrap/css/bootstrap-theme.css new file mode 100644 index 000000000..ebe57fbf6 --- /dev/null +++ b/themes/boots/bootstrap/css/bootstrap-theme.css @@ -0,0 +1,587 @@ +/*! + * Bootstrap v3.3.6 (http://getbootstrap.com) + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +.btn-default, +.btn-primary, +.btn-success, +.btn-info, +.btn-warning, +.btn-danger { + text-shadow: 0 -1px 0 rgba(0, 0, 0, .2); + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); +} +.btn-default:active, +.btn-primary:active, +.btn-success:active, +.btn-info:active, +.btn-warning:active, +.btn-danger:active, +.btn-default.active, +.btn-primary.active, +.btn-success.active, +.btn-info.active, +.btn-warning.active, +.btn-danger.active { + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); +} +.btn-default.disabled, +.btn-primary.disabled, +.btn-success.disabled, +.btn-info.disabled, +.btn-warning.disabled, +.btn-danger.disabled, +.btn-default[disabled], +.btn-primary[disabled], +.btn-success[disabled], +.btn-info[disabled], +.btn-warning[disabled], +.btn-danger[disabled], +fieldset[disabled] .btn-default, +fieldset[disabled] .btn-primary, +fieldset[disabled] .btn-success, +fieldset[disabled] .btn-info, +fieldset[disabled] .btn-warning, +fieldset[disabled] .btn-danger { + -webkit-box-shadow: none; + box-shadow: none; +} +.btn-default .badge, +.btn-primary .badge, +.btn-success .badge, +.btn-info .badge, +.btn-warning .badge, +.btn-danger .badge { + text-shadow: none; +} +.btn:active, +.btn.active { + background-image: none; +} +.btn-default { + text-shadow: 0 1px 0 #fff; + background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%); + background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0)); + background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + background-repeat: repeat-x; + border-color: #dbdbdb; + border-color: #ccc; +} +.btn-default:hover, +.btn-default:focus { + background-color: #e0e0e0; + background-position: 0 -15px; +} +.btn-default:active, +.btn-default.active { + background-color: #e0e0e0; + border-color: #dbdbdb; +} +.btn-default.disabled, +.btn-default[disabled], +fieldset[disabled] .btn-default, +.btn-default.disabled:hover, +.btn-default[disabled]:hover, +fieldset[disabled] .btn-default:hover, +.btn-default.disabled:focus, +.btn-default[disabled]:focus, +fieldset[disabled] .btn-default:focus, +.btn-default.disabled.focus, +.btn-default[disabled].focus, +fieldset[disabled] .btn-default.focus, +.btn-default.disabled:active, +.btn-default[disabled]:active, +fieldset[disabled] .btn-default:active, +.btn-default.disabled.active, +.btn-default[disabled].active, +fieldset[disabled] .btn-default.active { + background-color: #e0e0e0; + background-image: none; +} +.btn-primary { + background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%); + background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88)); + background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + background-repeat: repeat-x; + border-color: #245580; +} +.btn-primary:hover, +.btn-primary:focus { + background-color: #265a88; + background-position: 0 -15px; +} +.btn-primary:active, +.btn-primary.active { + background-color: #265a88; + border-color: #245580; +} +.btn-primary.disabled, +.btn-primary[disabled], +fieldset[disabled] .btn-primary, +.btn-primary.disabled:hover, +.btn-primary[disabled]:hover, +fieldset[disabled] .btn-primary:hover, +.btn-primary.disabled:focus, +.btn-primary[disabled]:focus, +fieldset[disabled] .btn-primary:focus, +.btn-primary.disabled.focus, +.btn-primary[disabled].focus, +fieldset[disabled] .btn-primary.focus, +.btn-primary.disabled:active, +.btn-primary[disabled]:active, +fieldset[disabled] .btn-primary:active, +.btn-primary.disabled.active, +.btn-primary[disabled].active, +fieldset[disabled] .btn-primary.active { + background-color: #265a88; + background-image: none; +} +.btn-success { + background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%); + background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641)); + background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + background-repeat: repeat-x; + border-color: #3e8f3e; +} +.btn-success:hover, +.btn-success:focus { + background-color: #419641; + background-position: 0 -15px; +} +.btn-success:active, +.btn-success.active { + background-color: #419641; + border-color: #3e8f3e; +} +.btn-success.disabled, +.btn-success[disabled], +fieldset[disabled] .btn-success, +.btn-success.disabled:hover, +.btn-success[disabled]:hover, +fieldset[disabled] .btn-success:hover, +.btn-success.disabled:focus, +.btn-success[disabled]:focus, +fieldset[disabled] .btn-success:focus, +.btn-success.disabled.focus, +.btn-success[disabled].focus, +fieldset[disabled] .btn-success.focus, +.btn-success.disabled:active, +.btn-success[disabled]:active, +fieldset[disabled] .btn-success:active, +.btn-success.disabled.active, +.btn-success[disabled].active, +fieldset[disabled] .btn-success.active { + background-color: #419641; + background-image: none; +} +.btn-info { + background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); + background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2)); + background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + background-repeat: repeat-x; + border-color: #28a4c9; +} +.btn-info:hover, +.btn-info:focus { + background-color: #2aabd2; + background-position: 0 -15px; +} +.btn-info:active, +.btn-info.active { + background-color: #2aabd2; + border-color: #28a4c9; +} +.btn-info.disabled, +.btn-info[disabled], +fieldset[disabled] .btn-info, +.btn-info.disabled:hover, +.btn-info[disabled]:hover, +fieldset[disabled] .btn-info:hover, +.btn-info.disabled:focus, +.btn-info[disabled]:focus, +fieldset[disabled] .btn-info:focus, +.btn-info.disabled.focus, +.btn-info[disabled].focus, +fieldset[disabled] .btn-info.focus, +.btn-info.disabled:active, +.btn-info[disabled]:active, +fieldset[disabled] .btn-info:active, +.btn-info.disabled.active, +.btn-info[disabled].active, +fieldset[disabled] .btn-info.active { + background-color: #2aabd2; + background-image: none; +} +.btn-warning { + background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); + background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316)); + background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + background-repeat: repeat-x; + border-color: #e38d13; +} +.btn-warning:hover, +.btn-warning:focus { + background-color: #eb9316; + background-position: 0 -15px; +} +.btn-warning:active, +.btn-warning.active { + background-color: #eb9316; + border-color: #e38d13; +} +.btn-warning.disabled, +.btn-warning[disabled], +fieldset[disabled] .btn-warning, +.btn-warning.disabled:hover, +.btn-warning[disabled]:hover, +fieldset[disabled] .btn-warning:hover, +.btn-warning.disabled:focus, +.btn-warning[disabled]:focus, +fieldset[disabled] .btn-warning:focus, +.btn-warning.disabled.focus, +.btn-warning[disabled].focus, +fieldset[disabled] .btn-warning.focus, +.btn-warning.disabled:active, +.btn-warning[disabled]:active, +fieldset[disabled] .btn-warning:active, +.btn-warning.disabled.active, +.btn-warning[disabled].active, +fieldset[disabled] .btn-warning.active { + background-color: #eb9316; + background-image: none; +} +.btn-danger { + background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%); + background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a)); + background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + background-repeat: repeat-x; + border-color: #b92c28; +} +.btn-danger:hover, +.btn-danger:focus { + background-color: #c12e2a; + background-position: 0 -15px; +} +.btn-danger:active, +.btn-danger.active { + background-color: #c12e2a; + border-color: #b92c28; +} +.btn-danger.disabled, +.btn-danger[disabled], +fieldset[disabled] .btn-danger, +.btn-danger.disabled:hover, +.btn-danger[disabled]:hover, +fieldset[disabled] .btn-danger:hover, +.btn-danger.disabled:focus, +.btn-danger[disabled]:focus, +fieldset[disabled] .btn-danger:focus, +.btn-danger.disabled.focus, +.btn-danger[disabled].focus, +fieldset[disabled] .btn-danger.focus, +.btn-danger.disabled:active, +.btn-danger[disabled]:active, +fieldset[disabled] .btn-danger:active, +.btn-danger.disabled.active, +.btn-danger[disabled].active, +fieldset[disabled] .btn-danger.active { + background-color: #c12e2a; + background-image: none; +} +.thumbnail, +.img-thumbnail { + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); + box-shadow: 0 1px 2px rgba(0, 0, 0, .075); +} +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus { + background-color: #e8e8e8; + background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); + background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); + background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); + background-repeat: repeat-x; +} +.dropdown-menu > .active > a, +.dropdown-menu > .active > a:hover, +.dropdown-menu > .active > a:focus { + background-color: #2e6da4; + background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); + background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); + background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); + background-repeat: repeat-x; +} +.navbar-default { + background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%); + background-image: -o-linear-gradient(top, #fff 0%, #f8f8f8 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f8f8f8)); + background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + background-repeat: repeat-x; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); +} +.navbar-default .navbar-nav > .open > a, +.navbar-default .navbar-nav > .active > a { + background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); + background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2)); + background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0); + background-repeat: repeat-x; + -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); + box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); +} +.navbar-brand, +.navbar-nav > li > a { + text-shadow: 0 1px 0 rgba(255, 255, 255, .25); +} +.navbar-inverse { + background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%); + background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222)); + background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + background-repeat: repeat-x; + border-radius: 4px; +} +.navbar-inverse .navbar-nav > .open > a, +.navbar-inverse .navbar-nav > .active > a { + background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%); + background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f)); + background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0); + background-repeat: repeat-x; + -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); + box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); +} +.navbar-inverse .navbar-brand, +.navbar-inverse .navbar-nav > li > a { + text-shadow: 0 -1px 0 rgba(0, 0, 0, .25); +} +.navbar-static-top, +.navbar-fixed-top, +.navbar-fixed-bottom { + border-radius: 0; +} +@media (max-width: 767px) { + .navbar .navbar-nav .open .dropdown-menu > .active > a, + .navbar .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #fff; + background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); + background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); + background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); + background-repeat: repeat-x; + } +} +.alert { + text-shadow: 0 1px 0 rgba(255, 255, 255, .2); + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); +} +.alert-success { + background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); + background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc)); + background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0); + background-repeat: repeat-x; + border-color: #b2dba1; +} +.alert-info { + background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%); + background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0)); + background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0); + background-repeat: repeat-x; + border-color: #9acfea; +} +.alert-warning { + background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); + background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0)); + background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0); + background-repeat: repeat-x; + border-color: #f5e79e; +} +.alert-danger { + background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); + background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3)); + background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0); + background-repeat: repeat-x; + border-color: #dca7a7; +} +.progress { + background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); + background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5)); + background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0); + background-repeat: repeat-x; +} +.progress-bar { + background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%); + background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#286090)); + background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0); + background-repeat: repeat-x; +} +.progress-bar-success { + background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%); + background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44)); + background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0); + background-repeat: repeat-x; +} +.progress-bar-info { + background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); + background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5)); + background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0); + background-repeat: repeat-x; +} +.progress-bar-warning { + background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); + background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f)); + background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0); + background-repeat: repeat-x; +} +.progress-bar-danger { + background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%); + background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c)); + background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0); + background-repeat: repeat-x; +} +.progress-bar-striped { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +.list-group { + border-radius: 4px; + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); + box-shadow: 0 1px 2px rgba(0, 0, 0, .075); +} +.list-group-item.active, +.list-group-item.active:hover, +.list-group-item.active:focus { + text-shadow: 0 -1px 0 #286090; + background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%); + background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2b669a)); + background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0); + background-repeat: repeat-x; + border-color: #2b669a; +} +.list-group-item.active .badge, +.list-group-item.active:hover .badge, +.list-group-item.active:focus .badge { + text-shadow: none; +} +.panel { + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05); + box-shadow: 0 1px 2px rgba(0, 0, 0, .05); +} +.panel-default > .panel-heading { + background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); + background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); + background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); + background-repeat: repeat-x; +} +.panel-primary > .panel-heading { + background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); + background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); + background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); + background-repeat: repeat-x; +} +.panel-success > .panel-heading { + background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); + background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6)); + background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0); + background-repeat: repeat-x; +} +.panel-info > .panel-heading { + background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); + background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3)); + background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0); + background-repeat: repeat-x; +} +.panel-warning > .panel-heading { + background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); + background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc)); + background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0); + background-repeat: repeat-x; +} +.panel-danger > .panel-heading { + background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%); + background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc)); + background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0); + background-repeat: repeat-x; +} +.well { + background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); + background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5)); + background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0); + background-repeat: repeat-x; + border-color: #dcdcdc; + -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); + box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); +} +/*# sourceMappingURL=bootstrap-theme.css.map */ diff --git a/themes/boots/bootstrap/css/bootstrap-theme.css.map b/themes/boots/bootstrap/css/bootstrap-theme.css.map new file mode 100644 index 000000000..21e19101e --- /dev/null +++ b/themes/boots/bootstrap/css/bootstrap-theme.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["bootstrap-theme.css","less/theme.less","less/mixins/vendor-prefixes.less","less/mixins/gradients.less","less/mixins/reset-filter.less"],"names":[],"mappings":"AAAA;;;;GAIG;ACeH;;;;;;EAME,yCAAA;EC2CA,4FAAA;EACQ,oFAAA;CFvDT;ACgBC;;;;;;;;;;;;ECsCA,yDAAA;EACQ,iDAAA;CFxCT;ACMC;;;;;;;;;;;;;;;;;;ECiCA,yBAAA;EACQ,iBAAA;CFnBT;AC/BD;;;;;;EAuBI,kBAAA;CDgBH;ACyBC;;EAEE,uBAAA;CDvBH;AC4BD;EErEI,sEAAA;EACA,iEAAA;EACA,2FAAA;EAAA,oEAAA;EAEA,uHAAA;ECnBF,oEAAA;EH4CA,4BAAA;EACA,sBAAA;EAuC2C,0BAAA;EAA2B,mBAAA;CDjBvE;ACpBC;;EAEE,0BAAA;EACA,6BAAA;CDsBH;ACnBC;;EAEE,0BAAA;EACA,sBAAA;CDqBH;ACfG;;;;;;;;;;;;;;;;;;EAME,0BAAA;EACA,uBAAA;CD6BL;ACbD;EEtEI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EAEA,uHAAA;ECnBF,oEAAA;EH4CA,4BAAA;EACA,sBAAA;CD8DD;AC5DC;;EAEE,0BAAA;EACA,6BAAA;CD8DH;AC3DC;;EAEE,0BAAA;EACA,sBAAA;CD6DH;ACvDG;;;;;;;;;;;;;;;;;;EAME,0BAAA;EACA,uBAAA;CDqEL;ACpDD;EEvEI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EAEA,uHAAA;ECnBF,oEAAA;EH4CA,4BAAA;EACA,sBAAA;CDsGD;ACpGC;;EAEE,0BAAA;EACA,6BAAA;CDsGH;ACnGC;;EAEE,0BAAA;EACA,sBAAA;CDqGH;AC/FG;;;;;;;;;;;;;;;;;;EAME,0BAAA;EACA,uBAAA;CD6GL;AC3FD;EExEI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EAEA,uHAAA;ECnBF,oEAAA;EH4CA,4BAAA;EACA,sBAAA;CD8ID;AC5IC;;EAEE,0BAAA;EACA,6BAAA;CD8IH;AC3IC;;EAEE,0BAAA;EACA,sBAAA;CD6IH;ACvIG;;;;;;;;;;;;;;;;;;EAME,0BAAA;EACA,uBAAA;CDqJL;AClID;EEzEI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EAEA,uHAAA;ECnBF,oEAAA;EH4CA,4BAAA;EACA,sBAAA;CDsLD;ACpLC;;EAEE,0BAAA;EACA,6BAAA;CDsLH;ACnLC;;EAEE,0BAAA;EACA,sBAAA;CDqLH;AC/KG;;;;;;;;;;;;;;;;;;EAME,0BAAA;EACA,uBAAA;CD6LL;ACzKD;EE1EI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EAEA,uHAAA;ECnBF,oEAAA;EH4CA,4BAAA;EACA,sBAAA;CD8ND;AC5NC;;EAEE,0BAAA;EACA,6BAAA;CD8NH;AC3NC;;EAEE,0BAAA;EACA,sBAAA;CD6NH;ACvNG;;;;;;;;;;;;;;;;;;EAME,0BAAA;EACA,uBAAA;CDqOL;AC1MD;;EClCE,mDAAA;EACQ,2CAAA;CFgPT;ACrMD;;EE3FI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;EF0FF,0BAAA;CD2MD;ACzMD;;;EEhGI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;EFgGF,0BAAA;CD+MD;ACtMD;EE7GI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;ECnBF,oEAAA;EH+HA,mBAAA;ECjEA,4FAAA;EACQ,oFAAA;CF8QT;ACjND;;EE7GI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;ED2CF,yDAAA;EACQ,iDAAA;CFwRT;AC9MD;;EAEE,+CAAA;CDgND;AC5MD;EEhII,sEAAA;EACA,iEAAA;EACA,2FAAA;EAAA,oEAAA;EACA,4BAAA;EACA,uHAAA;ECnBF,oEAAA;EHkJA,mBAAA;CDkND;ACrND;;EEhII,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;ED2CF,wDAAA;EACQ,gDAAA;CF+ST;AC/ND;;EAYI,0CAAA;CDuNH;AClND;;;EAGE,iBAAA;CDoND;AC/LD;EAfI;;;IAGE,YAAA;IE7JF,yEAAA;IACA,oEAAA;IACA,8FAAA;IAAA,uEAAA;IACA,4BAAA;IACA,uHAAA;GH+WD;CACF;AC3MD;EACE,8CAAA;EC3HA,2FAAA;EACQ,mFAAA;CFyUT;ACnMD;EEtLI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;EF8KF,sBAAA;CD+MD;AC1MD;EEvLI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;EF8KF,sBAAA;CDuND;ACjND;EExLI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;EF8KF,sBAAA;CD+ND;ACxND;EEzLI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;EF8KF,sBAAA;CDuOD;ACxND;EEjMI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;CH4ZH;ACrND;EE3MI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;CHmaH;AC3ND;EE5MI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;CH0aH;ACjOD;EE7MI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;CHibH;ACvOD;EE9MI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;CHwbH;AC7OD;EE/MI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;CH+bH;AChPD;EElLI,8MAAA;EACA,yMAAA;EACA,sMAAA;CHqaH;AC5OD;EACE,mBAAA;EC9KA,mDAAA;EACQ,2CAAA;CF6ZT;AC7OD;;;EAGE,8BAAA;EEnOE,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;EFiOF,sBAAA;CDmPD;ACxPD;;;EAQI,kBAAA;CDqPH;AC3OD;ECnME,kDAAA;EACQ,0CAAA;CFibT;ACrOD;EE5PI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;CHoeH;AC3OD;EE7PI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;CH2eH;ACjPD;EE9PI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;CHkfH;ACvPD;EE/PI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;CHyfH;AC7PD;EEhQI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;CHggBH;ACnQD;EEjQI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;CHugBH;ACnQD;EExQI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;EFsQF,sBAAA;EC3NA,0FAAA;EACQ,kFAAA;CFqeT","file":"bootstrap-theme.css","sourcesContent":["/*!\n * Bootstrap v3.3.6 (http://getbootstrap.com)\n * Copyright 2011-2015 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n.btn-default,\n.btn-primary,\n.btn-success,\n.btn-info,\n.btn-warning,\n.btn-danger {\n text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2);\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.btn-default:active,\n.btn-primary:active,\n.btn-success:active,\n.btn-info:active,\n.btn-warning:active,\n.btn-danger:active,\n.btn-default.active,\n.btn-primary.active,\n.btn-success.active,\n.btn-info.active,\n.btn-warning.active,\n.btn-danger.active {\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn-default.disabled,\n.btn-primary.disabled,\n.btn-success.disabled,\n.btn-info.disabled,\n.btn-warning.disabled,\n.btn-danger.disabled,\n.btn-default[disabled],\n.btn-primary[disabled],\n.btn-success[disabled],\n.btn-info[disabled],\n.btn-warning[disabled],\n.btn-danger[disabled],\nfieldset[disabled] .btn-default,\nfieldset[disabled] .btn-primary,\nfieldset[disabled] .btn-success,\nfieldset[disabled] .btn-info,\nfieldset[disabled] .btn-warning,\nfieldset[disabled] .btn-danger {\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn-default .badge,\n.btn-primary .badge,\n.btn-success .badge,\n.btn-info .badge,\n.btn-warning .badge,\n.btn-danger .badge {\n text-shadow: none;\n}\n.btn:active,\n.btn.active {\n background-image: none;\n}\n.btn-default {\n background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%);\n background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%);\n background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #dbdbdb;\n text-shadow: 0 1px 0 #fff;\n border-color: #ccc;\n}\n.btn-default:hover,\n.btn-default:focus {\n background-color: #e0e0e0;\n background-position: 0 -15px;\n}\n.btn-default:active,\n.btn-default.active {\n background-color: #e0e0e0;\n border-color: #dbdbdb;\n}\n.btn-default.disabled,\n.btn-default[disabled],\nfieldset[disabled] .btn-default,\n.btn-default.disabled:hover,\n.btn-default[disabled]:hover,\nfieldset[disabled] .btn-default:hover,\n.btn-default.disabled:focus,\n.btn-default[disabled]:focus,\nfieldset[disabled] .btn-default:focus,\n.btn-default.disabled.focus,\n.btn-default[disabled].focus,\nfieldset[disabled] .btn-default.focus,\n.btn-default.disabled:active,\n.btn-default[disabled]:active,\nfieldset[disabled] .btn-default:active,\n.btn-default.disabled.active,\n.btn-default[disabled].active,\nfieldset[disabled] .btn-default.active {\n background-color: #e0e0e0;\n background-image: none;\n}\n.btn-primary {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #245580;\n}\n.btn-primary:hover,\n.btn-primary:focus {\n background-color: #265a88;\n background-position: 0 -15px;\n}\n.btn-primary:active,\n.btn-primary.active {\n background-color: #265a88;\n border-color: #245580;\n}\n.btn-primary.disabled,\n.btn-primary[disabled],\nfieldset[disabled] .btn-primary,\n.btn-primary.disabled:hover,\n.btn-primary[disabled]:hover,\nfieldset[disabled] .btn-primary:hover,\n.btn-primary.disabled:focus,\n.btn-primary[disabled]:focus,\nfieldset[disabled] .btn-primary:focus,\n.btn-primary.disabled.focus,\n.btn-primary[disabled].focus,\nfieldset[disabled] .btn-primary.focus,\n.btn-primary.disabled:active,\n.btn-primary[disabled]:active,\nfieldset[disabled] .btn-primary:active,\n.btn-primary.disabled.active,\n.btn-primary[disabled].active,\nfieldset[disabled] .btn-primary.active {\n background-color: #265a88;\n background-image: none;\n}\n.btn-success {\n background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);\n background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%);\n background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #3e8f3e;\n}\n.btn-success:hover,\n.btn-success:focus {\n background-color: #419641;\n background-position: 0 -15px;\n}\n.btn-success:active,\n.btn-success.active {\n background-color: #419641;\n border-color: #3e8f3e;\n}\n.btn-success.disabled,\n.btn-success[disabled],\nfieldset[disabled] .btn-success,\n.btn-success.disabled:hover,\n.btn-success[disabled]:hover,\nfieldset[disabled] .btn-success:hover,\n.btn-success.disabled:focus,\n.btn-success[disabled]:focus,\nfieldset[disabled] .btn-success:focus,\n.btn-success.disabled.focus,\n.btn-success[disabled].focus,\nfieldset[disabled] .btn-success.focus,\n.btn-success.disabled:active,\n.btn-success[disabled]:active,\nfieldset[disabled] .btn-success:active,\n.btn-success.disabled.active,\n.btn-success[disabled].active,\nfieldset[disabled] .btn-success.active {\n background-color: #419641;\n background-image: none;\n}\n.btn-info {\n background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);\n background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);\n background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #28a4c9;\n}\n.btn-info:hover,\n.btn-info:focus {\n background-color: #2aabd2;\n background-position: 0 -15px;\n}\n.btn-info:active,\n.btn-info.active {\n background-color: #2aabd2;\n border-color: #28a4c9;\n}\n.btn-info.disabled,\n.btn-info[disabled],\nfieldset[disabled] .btn-info,\n.btn-info.disabled:hover,\n.btn-info[disabled]:hover,\nfieldset[disabled] .btn-info:hover,\n.btn-info.disabled:focus,\n.btn-info[disabled]:focus,\nfieldset[disabled] .btn-info:focus,\n.btn-info.disabled.focus,\n.btn-info[disabled].focus,\nfieldset[disabled] .btn-info.focus,\n.btn-info.disabled:active,\n.btn-info[disabled]:active,\nfieldset[disabled] .btn-info:active,\n.btn-info.disabled.active,\n.btn-info[disabled].active,\nfieldset[disabled] .btn-info.active {\n background-color: #2aabd2;\n background-image: none;\n}\n.btn-warning {\n background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);\n background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);\n background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #e38d13;\n}\n.btn-warning:hover,\n.btn-warning:focus {\n background-color: #eb9316;\n background-position: 0 -15px;\n}\n.btn-warning:active,\n.btn-warning.active {\n background-color: #eb9316;\n border-color: #e38d13;\n}\n.btn-warning.disabled,\n.btn-warning[disabled],\nfieldset[disabled] .btn-warning,\n.btn-warning.disabled:hover,\n.btn-warning[disabled]:hover,\nfieldset[disabled] .btn-warning:hover,\n.btn-warning.disabled:focus,\n.btn-warning[disabled]:focus,\nfieldset[disabled] .btn-warning:focus,\n.btn-warning.disabled.focus,\n.btn-warning[disabled].focus,\nfieldset[disabled] .btn-warning.focus,\n.btn-warning.disabled:active,\n.btn-warning[disabled]:active,\nfieldset[disabled] .btn-warning:active,\n.btn-warning.disabled.active,\n.btn-warning[disabled].active,\nfieldset[disabled] .btn-warning.active {\n background-color: #eb9316;\n background-image: none;\n}\n.btn-danger {\n background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);\n background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%);\n background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #b92c28;\n}\n.btn-danger:hover,\n.btn-danger:focus {\n background-color: #c12e2a;\n background-position: 0 -15px;\n}\n.btn-danger:active,\n.btn-danger.active {\n background-color: #c12e2a;\n border-color: #b92c28;\n}\n.btn-danger.disabled,\n.btn-danger[disabled],\nfieldset[disabled] .btn-danger,\n.btn-danger.disabled:hover,\n.btn-danger[disabled]:hover,\nfieldset[disabled] .btn-danger:hover,\n.btn-danger.disabled:focus,\n.btn-danger[disabled]:focus,\nfieldset[disabled] .btn-danger:focus,\n.btn-danger.disabled.focus,\n.btn-danger[disabled].focus,\nfieldset[disabled] .btn-danger.focus,\n.btn-danger.disabled:active,\n.btn-danger[disabled]:active,\nfieldset[disabled] .btn-danger:active,\n.btn-danger.disabled.active,\n.btn-danger[disabled].active,\nfieldset[disabled] .btn-danger.active {\n background-color: #c12e2a;\n background-image: none;\n}\n.thumbnail,\n.img-thumbnail {\n -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n}\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);\n background-color: #e8e8e8;\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);\n background-color: #2e6da4;\n}\n.navbar-default {\n background-image: -webkit-linear-gradient(top, #ffffff 0%, #f8f8f8 100%);\n background-image: -o-linear-gradient(top, #ffffff 0%, #f8f8f8 100%);\n background-image: linear-gradient(to bottom, #ffffff 0%, #f8f8f8 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075);\n}\n.navbar-default .navbar-nav > .open > a,\n.navbar-default .navbar-nav > .active > a {\n background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);\n background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);\n background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);\n -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075);\n}\n.navbar-brand,\n.navbar-nav > li > a {\n text-shadow: 0 1px 0 rgba(255, 255, 255, 0.25);\n}\n.navbar-inverse {\n background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%);\n background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%);\n background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n border-radius: 4px;\n}\n.navbar-inverse .navbar-nav > .open > a,\n.navbar-inverse .navbar-nav > .active > a {\n background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%);\n background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%);\n background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);\n -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25);\n box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25);\n}\n.navbar-inverse .navbar-brand,\n.navbar-inverse .navbar-nav > li > a {\n text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);\n}\n.navbar-static-top,\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n border-radius: 0;\n}\n@media (max-width: 767px) {\n .navbar .navbar-nav .open .dropdown-menu > .active > a,\n .navbar .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #fff;\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);\n }\n}\n.alert {\n text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2);\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);\n}\n.alert-success {\n background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);\n background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);\n background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);\n border-color: #b2dba1;\n}\n.alert-info {\n background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);\n background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%);\n background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);\n border-color: #9acfea;\n}\n.alert-warning {\n background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);\n background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);\n background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);\n border-color: #f5e79e;\n}\n.alert-danger {\n background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);\n background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);\n background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);\n border-color: #dca7a7;\n}\n.progress {\n background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);\n background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);\n background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);\n}\n.progress-bar {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);\n}\n.progress-bar-success {\n background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);\n background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%);\n background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);\n}\n.progress-bar-info {\n background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);\n background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);\n background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);\n}\n.progress-bar-warning {\n background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);\n background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);\n background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);\n}\n.progress-bar-danger {\n background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);\n background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%);\n background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);\n}\n.progress-bar-striped {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.list-group {\n border-radius: 4px;\n -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n text-shadow: 0 -1px 0 #286090;\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);\n border-color: #2b669a;\n}\n.list-group-item.active .badge,\n.list-group-item.active:hover .badge,\n.list-group-item.active:focus .badge {\n text-shadow: none;\n}\n.panel {\n -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);\n}\n.panel-default > .panel-heading {\n background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);\n}\n.panel-primary > .panel-heading {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);\n}\n.panel-success > .panel-heading {\n background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);\n background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);\n background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);\n}\n.panel-info > .panel-heading {\n background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);\n background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);\n background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);\n}\n.panel-warning > .panel-heading {\n background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);\n background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);\n background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);\n}\n.panel-danger > .panel-heading {\n background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);\n background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%);\n background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);\n}\n.well {\n background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);\n background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);\n background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);\n border-color: #dcdcdc;\n -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1);\n box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1);\n}\n/*# sourceMappingURL=bootstrap-theme.css.map */","/*!\n * Bootstrap v3.3.6 (http://getbootstrap.com)\n * Copyright 2011-2015 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n\n//\n// Load core variables and mixins\n// --------------------------------------------------\n\n@import \"variables.less\";\n@import \"mixins.less\";\n\n\n//\n// Buttons\n// --------------------------------------------------\n\n// Common styles\n.btn-default,\n.btn-primary,\n.btn-success,\n.btn-info,\n.btn-warning,\n.btn-danger {\n text-shadow: 0 -1px 0 rgba(0,0,0,.2);\n @shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 1px rgba(0,0,0,.075);\n .box-shadow(@shadow);\n\n // Reset the shadow\n &:active,\n &.active {\n .box-shadow(inset 0 3px 5px rgba(0,0,0,.125));\n }\n\n &.disabled,\n &[disabled],\n fieldset[disabled] & {\n .box-shadow(none);\n }\n\n .badge {\n text-shadow: none;\n }\n}\n\n// Mixin for generating new styles\n.btn-styles(@btn-color: #555) {\n #gradient > .vertical(@start-color: @btn-color; @end-color: darken(@btn-color, 12%));\n .reset-filter(); // Disable gradients for IE9 because filter bleeds through rounded corners; see https://github.com/twbs/bootstrap/issues/10620\n background-repeat: repeat-x;\n border-color: darken(@btn-color, 14%);\n\n &:hover,\n &:focus {\n background-color: darken(@btn-color, 12%);\n background-position: 0 -15px;\n }\n\n &:active,\n &.active {\n background-color: darken(@btn-color, 12%);\n border-color: darken(@btn-color, 14%);\n }\n\n &.disabled,\n &[disabled],\n fieldset[disabled] & {\n &,\n &:hover,\n &:focus,\n &.focus,\n &:active,\n &.active {\n background-color: darken(@btn-color, 12%);\n background-image: none;\n }\n }\n}\n\n// Common styles\n.btn {\n // Remove the gradient for the pressed/active state\n &:active,\n &.active {\n background-image: none;\n }\n}\n\n// Apply the mixin to the buttons\n.btn-default { .btn-styles(@btn-default-bg); text-shadow: 0 1px 0 #fff; border-color: #ccc; }\n.btn-primary { .btn-styles(@btn-primary-bg); }\n.btn-success { .btn-styles(@btn-success-bg); }\n.btn-info { .btn-styles(@btn-info-bg); }\n.btn-warning { .btn-styles(@btn-warning-bg); }\n.btn-danger { .btn-styles(@btn-danger-bg); }\n\n\n//\n// Images\n// --------------------------------------------------\n\n.thumbnail,\n.img-thumbnail {\n .box-shadow(0 1px 2px rgba(0,0,0,.075));\n}\n\n\n//\n// Dropdowns\n// --------------------------------------------------\n\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n #gradient > .vertical(@start-color: @dropdown-link-hover-bg; @end-color: darken(@dropdown-link-hover-bg, 5%));\n background-color: darken(@dropdown-link-hover-bg, 5%);\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n #gradient > .vertical(@start-color: @dropdown-link-active-bg; @end-color: darken(@dropdown-link-active-bg, 5%));\n background-color: darken(@dropdown-link-active-bg, 5%);\n}\n\n\n//\n// Navbar\n// --------------------------------------------------\n\n// Default navbar\n.navbar-default {\n #gradient > .vertical(@start-color: lighten(@navbar-default-bg, 10%); @end-color: @navbar-default-bg);\n .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered\n border-radius: @navbar-border-radius;\n @shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 5px rgba(0,0,0,.075);\n .box-shadow(@shadow);\n\n .navbar-nav > .open > a,\n .navbar-nav > .active > a {\n #gradient > .vertical(@start-color: darken(@navbar-default-link-active-bg, 5%); @end-color: darken(@navbar-default-link-active-bg, 2%));\n .box-shadow(inset 0 3px 9px rgba(0,0,0,.075));\n }\n}\n.navbar-brand,\n.navbar-nav > li > a {\n text-shadow: 0 1px 0 rgba(255,255,255,.25);\n}\n\n// Inverted navbar\n.navbar-inverse {\n #gradient > .vertical(@start-color: lighten(@navbar-inverse-bg, 10%); @end-color: @navbar-inverse-bg);\n .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered; see https://github.com/twbs/bootstrap/issues/10257\n border-radius: @navbar-border-radius;\n .navbar-nav > .open > a,\n .navbar-nav > .active > a {\n #gradient > .vertical(@start-color: @navbar-inverse-link-active-bg; @end-color: lighten(@navbar-inverse-link-active-bg, 2.5%));\n .box-shadow(inset 0 3px 9px rgba(0,0,0,.25));\n }\n\n .navbar-brand,\n .navbar-nav > li > a {\n text-shadow: 0 -1px 0 rgba(0,0,0,.25);\n }\n}\n\n// Undo rounded corners in static and fixed navbars\n.navbar-static-top,\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n border-radius: 0;\n}\n\n// Fix active state of dropdown items in collapsed mode\n@media (max-width: @grid-float-breakpoint-max) {\n .navbar .navbar-nav .open .dropdown-menu > .active > a {\n &,\n &:hover,\n &:focus {\n color: #fff;\n #gradient > .vertical(@start-color: @dropdown-link-active-bg; @end-color: darken(@dropdown-link-active-bg, 5%));\n }\n }\n}\n\n\n//\n// Alerts\n// --------------------------------------------------\n\n// Common styles\n.alert {\n text-shadow: 0 1px 0 rgba(255,255,255,.2);\n @shadow: inset 0 1px 0 rgba(255,255,255,.25), 0 1px 2px rgba(0,0,0,.05);\n .box-shadow(@shadow);\n}\n\n// Mixin for generating new styles\n.alert-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 7.5%));\n border-color: darken(@color, 15%);\n}\n\n// Apply the mixin to the alerts\n.alert-success { .alert-styles(@alert-success-bg); }\n.alert-info { .alert-styles(@alert-info-bg); }\n.alert-warning { .alert-styles(@alert-warning-bg); }\n.alert-danger { .alert-styles(@alert-danger-bg); }\n\n\n//\n// Progress bars\n// --------------------------------------------------\n\n// Give the progress background some depth\n.progress {\n #gradient > .vertical(@start-color: darken(@progress-bg, 4%); @end-color: @progress-bg)\n}\n\n// Mixin for generating new styles\n.progress-bar-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 10%));\n}\n\n// Apply the mixin to the progress bars\n.progress-bar { .progress-bar-styles(@progress-bar-bg); }\n.progress-bar-success { .progress-bar-styles(@progress-bar-success-bg); }\n.progress-bar-info { .progress-bar-styles(@progress-bar-info-bg); }\n.progress-bar-warning { .progress-bar-styles(@progress-bar-warning-bg); }\n.progress-bar-danger { .progress-bar-styles(@progress-bar-danger-bg); }\n\n// Reset the striped class because our mixins don't do multiple gradients and\n// the above custom styles override the new `.progress-bar-striped` in v3.2.0.\n.progress-bar-striped {\n #gradient > .striped();\n}\n\n\n//\n// List groups\n// --------------------------------------------------\n\n.list-group {\n border-radius: @border-radius-base;\n .box-shadow(0 1px 2px rgba(0,0,0,.075));\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n text-shadow: 0 -1px 0 darken(@list-group-active-bg, 10%);\n #gradient > .vertical(@start-color: @list-group-active-bg; @end-color: darken(@list-group-active-bg, 7.5%));\n border-color: darken(@list-group-active-border, 7.5%);\n\n .badge {\n text-shadow: none;\n }\n}\n\n\n//\n// Panels\n// --------------------------------------------------\n\n// Common styles\n.panel {\n .box-shadow(0 1px 2px rgba(0,0,0,.05));\n}\n\n// Mixin for generating new styles\n.panel-heading-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 5%));\n}\n\n// Apply the mixin to the panel headings only\n.panel-default > .panel-heading { .panel-heading-styles(@panel-default-heading-bg); }\n.panel-primary > .panel-heading { .panel-heading-styles(@panel-primary-heading-bg); }\n.panel-success > .panel-heading { .panel-heading-styles(@panel-success-heading-bg); }\n.panel-info > .panel-heading { .panel-heading-styles(@panel-info-heading-bg); }\n.panel-warning > .panel-heading { .panel-heading-styles(@panel-warning-heading-bg); }\n.panel-danger > .panel-heading { .panel-heading-styles(@panel-danger-heading-bg); }\n\n\n//\n// Wells\n// --------------------------------------------------\n\n.well {\n #gradient > .vertical(@start-color: darken(@well-bg, 5%); @end-color: @well-bg);\n border-color: darken(@well-bg, 10%);\n @shadow: inset 0 1px 3px rgba(0,0,0,.05), 0 1px 0 rgba(255,255,255,.1);\n .box-shadow(@shadow);\n}\n","// Vendor Prefixes\n//\n// All vendor mixins are deprecated as of v3.2.0 due to the introduction of\n// Autoprefixer in our Gruntfile. They have been removed in v4.\n\n// - Animations\n// - Backface visibility\n// - Box shadow\n// - Box sizing\n// - Content columns\n// - Hyphens\n// - Placeholder text\n// - Transformations\n// - Transitions\n// - User Select\n\n\n// Animations\n.animation(@animation) {\n -webkit-animation: @animation;\n -o-animation: @animation;\n animation: @animation;\n}\n.animation-name(@name) {\n -webkit-animation-name: @name;\n animation-name: @name;\n}\n.animation-duration(@duration) {\n -webkit-animation-duration: @duration;\n animation-duration: @duration;\n}\n.animation-timing-function(@timing-function) {\n -webkit-animation-timing-function: @timing-function;\n animation-timing-function: @timing-function;\n}\n.animation-delay(@delay) {\n -webkit-animation-delay: @delay;\n animation-delay: @delay;\n}\n.animation-iteration-count(@iteration-count) {\n -webkit-animation-iteration-count: @iteration-count;\n animation-iteration-count: @iteration-count;\n}\n.animation-direction(@direction) {\n -webkit-animation-direction: @direction;\n animation-direction: @direction;\n}\n.animation-fill-mode(@fill-mode) {\n -webkit-animation-fill-mode: @fill-mode;\n animation-fill-mode: @fill-mode;\n}\n\n// Backface visibility\n// Prevent browsers from flickering when using CSS 3D transforms.\n// Default value is `visible`, but can be changed to `hidden`\n\n.backface-visibility(@visibility) {\n -webkit-backface-visibility: @visibility;\n -moz-backface-visibility: @visibility;\n backface-visibility: @visibility;\n}\n\n// Drop shadows\n//\n// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's\n// supported browsers that have box shadow capabilities now support it.\n\n.box-shadow(@shadow) {\n -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1\n box-shadow: @shadow;\n}\n\n// Box sizing\n.box-sizing(@boxmodel) {\n -webkit-box-sizing: @boxmodel;\n -moz-box-sizing: @boxmodel;\n box-sizing: @boxmodel;\n}\n\n// CSS3 Content Columns\n.content-columns(@column-count; @column-gap: @grid-gutter-width) {\n -webkit-column-count: @column-count;\n -moz-column-count: @column-count;\n column-count: @column-count;\n -webkit-column-gap: @column-gap;\n -moz-column-gap: @column-gap;\n column-gap: @column-gap;\n}\n\n// Optional hyphenation\n.hyphens(@mode: auto) {\n word-wrap: break-word;\n -webkit-hyphens: @mode;\n -moz-hyphens: @mode;\n -ms-hyphens: @mode; // IE10+\n -o-hyphens: @mode;\n hyphens: @mode;\n}\n\n// Placeholder text\n.placeholder(@color: @input-color-placeholder) {\n // Firefox\n &::-moz-placeholder {\n color: @color;\n opacity: 1; // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526\n }\n &:-ms-input-placeholder { color: @color; } // Internet Explorer 10+\n &::-webkit-input-placeholder { color: @color; } // Safari and Chrome\n}\n\n// Transformations\n.scale(@ratio) {\n -webkit-transform: scale(@ratio);\n -ms-transform: scale(@ratio); // IE9 only\n -o-transform: scale(@ratio);\n transform: scale(@ratio);\n}\n.scale(@ratioX; @ratioY) {\n -webkit-transform: scale(@ratioX, @ratioY);\n -ms-transform: scale(@ratioX, @ratioY); // IE9 only\n -o-transform: scale(@ratioX, @ratioY);\n transform: scale(@ratioX, @ratioY);\n}\n.scaleX(@ratio) {\n -webkit-transform: scaleX(@ratio);\n -ms-transform: scaleX(@ratio); // IE9 only\n -o-transform: scaleX(@ratio);\n transform: scaleX(@ratio);\n}\n.scaleY(@ratio) {\n -webkit-transform: scaleY(@ratio);\n -ms-transform: scaleY(@ratio); // IE9 only\n -o-transform: scaleY(@ratio);\n transform: scaleY(@ratio);\n}\n.skew(@x; @y) {\n -webkit-transform: skewX(@x) skewY(@y);\n -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+\n -o-transform: skewX(@x) skewY(@y);\n transform: skewX(@x) skewY(@y);\n}\n.translate(@x; @y) {\n -webkit-transform: translate(@x, @y);\n -ms-transform: translate(@x, @y); // IE9 only\n -o-transform: translate(@x, @y);\n transform: translate(@x, @y);\n}\n.translate3d(@x; @y; @z) {\n -webkit-transform: translate3d(@x, @y, @z);\n transform: translate3d(@x, @y, @z);\n}\n.rotate(@degrees) {\n -webkit-transform: rotate(@degrees);\n -ms-transform: rotate(@degrees); // IE9 only\n -o-transform: rotate(@degrees);\n transform: rotate(@degrees);\n}\n.rotateX(@degrees) {\n -webkit-transform: rotateX(@degrees);\n -ms-transform: rotateX(@degrees); // IE9 only\n -o-transform: rotateX(@degrees);\n transform: rotateX(@degrees);\n}\n.rotateY(@degrees) {\n -webkit-transform: rotateY(@degrees);\n -ms-transform: rotateY(@degrees); // IE9 only\n -o-transform: rotateY(@degrees);\n transform: rotateY(@degrees);\n}\n.perspective(@perspective) {\n -webkit-perspective: @perspective;\n -moz-perspective: @perspective;\n perspective: @perspective;\n}\n.perspective-origin(@perspective) {\n -webkit-perspective-origin: @perspective;\n -moz-perspective-origin: @perspective;\n perspective-origin: @perspective;\n}\n.transform-origin(@origin) {\n -webkit-transform-origin: @origin;\n -moz-transform-origin: @origin;\n -ms-transform-origin: @origin; // IE9 only\n transform-origin: @origin;\n}\n\n\n// Transitions\n\n.transition(@transition) {\n -webkit-transition: @transition;\n -o-transition: @transition;\n transition: @transition;\n}\n.transition-property(@transition-property) {\n -webkit-transition-property: @transition-property;\n transition-property: @transition-property;\n}\n.transition-delay(@transition-delay) {\n -webkit-transition-delay: @transition-delay;\n transition-delay: @transition-delay;\n}\n.transition-duration(@transition-duration) {\n -webkit-transition-duration: @transition-duration;\n transition-duration: @transition-duration;\n}\n.transition-timing-function(@timing-function) {\n -webkit-transition-timing-function: @timing-function;\n transition-timing-function: @timing-function;\n}\n.transition-transform(@transition) {\n -webkit-transition: -webkit-transform @transition;\n -moz-transition: -moz-transform @transition;\n -o-transition: -o-transform @transition;\n transition: transform @transition;\n}\n\n\n// User select\n// For selecting text on the page\n\n.user-select(@select) {\n -webkit-user-select: @select;\n -moz-user-select: @select;\n -ms-user-select: @select; // IE10+\n user-select: @select;\n}\n","// Gradients\n\n#gradient {\n\n // Horizontal gradient, from left to right\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .horizontal(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Opera 12\n background-image: linear-gradient(to right, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n background-repeat: repeat-x;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\",argb(@start-color),argb(@end-color))); // IE9 and down\n }\n\n // Vertical gradient, from top to bottom\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .vertical(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Opera 12\n background-image: linear-gradient(to bottom, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n background-repeat: repeat-x;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\",argb(@start-color),argb(@end-color))); // IE9 and down\n }\n\n .directional(@start-color: #555; @end-color: #333; @deg: 45deg) {\n background-repeat: repeat-x;\n background-image: -webkit-linear-gradient(@deg, @start-color, @end-color); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(@deg, @start-color, @end-color); // Opera 12\n background-image: linear-gradient(@deg, @start-color, @end-color); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n }\n .horizontal-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color);\n background-image: -o-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(to right, @start-color, @mid-color @color-stop, @end-color);\n background-repeat: no-repeat;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n }\n .vertical-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-image: -o-linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-repeat: no-repeat;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n }\n .radial(@inner-color: #555; @outer-color: #333) {\n background-image: -webkit-radial-gradient(circle, @inner-color, @outer-color);\n background-image: radial-gradient(circle, @inner-color, @outer-color);\n background-repeat: no-repeat;\n }\n .striped(@color: rgba(255,255,255,.15); @angle: 45deg) {\n background-image: -webkit-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n background-image: linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n }\n}\n","// Reset filters for IE\n//\n// When you need to remove a gradient background, do not forget to use this to reset\n// the IE filter for IE9 and below.\n\n.reset-filter() {\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(enabled = false)\"));\n}\n"]} \ No newline at end of file diff --git a/themes/boots/bootstrap/css/bootstrap-theme.min.css b/themes/boots/bootstrap/css/bootstrap-theme.min.css new file mode 100644 index 000000000..dc95d8e4e --- /dev/null +++ b/themes/boots/bootstrap/css/bootstrap-theme.min.css @@ -0,0 +1,6 @@ +/*! + * Bootstrap v3.3.6 (http://getbootstrap.com) + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */.btn-danger,.btn-default,.btn-info,.btn-primary,.btn-success,.btn-warning{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-danger.active,.btn-danger:active,.btn-default.active,.btn-default:active,.btn-info.active,.btn-info:active,.btn-primary.active,.btn-primary:active,.btn-success.active,.btn-success:active,.btn-warning.active,.btn-warning:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-danger.disabled,.btn-danger[disabled],.btn-default.disabled,.btn-default[disabled],.btn-info.disabled,.btn-info[disabled],.btn-primary.disabled,.btn-primary[disabled],.btn-success.disabled,.btn-success[disabled],.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-danger,fieldset[disabled] .btn-default,fieldset[disabled] .btn-info,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-success,fieldset[disabled] .btn-warning{-webkit-box-shadow:none;box-shadow:none}.btn-danger .badge,.btn-default .badge,.btn-info .badge,.btn-primary .badge,.btn-success .badge,.btn-warning .badge{text-shadow:none}.btn.active,.btn:active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc}.btn-default:focus,.btn-default:hover{background-color:#e0e0e0;background-position:0 -15px}.btn-default.active,.btn-default:active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default.disabled,.btn-default.disabled.active,.btn-default.disabled.focus,.btn-default.disabled:active,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled],.btn-default[disabled].active,.btn-default[disabled].focus,.btn-default[disabled]:active,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default.active,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-o-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#265a88));background-image:linear-gradient(to bottom,#337ab7 0,#265a88 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#245580}.btn-primary:focus,.btn-primary:hover{background-color:#265a88;background-position:0 -15px}.btn-primary.active,.btn-primary:active{background-color:#265a88;border-color:#245580}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled.focus,.btn-primary.disabled:active,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled].focus,.btn-primary[disabled]:active,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary.active,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#265a88;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:focus,.btn-success:hover{background-color:#419641;background-position:0 -15px}.btn-success.active,.btn-success:active{background-color:#419641;border-color:#3e8f3e}.btn-success.disabled,.btn-success.disabled.active,.btn-success.disabled.focus,.btn-success.disabled:active,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled],.btn-success[disabled].active,.btn-success[disabled].focus,.btn-success[disabled]:active,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success.active,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:focus,.btn-info:hover{background-color:#2aabd2;background-position:0 -15px}.btn-info.active,.btn-info:active{background-color:#2aabd2;border-color:#28a4c9}.btn-info.disabled,.btn-info.disabled.active,.btn-info.disabled.focus,.btn-info.disabled:active,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled],.btn-info[disabled].active,.btn-info[disabled].focus,.btn-info[disabled]:active,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info.active,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:focus,.btn-warning:hover{background-color:#eb9316;background-position:0 -15px}.btn-warning.active,.btn-warning:active{background-color:#eb9316;border-color:#e38d13}.btn-warning.disabled,.btn-warning.disabled.active,.btn-warning.disabled.focus,.btn-warning.disabled:active,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled],.btn-warning[disabled].active,.btn-warning[disabled].focus,.btn-warning[disabled]:active,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning.active,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:focus,.btn-danger:hover{background-color:#c12e2a;background-position:0 -15px}.btn-danger.active,.btn-danger:active{background-color:#c12e2a;border-color:#b92c28}.btn-danger.disabled,.btn-danger.disabled.active,.btn-danger.disabled.focus,.btn-danger.disabled:active,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled],.btn-danger[disabled].active,.btn-danger[disabled].focus,.btn-danger[disabled]:active,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger.active,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#c12e2a;background-image:none}.img-thumbnail,.thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{background-color:#2e6da4;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-o-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dbdbdb),to(#e2e2e2));background-image:linear-gradient(to bottom,#dbdbdb 0,#e2e2e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-o-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#080808),to(#0f0f0f));background-image:linear-gradient(to bottom,#080808 0,#0f0f0f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0}@media (max-width:767px){.navbar .navbar-nav .open .dropdown-menu>.active>a,.navbar .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-o-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#286090));background-image:linear-gradient(to bottom,#337ab7 0,#286090 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{text-shadow:0 -1px 0 #286090;background-image:-webkit-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2b669a));background-image:linear-gradient(to bottom,#337ab7 0,#2b669a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);background-repeat:repeat-x;border-color:#2b669a}.list-group-item.active .badge,.list-group-item.active:focus .badge,.list-group-item.active:hover .badge{text-shadow:none}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} +/*# sourceMappingURL=bootstrap-theme.min.css.map */ \ No newline at end of file diff --git a/themes/boots/bootstrap/css/bootstrap-theme.min.css.map b/themes/boots/bootstrap/css/bootstrap-theme.min.css.map new file mode 100644 index 000000000..2c6b65afc --- /dev/null +++ b/themes/boots/bootstrap/css/bootstrap-theme.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["less/theme.less","less/mixins/vendor-prefixes.less","less/mixins/gradients.less","less/mixins/reset-filter.less"],"names":[],"mappings":";;;;AAmBA,YAAA,aAAA,UAAA,aAAA,aAAA,aAME,YAAA,EAAA,KAAA,EAAA,eC2CA,mBAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBDvCR,mBAAA,mBAAA,oBAAA,oBAAA,iBAAA,iBAAA,oBAAA,oBAAA,oBAAA,oBAAA,oBAAA,oBCsCA,mBAAA,MAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,iBDlCR,qBAAA,sBAAA,sBAAA,uBAAA,mBAAA,oBAAA,sBAAA,uBAAA,sBAAA,uBAAA,sBAAA,uBAAA,+BAAA,gCAAA,6BAAA,gCAAA,gCAAA,gCCiCA,mBAAA,KACQ,WAAA,KDlDV,mBAAA,oBAAA,iBAAA,oBAAA,oBAAA,oBAuBI,YAAA,KAyCF,YAAA,YAEE,iBAAA,KAKJ,aErEI,YAAA,EAAA,IAAA,EAAA,KACA,iBAAA,iDACA,iBAAA,4CAAA,iBAAA,qEAEA,iBAAA,+CCnBF,OAAA,+GH4CA,OAAA,0DACA,kBAAA,SAuC2C,aAAA,QAA2B,aAAA,KArCtE,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAgBN,aEtEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAiBN,aEvEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAkBN,UExEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,gBAAA,gBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,iBAAA,iBAEE,iBAAA,QACA,aAAA,QAMA,mBAAA,0BAAA,yBAAA,0BAAA,yBAAA,yBAAA,oBAAA,2BAAA,0BAAA,2BAAA,0BAAA,0BAAA,6BAAA,oCAAA,mCAAA,oCAAA,mCAAA,mCAME,iBAAA,QACA,iBAAA,KAmBN,aEzEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAoBN,YE1EI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,kBAAA,kBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,mBAAA,mBAEE,iBAAA,QACA,aAAA,QAMA,qBAAA,4BAAA,2BAAA,4BAAA,2BAAA,2BAAA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,+BAAA,sCAAA,qCAAA,sCAAA,qCAAA,qCAME,iBAAA,QACA,iBAAA,KA2BN,eAAA,WClCE,mBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,EAAA,IAAA,IAAA,iBD2CV,0BAAA,0BE3FI,iBAAA,QACA,iBAAA,oDACA,iBAAA,+CAAA,iBAAA,wEACA,iBAAA,kDACA,OAAA,+GF0FF,kBAAA,SAEF,yBAAA,+BAAA,+BEhGI,iBAAA,QACA,iBAAA,oDACA,iBAAA,+CAAA,iBAAA,wEACA,iBAAA,kDACA,OAAA,+GFgGF,kBAAA,SASF,gBE7GI,iBAAA,iDACA,iBAAA,4CACA,iBAAA,qEAAA,iBAAA,+CACA,OAAA,+GACA,OAAA,0DCnBF,kBAAA,SH+HA,cAAA,ICjEA,mBAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBD6DV,sCAAA,oCE7GI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SD2CF,mBAAA,MAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,iBD0EV,cAAA,iBAEE,YAAA,EAAA,IAAA,EAAA,sBAIF,gBEhII,iBAAA,iDACA,iBAAA,4CACA,iBAAA,qEAAA,iBAAA,+CACA,OAAA,+GACA,OAAA,0DCnBF,kBAAA,SHkJA,cAAA,IAHF,sCAAA,oCEhII,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SD2CF,mBAAA,MAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,gBDgFV,8BAAA,iCAYI,YAAA,EAAA,KAAA,EAAA,gBAKJ,qBAAA,kBAAA,mBAGE,cAAA,EAqBF,yBAfI,mDAAA,yDAAA,yDAGE,MAAA,KE7JF,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,UFqKJ,OACE,YAAA,EAAA,IAAA,EAAA,qBC3HA,mBAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,gBDsIV,eEtLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAKF,YEvLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAMF,eExLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAOF,cEzLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAeF,UEjMI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFuMJ,cE3MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFwMJ,sBE5MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFyMJ,mBE7MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF0MJ,sBE9MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF2MJ,qBE/MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF+MJ,sBElLI,iBAAA,yKACA,iBAAA,oKACA,iBAAA,iKFyLJ,YACE,cAAA,IC9KA,mBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,EAAA,IAAA,IAAA,iBDgLV,wBAAA,8BAAA,8BAGE,YAAA,EAAA,KAAA,EAAA,QEnOE,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFiOF,aAAA,QALF,+BAAA,qCAAA,qCAQI,YAAA,KAUJ,OCnME,mBAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,EAAA,IAAA,IAAA,gBD4MV,8BE5PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFyPJ,8BE7PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF0PJ,8BE9PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF2PJ,2BE/PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF4PJ,8BEhQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF6PJ,6BEjQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFoQJ,MExQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFsQF,aAAA,QC3NA,mBAAA,MAAA,EAAA,IAAA,IAAA,gBAAA,EAAA,IAAA,EAAA,qBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,gBAAA,EAAA,IAAA,EAAA"} \ No newline at end of file diff --git a/themes/boots/bootstrap/css/bootstrap.css b/themes/boots/bootstrap/css/bootstrap.css new file mode 100644 index 000000000..42c79d6e4 --- /dev/null +++ b/themes/boots/bootstrap/css/bootstrap.css @@ -0,0 +1,6760 @@ +/*! + * Bootstrap v3.3.6 (http://getbootstrap.com) + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ +html { + font-family: sans-serif; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} +body { + margin: 0; +} +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +menu, +nav, +section, +summary { + display: block; +} +audio, +canvas, +progress, +video { + display: inline-block; + vertical-align: baseline; +} +audio:not([controls]) { + display: none; + height: 0; +} +[hidden], +template { + display: none; +} +a { + background-color: transparent; +} +a:active, +a:hover { + outline: 0; +} +abbr[title] { + border-bottom: 1px dotted; +} +b, +strong { + font-weight: bold; +} +dfn { + font-style: italic; +} +h1 { + margin: .67em 0; + font-size: 2em; +} +mark { + color: #000; + background: #ff0; +} +small { + font-size: 80%; +} +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} +sup { + top: -.5em; +} +sub { + bottom: -.25em; +} +img { + border: 0; +} +svg:not(:root) { + overflow: hidden; +} +figure { + margin: 1em 40px; +} +hr { + height: 0; + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; +} +pre { + overflow: auto; +} +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} +button, +input, +optgroup, +select, +textarea { + margin: 0; + font: inherit; + color: inherit; +} +button { + overflow: visible; +} +button, +select { + text-transform: none; +} +button, +html input[type="button"], +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; + cursor: pointer; +} +button[disabled], +html input[disabled] { + cursor: default; +} +button::-moz-focus-inner, +input::-moz-focus-inner { + padding: 0; + border: 0; +} +input { + line-height: normal; +} +input[type="checkbox"], +input[type="radio"] { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + padding: 0; +} +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} +input[type="search"] { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + -webkit-appearance: textfield; +} +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} +fieldset { + padding: .35em .625em .75em; + margin: 0 2px; + border: 1px solid #c0c0c0; +} +legend { + padding: 0; + border: 0; +} +textarea { + overflow: auto; +} +optgroup { + font-weight: bold; +} +table { + border-spacing: 0; + border-collapse: collapse; +} +td, +th { + padding: 0; +} +/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */ +@media print { + *, + *:before, + *:after { + color: #000 !important; + text-shadow: none !important; + background: transparent !important; + -webkit-box-shadow: none !important; + box-shadow: none !important; + } + a, + a:visited { + text-decoration: underline; + } + a[href]:after { + content: " (" attr(href) ")"; + } + abbr[title]:after { + content: " (" attr(title) ")"; + } + a[href^="#"]:after, + a[href^="javascript:"]:after { + content: ""; + } + pre, + blockquote { + border: 1px solid #999; + + page-break-inside: avoid; + } + thead { + display: table-header-group; + } + tr, + img { + page-break-inside: avoid; + } + img { + max-width: 100% !important; + } + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + h2, + h3 { + page-break-after: avoid; + } + .navbar { + display: none; + } + .btn > .caret, + .dropup > .btn > .caret { + border-top-color: #000 !important; + } + .label { + border: 1px solid #000; + } + .table { + border-collapse: collapse !important; + } + .table td, + .table th { + background-color: #fff !important; + } + .table-bordered th, + .table-bordered td { + border: 1px solid #ddd !important; + } +} +@font-face { + font-family: 'Glyphicons Halflings'; + + src: url('../fonts/glyphicons-halflings-regular.eot'); + src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); +} +.glyphicon { + position: relative; + top: 1px; + display: inline-block; + font-family: 'Glyphicons Halflings'; + font-style: normal; + font-weight: normal; + line-height: 1; + + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +.glyphicon-asterisk:before { + content: "\002a"; +} +.glyphicon-plus:before { + content: "\002b"; +} +.glyphicon-euro:before, +.glyphicon-eur:before { + content: "\20ac"; +} +.glyphicon-minus:before { + content: "\2212"; +} +.glyphicon-cloud:before { + content: "\2601"; +} +.glyphicon-envelope:before { + content: "\2709"; +} +.glyphicon-pencil:before { + content: "\270f"; +} +.glyphicon-glass:before { + content: "\e001"; +} +.glyphicon-music:before { + content: "\e002"; +} +.glyphicon-search:before { + content: "\e003"; +} +.glyphicon-heart:before { + content: "\e005"; +} +.glyphicon-star:before { + content: "\e006"; +} +.glyphicon-star-empty:before { + content: "\e007"; +} +.glyphicon-user:before { + content: "\e008"; +} +.glyphicon-film:before { + content: "\e009"; +} +.glyphicon-th-large:before { + content: "\e010"; +} +.glyphicon-th:before { + content: "\e011"; +} +.glyphicon-th-list:before { + content: "\e012"; +} +.glyphicon-ok:before { + content: "\e013"; +} +.glyphicon-remove:before { + content: "\e014"; +} +.glyphicon-zoom-in:before { + content: "\e015"; +} +.glyphicon-zoom-out:before { + content: "\e016"; +} +.glyphicon-off:before { + content: "\e017"; +} +.glyphicon-signal:before { + content: "\e018"; +} +.glyphicon-cog:before { + content: "\e019"; +} +.glyphicon-trash:before { + content: "\e020"; +} +.glyphicon-home:before { + content: "\e021"; +} +.glyphicon-file:before { + content: "\e022"; +} +.glyphicon-time:before { + content: "\e023"; +} +.glyphicon-road:before { + content: "\e024"; +} +.glyphicon-download-alt:before { + content: "\e025"; +} +.glyphicon-download:before { + content: "\e026"; +} +.glyphicon-upload:before { + content: "\e027"; +} +.glyphicon-inbox:before { + content: "\e028"; +} +.glyphicon-play-circle:before { + content: "\e029"; +} +.glyphicon-repeat:before { + content: "\e030"; +} +.glyphicon-refresh:before { + content: "\e031"; +} +.glyphicon-list-alt:before { + content: "\e032"; +} +.glyphicon-lock:before { + content: "\e033"; +} +.glyphicon-flag:before { + content: "\e034"; +} +.glyphicon-headphones:before { + content: "\e035"; +} +.glyphicon-volume-off:before { + content: "\e036"; +} +.glyphicon-volume-down:before { + content: "\e037"; +} +.glyphicon-volume-up:before { + content: "\e038"; +} +.glyphicon-qrcode:before { + content: "\e039"; +} +.glyphicon-barcode:before { + content: "\e040"; +} +.glyphicon-tag:before { + content: "\e041"; +} +.glyphicon-tags:before { + content: "\e042"; +} +.glyphicon-book:before { + content: "\e043"; +} +.glyphicon-bookmark:before { + content: "\e044"; +} +.glyphicon-print:before { + content: "\e045"; +} +.glyphicon-camera:before { + content: "\e046"; +} +.glyphicon-font:before { + content: "\e047"; +} +.glyphicon-bold:before { + content: "\e048"; +} +.glyphicon-italic:before { + content: "\e049"; +} +.glyphicon-text-height:before { + content: "\e050"; +} +.glyphicon-text-width:before { + content: "\e051"; +} +.glyphicon-align-left:before { + content: "\e052"; +} +.glyphicon-align-center:before { + content: "\e053"; +} +.glyphicon-align-right:before { + content: "\e054"; +} +.glyphicon-align-justify:before { + content: "\e055"; +} +.glyphicon-list:before { + content: "\e056"; +} +.glyphicon-indent-left:before { + content: "\e057"; +} +.glyphicon-indent-right:before { + content: "\e058"; +} +.glyphicon-facetime-video:before { + content: "\e059"; +} +.glyphicon-picture:before { + content: "\e060"; +} +.glyphicon-map-marker:before { + content: "\e062"; +} +.glyphicon-adjust:before { + content: "\e063"; +} +.glyphicon-tint:before { + content: "\e064"; +} +.glyphicon-edit:before { + content: "\e065"; +} +.glyphicon-share:before { + content: "\e066"; +} +.glyphicon-check:before { + content: "\e067"; +} +.glyphicon-move:before { + content: "\e068"; +} +.glyphicon-step-backward:before { + content: "\e069"; +} +.glyphicon-fast-backward:before { + content: "\e070"; +} +.glyphicon-backward:before { + content: "\e071"; +} +.glyphicon-play:before { + content: "\e072"; +} +.glyphicon-pause:before { + content: "\e073"; +} +.glyphicon-stop:before { + content: "\e074"; +} +.glyphicon-forward:before { + content: "\e075"; +} +.glyphicon-fast-forward:before { + content: "\e076"; +} +.glyphicon-step-forward:before { + content: "\e077"; +} +.glyphicon-eject:before { + content: "\e078"; +} +.glyphicon-chevron-left:before { + content: "\e079"; +} +.glyphicon-chevron-right:before { + content: "\e080"; +} +.glyphicon-plus-sign:before { + content: "\e081"; +} +.glyphicon-minus-sign:before { + content: "\e082"; +} +.glyphicon-remove-sign:before { + content: "\e083"; +} +.glyphicon-ok-sign:before { + content: "\e084"; +} +.glyphicon-question-sign:before { + content: "\e085"; +} +.glyphicon-info-sign:before { + content: "\e086"; +} +.glyphicon-screenshot:before { + content: "\e087"; +} +.glyphicon-remove-circle:before { + content: "\e088"; +} +.glyphicon-ok-circle:before { + content: "\e089"; +} +.glyphicon-ban-circle:before { + content: "\e090"; +} +.glyphicon-arrow-left:before { + content: "\e091"; +} +.glyphicon-arrow-right:before { + content: "\e092"; +} +.glyphicon-arrow-up:before { + content: "\e093"; +} +.glyphicon-arrow-down:before { + content: "\e094"; +} +.glyphicon-share-alt:before { + content: "\e095"; +} +.glyphicon-resize-full:before { + content: "\e096"; +} +.glyphicon-resize-small:before { + content: "\e097"; +} +.glyphicon-exclamation-sign:before { + content: "\e101"; +} +.glyphicon-gift:before { + content: "\e102"; +} +.glyphicon-leaf:before { + content: "\e103"; +} +.glyphicon-fire:before { + content: "\e104"; +} +.glyphicon-eye-open:before { + content: "\e105"; +} +.glyphicon-eye-close:before { + content: "\e106"; +} +.glyphicon-warning-sign:before { + content: "\e107"; +} +.glyphicon-plane:before { + content: "\e108"; +} +.glyphicon-calendar:before { + content: "\e109"; +} +.glyphicon-random:before { + content: "\e110"; +} +.glyphicon-comment:before { + content: "\e111"; +} +.glyphicon-magnet:before { + content: "\e112"; +} +.glyphicon-chevron-up:before { + content: "\e113"; +} +.glyphicon-chevron-down:before { + content: "\e114"; +} +.glyphicon-retweet:before { + content: "\e115"; +} +.glyphicon-shopping-cart:before { + content: "\e116"; +} +.glyphicon-folder-close:before { + content: "\e117"; +} +.glyphicon-folder-open:before { + content: "\e118"; +} +.glyphicon-resize-vertical:before { + content: "\e119"; +} +.glyphicon-resize-horizontal:before { + content: "\e120"; +} +.glyphicon-hdd:before { + content: "\e121"; +} +.glyphicon-bullhorn:before { + content: "\e122"; +} +.glyphicon-bell:before { + content: "\e123"; +} +.glyphicon-certificate:before { + content: "\e124"; +} +.glyphicon-thumbs-up:before { + content: "\e125"; +} +.glyphicon-thumbs-down:before { + content: "\e126"; +} +.glyphicon-hand-right:before { + content: "\e127"; +} +.glyphicon-hand-left:before { + content: "\e128"; +} +.glyphicon-hand-up:before { + content: "\e129"; +} +.glyphicon-hand-down:before { + content: "\e130"; +} +.glyphicon-circle-arrow-right:before { + content: "\e131"; +} +.glyphicon-circle-arrow-left:before { + content: "\e132"; +} +.glyphicon-circle-arrow-up:before { + content: "\e133"; +} +.glyphicon-circle-arrow-down:before { + content: "\e134"; +} +.glyphicon-globe:before { + content: "\e135"; +} +.glyphicon-wrench:before { + content: "\e136"; +} +.glyphicon-tasks:before { + content: "\e137"; +} +.glyphicon-filter:before { + content: "\e138"; +} +.glyphicon-briefcase:before { + content: "\e139"; +} +.glyphicon-fullscreen:before { + content: "\e140"; +} +.glyphicon-dashboard:before { + content: "\e141"; +} +.glyphicon-paperclip:before { + content: "\e142"; +} +.glyphicon-heart-empty:before { + content: "\e143"; +} +.glyphicon-link:before { + content: "\e144"; +} +.glyphicon-phone:before { + content: "\e145"; +} +.glyphicon-pushpin:before { + content: "\e146"; +} +.glyphicon-usd:before { + content: "\e148"; +} +.glyphicon-gbp:before { + content: "\e149"; +} +.glyphicon-sort:before { + content: "\e150"; +} +.glyphicon-sort-by-alphabet:before { + content: "\e151"; +} +.glyphicon-sort-by-alphabet-alt:before { + content: "\e152"; +} +.glyphicon-sort-by-order:before { + content: "\e153"; +} +.glyphicon-sort-by-order-alt:before { + content: "\e154"; +} +.glyphicon-sort-by-attributes:before { + content: "\e155"; +} +.glyphicon-sort-by-attributes-alt:before { + content: "\e156"; +} +.glyphicon-unchecked:before { + content: "\e157"; +} +.glyphicon-expand:before { + content: "\e158"; +} +.glyphicon-collapse-down:before { + content: "\e159"; +} +.glyphicon-collapse-up:before { + content: "\e160"; +} +.glyphicon-log-in:before { + content: "\e161"; +} +.glyphicon-flash:before { + content: "\e162"; +} +.glyphicon-log-out:before { + content: "\e163"; +} +.glyphicon-new-window:before { + content: "\e164"; +} +.glyphicon-record:before { + content: "\e165"; +} +.glyphicon-save:before { + content: "\e166"; +} +.glyphicon-open:before { + content: "\e167"; +} +.glyphicon-saved:before { + content: "\e168"; +} +.glyphicon-import:before { + content: "\e169"; +} +.glyphicon-export:before { + content: "\e170"; +} +.glyphicon-send:before { + content: "\e171"; +} +.glyphicon-floppy-disk:before { + content: "\e172"; +} +.glyphicon-floppy-saved:before { + content: "\e173"; +} +.glyphicon-floppy-remove:before { + content: "\e174"; +} +.glyphicon-floppy-save:before { + content: "\e175"; +} +.glyphicon-floppy-open:before { + content: "\e176"; +} +.glyphicon-credit-card:before { + content: "\e177"; +} +.glyphicon-transfer:before { + content: "\e178"; +} +.glyphicon-cutlery:before { + content: "\e179"; +} +.glyphicon-header:before { + content: "\e180"; +} +.glyphicon-compressed:before { + content: "\e181"; +} +.glyphicon-earphone:before { + content: "\e182"; +} +.glyphicon-phone-alt:before { + content: "\e183"; +} +.glyphicon-tower:before { + content: "\e184"; +} +.glyphicon-stats:before { + content: "\e185"; +} +.glyphicon-sd-video:before { + content: "\e186"; +} +.glyphicon-hd-video:before { + content: "\e187"; +} +.glyphicon-subtitles:before { + content: "\e188"; +} +.glyphicon-sound-stereo:before { + content: "\e189"; +} +.glyphicon-sound-dolby:before { + content: "\e190"; +} +.glyphicon-sound-5-1:before { + content: "\e191"; +} +.glyphicon-sound-6-1:before { + content: "\e192"; +} +.glyphicon-sound-7-1:before { + content: "\e193"; +} +.glyphicon-copyright-mark:before { + content: "\e194"; +} +.glyphicon-registration-mark:before { + content: "\e195"; +} +.glyphicon-cloud-download:before { + content: "\e197"; +} +.glyphicon-cloud-upload:before { + content: "\e198"; +} +.glyphicon-tree-conifer:before { + content: "\e199"; +} +.glyphicon-tree-deciduous:before { + content: "\e200"; +} +.glyphicon-cd:before { + content: "\e201"; +} +.glyphicon-save-file:before { + content: "\e202"; +} +.glyphicon-open-file:before { + content: "\e203"; +} +.glyphicon-level-up:before { + content: "\e204"; +} +.glyphicon-copy:before { + content: "\e205"; +} +.glyphicon-paste:before { + content: "\e206"; +} +.glyphicon-alert:before { + content: "\e209"; +} +.glyphicon-equalizer:before { + content: "\e210"; +} +.glyphicon-king:before { + content: "\e211"; +} +.glyphicon-queen:before { + content: "\e212"; +} +.glyphicon-pawn:before { + content: "\e213"; +} +.glyphicon-bishop:before { + content: "\e214"; +} +.glyphicon-knight:before { + content: "\e215"; +} +.glyphicon-baby-formula:before { + content: "\e216"; +} +.glyphicon-tent:before { + content: "\26fa"; +} +.glyphicon-blackboard:before { + content: "\e218"; +} +.glyphicon-bed:before { + content: "\e219"; +} +.glyphicon-apple:before { + content: "\f8ff"; +} +.glyphicon-erase:before { + content: "\e221"; +} +.glyphicon-hourglass:before { + content: "\231b"; +} +.glyphicon-lamp:before { + content: "\e223"; +} +.glyphicon-duplicate:before { + content: "\e224"; +} +.glyphicon-piggy-bank:before { + content: "\e225"; +} +.glyphicon-scissors:before { + content: "\e226"; +} +.glyphicon-bitcoin:before { + content: "\e227"; +} +.glyphicon-btc:before { + content: "\e227"; +} +.glyphicon-xbt:before { + content: "\e227"; +} +.glyphicon-yen:before { + content: "\00a5"; +} +.glyphicon-jpy:before { + content: "\00a5"; +} +.glyphicon-ruble:before { + content: "\20bd"; +} +.glyphicon-rub:before { + content: "\20bd"; +} +.glyphicon-scale:before { + content: "\e230"; +} +.glyphicon-ice-lolly:before { + content: "\e231"; +} +.glyphicon-ice-lolly-tasted:before { + content: "\e232"; +} +.glyphicon-education:before { + content: "\e233"; +} +.glyphicon-option-horizontal:before { + content: "\e234"; +} +.glyphicon-option-vertical:before { + content: "\e235"; +} +.glyphicon-menu-hamburger:before { + content: "\e236"; +} +.glyphicon-modal-window:before { + content: "\e237"; +} +.glyphicon-oil:before { + content: "\e238"; +} +.glyphicon-grain:before { + content: "\e239"; +} +.glyphicon-sunglasses:before { + content: "\e240"; +} +.glyphicon-text-size:before { + content: "\e241"; +} +.glyphicon-text-color:before { + content: "\e242"; +} +.glyphicon-text-background:before { + content: "\e243"; +} +.glyphicon-object-align-top:before { + content: "\e244"; +} +.glyphicon-object-align-bottom:before { + content: "\e245"; +} +.glyphicon-object-align-horizontal:before { + content: "\e246"; +} +.glyphicon-object-align-left:before { + content: "\e247"; +} +.glyphicon-object-align-vertical:before { + content: "\e248"; +} +.glyphicon-object-align-right:before { + content: "\e249"; +} +.glyphicon-triangle-right:before { + content: "\e250"; +} +.glyphicon-triangle-left:before { + content: "\e251"; +} +.glyphicon-triangle-bottom:before { + content: "\e252"; +} +.glyphicon-triangle-top:before { + content: "\e253"; +} +.glyphicon-console:before { + content: "\e254"; +} +.glyphicon-superscript:before { + content: "\e255"; +} +.glyphicon-subscript:before { + content: "\e256"; +} +.glyphicon-menu-left:before { + content: "\e257"; +} +.glyphicon-menu-right:before { + content: "\e258"; +} +.glyphicon-menu-down:before { + content: "\e259"; +} +.glyphicon-menu-up:before { + content: "\e260"; +} +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +*:before, +*:after { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +html { + font-size: 10px; + + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} +body { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 1.42857143; + color: #333; + background-color: #fff; +} +input, +button, +select, +textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; +} +a { + color: #337ab7; + text-decoration: none; +} +a:hover, +a:focus { + color: #23527c; + text-decoration: underline; +} +a:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +figure { + margin: 0; +} +img { + vertical-align: middle; +} +.img-responsive, +.thumbnail > img, +.thumbnail a > img, +.carousel-inner > .item > img, +.carousel-inner > .item > a > img { + display: block; + max-width: 100%; + height: auto; +} +.img-rounded { + border-radius: 6px; +} +.img-thumbnail { + display: inline-block; + max-width: 100%; + height: auto; + padding: 4px; + line-height: 1.42857143; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 4px; + -webkit-transition: all .2s ease-in-out; + -o-transition: all .2s ease-in-out; + transition: all .2s ease-in-out; +} +.img-circle { + border-radius: 50%; +} +hr { + margin-top: 20px; + margin-bottom: 20px; + border: 0; + border-top: 1px solid #eee; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} +.sr-only-focusable:active, +.sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; +} +[role="button"] { + cursor: pointer; +} +h1, +h2, +h3, +h4, +h5, +h6, +.h1, +.h2, +.h3, +.h4, +.h5, +.h6 { + font-family: inherit; + font-weight: 500; + line-height: 1.1; + color: inherit; +} +h1 small, +h2 small, +h3 small, +h4 small, +h5 small, +h6 small, +.h1 small, +.h2 small, +.h3 small, +.h4 small, +.h5 small, +.h6 small, +h1 .small, +h2 .small, +h3 .small, +h4 .small, +h5 .small, +h6 .small, +.h1 .small, +.h2 .small, +.h3 .small, +.h4 .small, +.h5 .small, +.h6 .small { + font-weight: normal; + line-height: 1; + color: #777; +} +h1, +.h1, +h2, +.h2, +h3, +.h3 { + margin-top: 20px; + margin-bottom: 10px; +} +h1 small, +.h1 small, +h2 small, +.h2 small, +h3 small, +.h3 small, +h1 .small, +.h1 .small, +h2 .small, +.h2 .small, +h3 .small, +.h3 .small { + font-size: 65%; +} +h4, +.h4, +h5, +.h5, +h6, +.h6 { + margin-top: 10px; + margin-bottom: 10px; +} +h4 small, +.h4 small, +h5 small, +.h5 small, +h6 small, +.h6 small, +h4 .small, +.h4 .small, +h5 .small, +.h5 .small, +h6 .small, +.h6 .small { + font-size: 75%; +} +h1, +.h1 { + font-size: 36px; +} +h2, +.h2 { + font-size: 30px; +} +h3, +.h3 { + font-size: 24px; +} +h4, +.h4 { + font-size: 18px; +} +h5, +.h5 { + font-size: 14px; +} +h6, +.h6 { + font-size: 12px; +} +p { + margin: 0 0 10px; +} +.lead { + margin-bottom: 20px; + font-size: 16px; + font-weight: 300; + line-height: 1.4; +} +@media (min-width: 768px) { + .lead { + font-size: 21px; + } +} +small, +.small { + font-size: 85%; +} +mark, +.mark { + padding: .2em; + background-color: #fcf8e3; +} +.text-left { + text-align: left; +} +.text-right { + text-align: right; +} +.text-center { + text-align: center; +} +.text-justify { + text-align: justify; +} +.text-nowrap { + white-space: nowrap; +} +.text-lowercase { + text-transform: lowercase; +} +.text-uppercase { + text-transform: uppercase; +} +.text-capitalize { + text-transform: capitalize; +} +.text-muted { + color: #777; +} +.text-primary { + color: #337ab7; +} +a.text-primary:hover, +a.text-primary:focus { + color: #286090; +} +.text-success { + color: #3c763d; +} +a.text-success:hover, +a.text-success:focus { + color: #2b542c; +} +.text-info { + color: #31708f; +} +a.text-info:hover, +a.text-info:focus { + color: #245269; +} +.text-warning { + color: #8a6d3b; +} +a.text-warning:hover, +a.text-warning:focus { + color: #66512c; +} +.text-danger { + color: #a94442; +} +a.text-danger:hover, +a.text-danger:focus { + color: #843534; +} +.bg-primary { + color: #fff; + background-color: #337ab7; +} +a.bg-primary:hover, +a.bg-primary:focus { + background-color: #286090; +} +.bg-success { + background-color: #dff0d8; +} +a.bg-success:hover, +a.bg-success:focus { + background-color: #c1e2b3; +} +.bg-info { + background-color: #d9edf7; +} +a.bg-info:hover, +a.bg-info:focus { + background-color: #afd9ee; +} +.bg-warning { + background-color: #fcf8e3; +} +a.bg-warning:hover, +a.bg-warning:focus { + background-color: #f7ecb5; +} +.bg-danger { + background-color: #f2dede; +} +a.bg-danger:hover, +a.bg-danger:focus { + background-color: #e4b9b9; +} +.page-header { + padding-bottom: 9px; + margin: 40px 0 20px; + border-bottom: 1px solid #eee; +} +ul, +ol { + margin-top: 0; + margin-bottom: 10px; +} +ul ul, +ol ul, +ul ol, +ol ol { + margin-bottom: 0; +} +.list-unstyled { + padding-left: 0; + list-style: none; +} +.list-inline { + padding-left: 0; + margin-left: -5px; + list-style: none; +} +.list-inline > li { + display: inline-block; + padding-right: 5px; + padding-left: 5px; +} +dl { + margin-top: 0; + margin-bottom: 20px; +} +dt, +dd { + line-height: 1.42857143; +} +dt { + font-weight: bold; +} +dd { + margin-left: 0; +} +@media (min-width: 768px) { + .dl-horizontal dt { + float: left; + width: 160px; + overflow: hidden; + clear: left; + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; + } + .dl-horizontal dd { + margin-left: 180px; + } +} +abbr[title], +abbr[data-original-title] { + cursor: help; + border-bottom: 1px dotted #777; +} +.initialism { + font-size: 90%; + text-transform: uppercase; +} +blockquote { + padding: 10px 20px; + margin: 0 0 20px; + font-size: 17.5px; + border-left: 5px solid #eee; +} +blockquote p:last-child, +blockquote ul:last-child, +blockquote ol:last-child { + margin-bottom: 0; +} +blockquote footer, +blockquote small, +blockquote .small { + display: block; + font-size: 80%; + line-height: 1.42857143; + color: #777; +} +blockquote footer:before, +blockquote small:before, +blockquote .small:before { + content: '\2014 \00A0'; +} +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + text-align: right; + border-right: 5px solid #eee; + border-left: 0; +} +.blockquote-reverse footer:before, +blockquote.pull-right footer:before, +.blockquote-reverse small:before, +blockquote.pull-right small:before, +.blockquote-reverse .small:before, +blockquote.pull-right .small:before { + content: ''; +} +.blockquote-reverse footer:after, +blockquote.pull-right footer:after, +.blockquote-reverse small:after, +blockquote.pull-right small:after, +.blockquote-reverse .small:after, +blockquote.pull-right .small:after { + content: '\00A0 \2014'; +} +address { + margin-bottom: 20px; + font-style: normal; + line-height: 1.42857143; +} +code, +kbd, +pre, +samp { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; +} +code { + padding: 2px 4px; + font-size: 90%; + color: #c7254e; + background-color: #f9f2f4; + border-radius: 4px; +} +kbd { + padding: 2px 4px; + font-size: 90%; + color: #fff; + background-color: #333; + border-radius: 3px; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25); +} +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: bold; + -webkit-box-shadow: none; + box-shadow: none; +} +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 1.42857143; + color: #333; + word-break: break-all; + word-wrap: break-word; + background-color: #f5f5f5; + border: 1px solid #ccc; + border-radius: 4px; +} +pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; +} +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} +.container { + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} +@media (min-width: 768px) { + .container { + width: 750px; + } +} +@media (min-width: 992px) { + .container { + width: 970px; + } +} +@media (min-width: 1200px) { + .container { + width: 1170px; + } +} +.container-fluid { + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} +.row { + margin-right: -15px; + margin-left: -15px; +} +.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; +} +.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 { + float: left; +} +.col-xs-12 { + width: 100%; +} +.col-xs-11 { + width: 91.66666667%; +} +.col-xs-10 { + width: 83.33333333%; +} +.col-xs-9 { + width: 75%; +} +.col-xs-8 { + width: 66.66666667%; +} +.col-xs-7 { + width: 58.33333333%; +} +.col-xs-6 { + width: 50%; +} +.col-xs-5 { + width: 41.66666667%; +} +.col-xs-4 { + width: 33.33333333%; +} +.col-xs-3 { + width: 25%; +} +.col-xs-2 { + width: 16.66666667%; +} +.col-xs-1 { + width: 8.33333333%; +} +.col-xs-pull-12 { + right: 100%; +} +.col-xs-pull-11 { + right: 91.66666667%; +} +.col-xs-pull-10 { + right: 83.33333333%; +} +.col-xs-pull-9 { + right: 75%; +} +.col-xs-pull-8 { + right: 66.66666667%; +} +.col-xs-pull-7 { + right: 58.33333333%; +} +.col-xs-pull-6 { + right: 50%; +} +.col-xs-pull-5 { + right: 41.66666667%; +} +.col-xs-pull-4 { + right: 33.33333333%; +} +.col-xs-pull-3 { + right: 25%; +} +.col-xs-pull-2 { + right: 16.66666667%; +} +.col-xs-pull-1 { + right: 8.33333333%; +} +.col-xs-pull-0 { + right: auto; +} +.col-xs-push-12 { + left: 100%; +} +.col-xs-push-11 { + left: 91.66666667%; +} +.col-xs-push-10 { + left: 83.33333333%; +} +.col-xs-push-9 { + left: 75%; +} +.col-xs-push-8 { + left: 66.66666667%; +} +.col-xs-push-7 { + left: 58.33333333%; +} +.col-xs-push-6 { + left: 50%; +} +.col-xs-push-5 { + left: 41.66666667%; +} +.col-xs-push-4 { + left: 33.33333333%; +} +.col-xs-push-3 { + left: 25%; +} +.col-xs-push-2 { + left: 16.66666667%; +} +.col-xs-push-1 { + left: 8.33333333%; +} +.col-xs-push-0 { + left: auto; +} +.col-xs-offset-12 { + margin-left: 100%; +} +.col-xs-offset-11 { + margin-left: 91.66666667%; +} +.col-xs-offset-10 { + margin-left: 83.33333333%; +} +.col-xs-offset-9 { + margin-left: 75%; +} +.col-xs-offset-8 { + margin-left: 66.66666667%; +} +.col-xs-offset-7 { + margin-left: 58.33333333%; +} +.col-xs-offset-6 { + margin-left: 50%; +} +.col-xs-offset-5 { + margin-left: 41.66666667%; +} +.col-xs-offset-4 { + margin-left: 33.33333333%; +} +.col-xs-offset-3 { + margin-left: 25%; +} +.col-xs-offset-2 { + margin-left: 16.66666667%; +} +.col-xs-offset-1 { + margin-left: 8.33333333%; +} +.col-xs-offset-0 { + margin-left: 0; +} +@media (min-width: 768px) { + .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 { + float: left; + } + .col-sm-12 { + width: 100%; + } + .col-sm-11 { + width: 91.66666667%; + } + .col-sm-10 { + width: 83.33333333%; + } + .col-sm-9 { + width: 75%; + } + .col-sm-8 { + width: 66.66666667%; + } + .col-sm-7 { + width: 58.33333333%; + } + .col-sm-6 { + width: 50%; + } + .col-sm-5 { + width: 41.66666667%; + } + .col-sm-4 { + width: 33.33333333%; + } + .col-sm-3 { + width: 25%; + } + .col-sm-2 { + width: 16.66666667%; + } + .col-sm-1 { + width: 8.33333333%; + } + .col-sm-pull-12 { + right: 100%; + } + .col-sm-pull-11 { + right: 91.66666667%; + } + .col-sm-pull-10 { + right: 83.33333333%; + } + .col-sm-pull-9 { + right: 75%; + } + .col-sm-pull-8 { + right: 66.66666667%; + } + .col-sm-pull-7 { + right: 58.33333333%; + } + .col-sm-pull-6 { + right: 50%; + } + .col-sm-pull-5 { + right: 41.66666667%; + } + .col-sm-pull-4 { + right: 33.33333333%; + } + .col-sm-pull-3 { + right: 25%; + } + .col-sm-pull-2 { + right: 16.66666667%; + } + .col-sm-pull-1 { + right: 8.33333333%; + } + .col-sm-pull-0 { + right: auto; + } + .col-sm-push-12 { + left: 100%; + } + .col-sm-push-11 { + left: 91.66666667%; + } + .col-sm-push-10 { + left: 83.33333333%; + } + .col-sm-push-9 { + left: 75%; + } + .col-sm-push-8 { + left: 66.66666667%; + } + .col-sm-push-7 { + left: 58.33333333%; + } + .col-sm-push-6 { + left: 50%; + } + .col-sm-push-5 { + left: 41.66666667%; + } + .col-sm-push-4 { + left: 33.33333333%; + } + .col-sm-push-3 { + left: 25%; + } + .col-sm-push-2 { + left: 16.66666667%; + } + .col-sm-push-1 { + left: 8.33333333%; + } + .col-sm-push-0 { + left: auto; + } + .col-sm-offset-12 { + margin-left: 100%; + } + .col-sm-offset-11 { + margin-left: 91.66666667%; + } + .col-sm-offset-10 { + margin-left: 83.33333333%; + } + .col-sm-offset-9 { + margin-left: 75%; + } + .col-sm-offset-8 { + margin-left: 66.66666667%; + } + .col-sm-offset-7 { + margin-left: 58.33333333%; + } + .col-sm-offset-6 { + margin-left: 50%; + } + .col-sm-offset-5 { + margin-left: 41.66666667%; + } + .col-sm-offset-4 { + margin-left: 33.33333333%; + } + .col-sm-offset-3 { + margin-left: 25%; + } + .col-sm-offset-2 { + margin-left: 16.66666667%; + } + .col-sm-offset-1 { + margin-left: 8.33333333%; + } + .col-sm-offset-0 { + margin-left: 0; + } +} +@media (min-width: 992px) { + .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 { + float: left; + } + .col-md-12 { + width: 100%; + } + .col-md-11 { + width: 91.66666667%; + } + .col-md-10 { + width: 83.33333333%; + } + .col-md-9 { + width: 75%; + } + .col-md-8 { + width: 66.66666667%; + } + .col-md-7 { + width: 58.33333333%; + } + .col-md-6 { + width: 50%; + } + .col-md-5 { + width: 41.66666667%; + } + .col-md-4 { + width: 33.33333333%; + } + .col-md-3 { + width: 25%; + } + .col-md-2 { + width: 16.66666667%; + } + .col-md-1 { + width: 8.33333333%; + } + .col-md-pull-12 { + right: 100%; + } + .col-md-pull-11 { + right: 91.66666667%; + } + .col-md-pull-10 { + right: 83.33333333%; + } + .col-md-pull-9 { + right: 75%; + } + .col-md-pull-8 { + right: 66.66666667%; + } + .col-md-pull-7 { + right: 58.33333333%; + } + .col-md-pull-6 { + right: 50%; + } + .col-md-pull-5 { + right: 41.66666667%; + } + .col-md-pull-4 { + right: 33.33333333%; + } + .col-md-pull-3 { + right: 25%; + } + .col-md-pull-2 { + right: 16.66666667%; + } + .col-md-pull-1 { + right: 8.33333333%; + } + .col-md-pull-0 { + right: auto; + } + .col-md-push-12 { + left: 100%; + } + .col-md-push-11 { + left: 91.66666667%; + } + .col-md-push-10 { + left: 83.33333333%; + } + .col-md-push-9 { + left: 75%; + } + .col-md-push-8 { + left: 66.66666667%; + } + .col-md-push-7 { + left: 58.33333333%; + } + .col-md-push-6 { + left: 50%; + } + .col-md-push-5 { + left: 41.66666667%; + } + .col-md-push-4 { + left: 33.33333333%; + } + .col-md-push-3 { + left: 25%; + } + .col-md-push-2 { + left: 16.66666667%; + } + .col-md-push-1 { + left: 8.33333333%; + } + .col-md-push-0 { + left: auto; + } + .col-md-offset-12 { + margin-left: 100%; + } + .col-md-offset-11 { + margin-left: 91.66666667%; + } + .col-md-offset-10 { + margin-left: 83.33333333%; + } + .col-md-offset-9 { + margin-left: 75%; + } + .col-md-offset-8 { + margin-left: 66.66666667%; + } + .col-md-offset-7 { + margin-left: 58.33333333%; + } + .col-md-offset-6 { + margin-left: 50%; + } + .col-md-offset-5 { + margin-left: 41.66666667%; + } + .col-md-offset-4 { + margin-left: 33.33333333%; + } + .col-md-offset-3 { + margin-left: 25%; + } + .col-md-offset-2 { + margin-left: 16.66666667%; + } + .col-md-offset-1 { + margin-left: 8.33333333%; + } + .col-md-offset-0 { + margin-left: 0; + } +} +@media (min-width: 1200px) { + .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 { + float: left; + } + .col-lg-12 { + width: 100%; + } + .col-lg-11 { + width: 91.66666667%; + } + .col-lg-10 { + width: 83.33333333%; + } + .col-lg-9 { + width: 75%; + } + .col-lg-8 { + width: 66.66666667%; + } + .col-lg-7 { + width: 58.33333333%; + } + .col-lg-6 { + width: 50%; + } + .col-lg-5 { + width: 41.66666667%; + } + .col-lg-4 { + width: 33.33333333%; + } + .col-lg-3 { + width: 25%; + } + .col-lg-2 { + width: 16.66666667%; + } + .col-lg-1 { + width: 8.33333333%; + } + .col-lg-pull-12 { + right: 100%; + } + .col-lg-pull-11 { + right: 91.66666667%; + } + .col-lg-pull-10 { + right: 83.33333333%; + } + .col-lg-pull-9 { + right: 75%; + } + .col-lg-pull-8 { + right: 66.66666667%; + } + .col-lg-pull-7 { + right: 58.33333333%; + } + .col-lg-pull-6 { + right: 50%; + } + .col-lg-pull-5 { + right: 41.66666667%; + } + .col-lg-pull-4 { + right: 33.33333333%; + } + .col-lg-pull-3 { + right: 25%; + } + .col-lg-pull-2 { + right: 16.66666667%; + } + .col-lg-pull-1 { + right: 8.33333333%; + } + .col-lg-pull-0 { + right: auto; + } + .col-lg-push-12 { + left: 100%; + } + .col-lg-push-11 { + left: 91.66666667%; + } + .col-lg-push-10 { + left: 83.33333333%; + } + .col-lg-push-9 { + left: 75%; + } + .col-lg-push-8 { + left: 66.66666667%; + } + .col-lg-push-7 { + left: 58.33333333%; + } + .col-lg-push-6 { + left: 50%; + } + .col-lg-push-5 { + left: 41.66666667%; + } + .col-lg-push-4 { + left: 33.33333333%; + } + .col-lg-push-3 { + left: 25%; + } + .col-lg-push-2 { + left: 16.66666667%; + } + .col-lg-push-1 { + left: 8.33333333%; + } + .col-lg-push-0 { + left: auto; + } + .col-lg-offset-12 { + margin-left: 100%; + } + .col-lg-offset-11 { + margin-left: 91.66666667%; + } + .col-lg-offset-10 { + margin-left: 83.33333333%; + } + .col-lg-offset-9 { + margin-left: 75%; + } + .col-lg-offset-8 { + margin-left: 66.66666667%; + } + .col-lg-offset-7 { + margin-left: 58.33333333%; + } + .col-lg-offset-6 { + margin-left: 50%; + } + .col-lg-offset-5 { + margin-left: 41.66666667%; + } + .col-lg-offset-4 { + margin-left: 33.33333333%; + } + .col-lg-offset-3 { + margin-left: 25%; + } + .col-lg-offset-2 { + margin-left: 16.66666667%; + } + .col-lg-offset-1 { + margin-left: 8.33333333%; + } + .col-lg-offset-0 { + margin-left: 0; + } +} +table { + background-color: transparent; +} +caption { + padding-top: 8px; + padding-bottom: 8px; + color: #777; + text-align: left; +} +th { + text-align: left; +} +.table { + width: 100%; + max-width: 100%; + margin-bottom: 20px; +} +.table > thead > tr > th, +.table > tbody > tr > th, +.table > tfoot > tr > th, +.table > thead > tr > td, +.table > tbody > tr > td, +.table > tfoot > tr > td { + padding: 8px; + line-height: 1.42857143; + vertical-align: top; + border-top: 1px solid #ddd; +} +.table > thead > tr > th { + vertical-align: bottom; + border-bottom: 2px solid #ddd; +} +.table > caption + thead > tr:first-child > th, +.table > colgroup + thead > tr:first-child > th, +.table > thead:first-child > tr:first-child > th, +.table > caption + thead > tr:first-child > td, +.table > colgroup + thead > tr:first-child > td, +.table > thead:first-child > tr:first-child > td { + border-top: 0; +} +.table > tbody + tbody { + border-top: 2px solid #ddd; +} +.table .table { + background-color: #fff; +} +.table-condensed > thead > tr > th, +.table-condensed > tbody > tr > th, +.table-condensed > tfoot > tr > th, +.table-condensed > thead > tr > td, +.table-condensed > tbody > tr > td, +.table-condensed > tfoot > tr > td { + padding: 5px; +} +.table-bordered { + border: 1px solid #ddd; +} +.table-bordered > thead > tr > th, +.table-bordered > tbody > tr > th, +.table-bordered > tfoot > tr > th, +.table-bordered > thead > tr > td, +.table-bordered > tbody > tr > td, +.table-bordered > tfoot > tr > td { + border: 1px solid #ddd; +} +.table-bordered > thead > tr > th, +.table-bordered > thead > tr > td { + border-bottom-width: 2px; +} +.table-striped > tbody > tr:nth-of-type(odd) { + background-color: #f9f9f9; +} +.table-hover > tbody > tr:hover { + background-color: #f5f5f5; +} +table col[class*="col-"] { + position: static; + display: table-column; + float: none; +} +table td[class*="col-"], +table th[class*="col-"] { + position: static; + display: table-cell; + float: none; +} +.table > thead > tr > td.active, +.table > tbody > tr > td.active, +.table > tfoot > tr > td.active, +.table > thead > tr > th.active, +.table > tbody > tr > th.active, +.table > tfoot > tr > th.active, +.table > thead > tr.active > td, +.table > tbody > tr.active > td, +.table > tfoot > tr.active > td, +.table > thead > tr.active > th, +.table > tbody > tr.active > th, +.table > tfoot > tr.active > th { + background-color: #f5f5f5; +} +.table-hover > tbody > tr > td.active:hover, +.table-hover > tbody > tr > th.active:hover, +.table-hover > tbody > tr.active:hover > td, +.table-hover > tbody > tr:hover > .active, +.table-hover > tbody > tr.active:hover > th { + background-color: #e8e8e8; +} +.table > thead > tr > td.success, +.table > tbody > tr > td.success, +.table > tfoot > tr > td.success, +.table > thead > tr > th.success, +.table > tbody > tr > th.success, +.table > tfoot > tr > th.success, +.table > thead > tr.success > td, +.table > tbody > tr.success > td, +.table > tfoot > tr.success > td, +.table > thead > tr.success > th, +.table > tbody > tr.success > th, +.table > tfoot > tr.success > th { + background-color: #dff0d8; +} +.table-hover > tbody > tr > td.success:hover, +.table-hover > tbody > tr > th.success:hover, +.table-hover > tbody > tr.success:hover > td, +.table-hover > tbody > tr:hover > .success, +.table-hover > tbody > tr.success:hover > th { + background-color: #d0e9c6; +} +.table > thead > tr > td.info, +.table > tbody > tr > td.info, +.table > tfoot > tr > td.info, +.table > thead > tr > th.info, +.table > tbody > tr > th.info, +.table > tfoot > tr > th.info, +.table > thead > tr.info > td, +.table > tbody > tr.info > td, +.table > tfoot > tr.info > td, +.table > thead > tr.info > th, +.table > tbody > tr.info > th, +.table > tfoot > tr.info > th { + background-color: #d9edf7; +} +.table-hover > tbody > tr > td.info:hover, +.table-hover > tbody > tr > th.info:hover, +.table-hover > tbody > tr.info:hover > td, +.table-hover > tbody > tr:hover > .info, +.table-hover > tbody > tr.info:hover > th { + background-color: #c4e3f3; +} +.table > thead > tr > td.warning, +.table > tbody > tr > td.warning, +.table > tfoot > tr > td.warning, +.table > thead > tr > th.warning, +.table > tbody > tr > th.warning, +.table > tfoot > tr > th.warning, +.table > thead > tr.warning > td, +.table > tbody > tr.warning > td, +.table > tfoot > tr.warning > td, +.table > thead > tr.warning > th, +.table > tbody > tr.warning > th, +.table > tfoot > tr.warning > th { + background-color: #fcf8e3; +} +.table-hover > tbody > tr > td.warning:hover, +.table-hover > tbody > tr > th.warning:hover, +.table-hover > tbody > tr.warning:hover > td, +.table-hover > tbody > tr:hover > .warning, +.table-hover > tbody > tr.warning:hover > th { + background-color: #faf2cc; +} +.table > thead > tr > td.danger, +.table > tbody > tr > td.danger, +.table > tfoot > tr > td.danger, +.table > thead > tr > th.danger, +.table > tbody > tr > th.danger, +.table > tfoot > tr > th.danger, +.table > thead > tr.danger > td, +.table > tbody > tr.danger > td, +.table > tfoot > tr.danger > td, +.table > thead > tr.danger > th, +.table > tbody > tr.danger > th, +.table > tfoot > tr.danger > th { + background-color: #f2dede; +} +.table-hover > tbody > tr > td.danger:hover, +.table-hover > tbody > tr > th.danger:hover, +.table-hover > tbody > tr.danger:hover > td, +.table-hover > tbody > tr:hover > .danger, +.table-hover > tbody > tr.danger:hover > th { + background-color: #ebcccc; +} +.table-responsive { + min-height: .01%; + overflow-x: auto; +} +@media screen and (max-width: 767px) { + .table-responsive { + width: 100%; + margin-bottom: 15px; + overflow-y: hidden; + -ms-overflow-style: -ms-autohiding-scrollbar; + border: 1px solid #ddd; + } + .table-responsive > .table { + margin-bottom: 0; + } + .table-responsive > .table > thead > tr > th, + .table-responsive > .table > tbody > tr > th, + .table-responsive > .table > tfoot > tr > th, + .table-responsive > .table > thead > tr > td, + .table-responsive > .table > tbody > tr > td, + .table-responsive > .table > tfoot > tr > td { + white-space: nowrap; + } + .table-responsive > .table-bordered { + border: 0; + } + .table-responsive > .table-bordered > thead > tr > th:first-child, + .table-responsive > .table-bordered > tbody > tr > th:first-child, + .table-responsive > .table-bordered > tfoot > tr > th:first-child, + .table-responsive > .table-bordered > thead > tr > td:first-child, + .table-responsive > .table-bordered > tbody > tr > td:first-child, + .table-responsive > .table-bordered > tfoot > tr > td:first-child { + border-left: 0; + } + .table-responsive > .table-bordered > thead > tr > th:last-child, + .table-responsive > .table-bordered > tbody > tr > th:last-child, + .table-responsive > .table-bordered > tfoot > tr > th:last-child, + .table-responsive > .table-bordered > thead > tr > td:last-child, + .table-responsive > .table-bordered > tbody > tr > td:last-child, + .table-responsive > .table-bordered > tfoot > tr > td:last-child { + border-right: 0; + } + .table-responsive > .table-bordered > tbody > tr:last-child > th, + .table-responsive > .table-bordered > tfoot > tr:last-child > th, + .table-responsive > .table-bordered > tbody > tr:last-child > td, + .table-responsive > .table-bordered > tfoot > tr:last-child > td { + border-bottom: 0; + } +} +fieldset { + min-width: 0; + padding: 0; + margin: 0; + border: 0; +} +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 20px; + font-size: 21px; + line-height: inherit; + color: #333; + border: 0; + border-bottom: 1px solid #e5e5e5; +} +label { + display: inline-block; + max-width: 100%; + margin-bottom: 5px; + font-weight: bold; +} +input[type="search"] { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +input[type="radio"], +input[type="checkbox"] { + margin: 4px 0 0; + margin-top: 1px \9; + line-height: normal; +} +input[type="file"] { + display: block; +} +input[type="range"] { + display: block; + width: 100%; +} +select[multiple], +select[size] { + height: auto; +} +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +output { + display: block; + padding-top: 7px; + font-size: 14px; + line-height: 1.42857143; + color: #555; +} +.form-control { + display: block; + width: 100%; + height: 34px; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857143; + color: #555; + background-color: #fff; + background-image: none; + border: 1px solid #ccc; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; + -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; +} +.form-control:focus { + border-color: #66afe9; + outline: 0; + -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6); + box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6); +} +.form-control::-moz-placeholder { + color: #999; + opacity: 1; +} +.form-control:-ms-input-placeholder { + color: #999; +} +.form-control::-webkit-input-placeholder { + color: #999; +} +.form-control::-ms-expand { + background-color: transparent; + border: 0; +} +.form-control[disabled], +.form-control[readonly], +fieldset[disabled] .form-control { + background-color: #eee; + opacity: 1; +} +.form-control[disabled], +fieldset[disabled] .form-control { + cursor: not-allowed; +} +textarea.form-control { + height: auto; +} +input[type="search"] { + -webkit-appearance: none; +} +@media screen and (-webkit-min-device-pixel-ratio: 0) { + input[type="date"].form-control, + input[type="time"].form-control, + input[type="datetime-local"].form-control, + input[type="month"].form-control { + line-height: 34px; + } + input[type="date"].input-sm, + input[type="time"].input-sm, + input[type="datetime-local"].input-sm, + input[type="month"].input-sm, + .input-group-sm input[type="date"], + .input-group-sm input[type="time"], + .input-group-sm input[type="datetime-local"], + .input-group-sm input[type="month"] { + line-height: 30px; + } + input[type="date"].input-lg, + input[type="time"].input-lg, + input[type="datetime-local"].input-lg, + input[type="month"].input-lg, + .input-group-lg input[type="date"], + .input-group-lg input[type="time"], + .input-group-lg input[type="datetime-local"], + .input-group-lg input[type="month"] { + line-height: 46px; + } +} +.form-group { + margin-bottom: 15px; +} +.radio, +.checkbox { + position: relative; + display: block; + margin-top: 10px; + margin-bottom: 10px; +} +.radio label, +.checkbox label { + min-height: 20px; + padding-left: 20px; + margin-bottom: 0; + font-weight: normal; + cursor: pointer; +} +.radio input[type="radio"], +.radio-inline input[type="radio"], +.checkbox input[type="checkbox"], +.checkbox-inline input[type="checkbox"] { + position: absolute; + margin-top: 4px \9; + margin-left: -20px; +} +.radio + .radio, +.checkbox + .checkbox { + margin-top: -5px; +} +.radio-inline, +.checkbox-inline { + position: relative; + display: inline-block; + padding-left: 20px; + margin-bottom: 0; + font-weight: normal; + vertical-align: middle; + cursor: pointer; +} +.radio-inline + .radio-inline, +.checkbox-inline + .checkbox-inline { + margin-top: 0; + margin-left: 10px; +} +input[type="radio"][disabled], +input[type="checkbox"][disabled], +input[type="radio"].disabled, +input[type="checkbox"].disabled, +fieldset[disabled] input[type="radio"], +fieldset[disabled] input[type="checkbox"] { + cursor: not-allowed; +} +.radio-inline.disabled, +.checkbox-inline.disabled, +fieldset[disabled] .radio-inline, +fieldset[disabled] .checkbox-inline { + cursor: not-allowed; +} +.radio.disabled label, +.checkbox.disabled label, +fieldset[disabled] .radio label, +fieldset[disabled] .checkbox label { + cursor: not-allowed; +} +.form-control-static { + min-height: 34px; + padding-top: 7px; + padding-bottom: 7px; + margin-bottom: 0; +} +.form-control-static.input-lg, +.form-control-static.input-sm { + padding-right: 0; + padding-left: 0; +} +.input-sm { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +select.input-sm { + height: 30px; + line-height: 30px; +} +textarea.input-sm, +select[multiple].input-sm { + height: auto; +} +.form-group-sm .form-control { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +.form-group-sm select.form-control { + height: 30px; + line-height: 30px; +} +.form-group-sm textarea.form-control, +.form-group-sm select[multiple].form-control { + height: auto; +} +.form-group-sm .form-control-static { + height: 30px; + min-height: 32px; + padding: 6px 10px; + font-size: 12px; + line-height: 1.5; +} +.input-lg { + height: 46px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; + border-radius: 6px; +} +select.input-lg { + height: 46px; + line-height: 46px; +} +textarea.input-lg, +select[multiple].input-lg { + height: auto; +} +.form-group-lg .form-control { + height: 46px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; + border-radius: 6px; +} +.form-group-lg select.form-control { + height: 46px; + line-height: 46px; +} +.form-group-lg textarea.form-control, +.form-group-lg select[multiple].form-control { + height: auto; +} +.form-group-lg .form-control-static { + height: 46px; + min-height: 38px; + padding: 11px 16px; + font-size: 18px; + line-height: 1.3333333; +} +.has-feedback { + position: relative; +} +.has-feedback .form-control { + padding-right: 42.5px; +} +.form-control-feedback { + position: absolute; + top: 0; + right: 0; + z-index: 2; + display: block; + width: 34px; + height: 34px; + line-height: 34px; + text-align: center; + pointer-events: none; +} +.input-lg + .form-control-feedback, +.input-group-lg + .form-control-feedback, +.form-group-lg .form-control + .form-control-feedback { + width: 46px; + height: 46px; + line-height: 46px; +} +.input-sm + .form-control-feedback, +.input-group-sm + .form-control-feedback, +.form-group-sm .form-control + .form-control-feedback { + width: 30px; + height: 30px; + line-height: 30px; +} +.has-success .help-block, +.has-success .control-label, +.has-success .radio, +.has-success .checkbox, +.has-success .radio-inline, +.has-success .checkbox-inline, +.has-success.radio label, +.has-success.checkbox label, +.has-success.radio-inline label, +.has-success.checkbox-inline label { + color: #3c763d; +} +.has-success .form-control { + border-color: #3c763d; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); +} +.has-success .form-control:focus { + border-color: #2b542c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168; +} +.has-success .input-group-addon { + color: #3c763d; + background-color: #dff0d8; + border-color: #3c763d; +} +.has-success .form-control-feedback { + color: #3c763d; +} +.has-warning .help-block, +.has-warning .control-label, +.has-warning .radio, +.has-warning .checkbox, +.has-warning .radio-inline, +.has-warning .checkbox-inline, +.has-warning.radio label, +.has-warning.checkbox label, +.has-warning.radio-inline label, +.has-warning.checkbox-inline label { + color: #8a6d3b; +} +.has-warning .form-control { + border-color: #8a6d3b; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); +} +.has-warning .form-control:focus { + border-color: #66512c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b; +} +.has-warning .input-group-addon { + color: #8a6d3b; + background-color: #fcf8e3; + border-color: #8a6d3b; +} +.has-warning .form-control-feedback { + color: #8a6d3b; +} +.has-error .help-block, +.has-error .control-label, +.has-error .radio, +.has-error .checkbox, +.has-error .radio-inline, +.has-error .checkbox-inline, +.has-error.radio label, +.has-error.checkbox label, +.has-error.radio-inline label, +.has-error.checkbox-inline label { + color: #a94442; +} +.has-error .form-control { + border-color: #a94442; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); +} +.has-error .form-control:focus { + border-color: #843534; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483; +} +.has-error .input-group-addon { + color: #a94442; + background-color: #f2dede; + border-color: #a94442; +} +.has-error .form-control-feedback { + color: #a94442; +} +.has-feedback label ~ .form-control-feedback { + top: 25px; +} +.has-feedback label.sr-only ~ .form-control-feedback { + top: 0; +} +.help-block { + display: block; + margin-top: 5px; + margin-bottom: 10px; + color: #737373; +} +@media (min-width: 768px) { + .form-inline .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .form-control { + display: inline-block; + width: auto; + vertical-align: middle; + } + .form-inline .form-control-static { + display: inline-block; + } + .form-inline .input-group { + display: inline-table; + vertical-align: middle; + } + .form-inline .input-group .input-group-addon, + .form-inline .input-group .input-group-btn, + .form-inline .input-group .form-control { + width: auto; + } + .form-inline .input-group > .form-control { + width: 100%; + } + .form-inline .control-label { + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .radio, + .form-inline .checkbox { + display: inline-block; + margin-top: 0; + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .radio label, + .form-inline .checkbox label { + padding-left: 0; + } + .form-inline .radio input[type="radio"], + .form-inline .checkbox input[type="checkbox"] { + position: relative; + margin-left: 0; + } + .form-inline .has-feedback .form-control-feedback { + top: 0; + } +} +.form-horizontal .radio, +.form-horizontal .checkbox, +.form-horizontal .radio-inline, +.form-horizontal .checkbox-inline { + padding-top: 7px; + margin-top: 0; + margin-bottom: 0; +} +.form-horizontal .radio, +.form-horizontal .checkbox { + min-height: 27px; +} +.form-horizontal .form-group { + margin-right: -15px; + margin-left: -15px; +} +@media (min-width: 768px) { + .form-horizontal .control-label { + padding-top: 7px; + margin-bottom: 0; + text-align: right; + } +} +.form-horizontal .has-feedback .form-control-feedback { + right: 15px; +} +@media (min-width: 768px) { + .form-horizontal .form-group-lg .control-label { + padding-top: 11px; + font-size: 18px; + } +} +@media (min-width: 768px) { + .form-horizontal .form-group-sm .control-label { + padding-top: 6px; + font-size: 12px; + } +} +.btn { + display: inline-block; + padding: 6px 12px; + margin-bottom: 0; + font-size: 14px; + font-weight: normal; + line-height: 1.42857143; + text-align: center; + white-space: nowrap; + vertical-align: middle; + -ms-touch-action: manipulation; + touch-action: manipulation; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-image: none; + border: 1px solid transparent; + border-radius: 4px; +} +.btn:focus, +.btn:active:focus, +.btn.active:focus, +.btn.focus, +.btn:active.focus, +.btn.active.focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +.btn:hover, +.btn:focus, +.btn.focus { + color: #333; + text-decoration: none; +} +.btn:active, +.btn.active { + background-image: none; + outline: 0; + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); +} +.btn.disabled, +.btn[disabled], +fieldset[disabled] .btn { + cursor: not-allowed; + filter: alpha(opacity=65); + -webkit-box-shadow: none; + box-shadow: none; + opacity: .65; +} +a.btn.disabled, +fieldset[disabled] a.btn { + pointer-events: none; +} +.btn-default { + color: #333; + background-color: #fff; + border-color: #ccc; +} +.btn-default:focus, +.btn-default.focus { + color: #333; + background-color: #e6e6e6; + border-color: #8c8c8c; +} +.btn-default:hover { + color: #333; + background-color: #e6e6e6; + border-color: #adadad; +} +.btn-default:active, +.btn-default.active, +.open > .dropdown-toggle.btn-default { + color: #333; + background-color: #e6e6e6; + border-color: #adadad; +} +.btn-default:active:hover, +.btn-default.active:hover, +.open > .dropdown-toggle.btn-default:hover, +.btn-default:active:focus, +.btn-default.active:focus, +.open > .dropdown-toggle.btn-default:focus, +.btn-default:active.focus, +.btn-default.active.focus, +.open > .dropdown-toggle.btn-default.focus { + color: #333; + background-color: #d4d4d4; + border-color: #8c8c8c; +} +.btn-default:active, +.btn-default.active, +.open > .dropdown-toggle.btn-default { + background-image: none; +} +.btn-default.disabled:hover, +.btn-default[disabled]:hover, +fieldset[disabled] .btn-default:hover, +.btn-default.disabled:focus, +.btn-default[disabled]:focus, +fieldset[disabled] .btn-default:focus, +.btn-default.disabled.focus, +.btn-default[disabled].focus, +fieldset[disabled] .btn-default.focus { + background-color: #fff; + border-color: #ccc; +} +.btn-default .badge { + color: #fff; + background-color: #333; +} +.btn-primary { + color: #fff; + background-color: #337ab7; + border-color: #2e6da4; +} +.btn-primary:focus, +.btn-primary.focus { + color: #fff; + background-color: #286090; + border-color: #122b40; +} +.btn-primary:hover { + color: #fff; + background-color: #286090; + border-color: #204d74; +} +.btn-primary:active, +.btn-primary.active, +.open > .dropdown-toggle.btn-primary { + color: #fff; + background-color: #286090; + border-color: #204d74; +} +.btn-primary:active:hover, +.btn-primary.active:hover, +.open > .dropdown-toggle.btn-primary:hover, +.btn-primary:active:focus, +.btn-primary.active:focus, +.open > .dropdown-toggle.btn-primary:focus, +.btn-primary:active.focus, +.btn-primary.active.focus, +.open > .dropdown-toggle.btn-primary.focus { + color: #fff; + background-color: #204d74; + border-color: #122b40; +} +.btn-primary:active, +.btn-primary.active, +.open > .dropdown-toggle.btn-primary { + background-image: none; +} +.btn-primary.disabled:hover, +.btn-primary[disabled]:hover, +fieldset[disabled] .btn-primary:hover, +.btn-primary.disabled:focus, +.btn-primary[disabled]:focus, +fieldset[disabled] .btn-primary:focus, +.btn-primary.disabled.focus, +.btn-primary[disabled].focus, +fieldset[disabled] .btn-primary.focus { + background-color: #337ab7; + border-color: #2e6da4; +} +.btn-primary .badge { + color: #337ab7; + background-color: #fff; +} +.btn-success { + color: #fff; + background-color: #5cb85c; + border-color: #4cae4c; +} +.btn-success:focus, +.btn-success.focus { + color: #fff; + background-color: #449d44; + border-color: #255625; +} +.btn-success:hover { + color: #fff; + background-color: #449d44; + border-color: #398439; +} +.btn-success:active, +.btn-success.active, +.open > .dropdown-toggle.btn-success { + color: #fff; + background-color: #449d44; + border-color: #398439; +} +.btn-success:active:hover, +.btn-success.active:hover, +.open > .dropdown-toggle.btn-success:hover, +.btn-success:active:focus, +.btn-success.active:focus, +.open > .dropdown-toggle.btn-success:focus, +.btn-success:active.focus, +.btn-success.active.focus, +.open > .dropdown-toggle.btn-success.focus { + color: #fff; + background-color: #398439; + border-color: #255625; +} +.btn-success:active, +.btn-success.active, +.open > .dropdown-toggle.btn-success { + background-image: none; +} +.btn-success.disabled:hover, +.btn-success[disabled]:hover, +fieldset[disabled] .btn-success:hover, +.btn-success.disabled:focus, +.btn-success[disabled]:focus, +fieldset[disabled] .btn-success:focus, +.btn-success.disabled.focus, +.btn-success[disabled].focus, +fieldset[disabled] .btn-success.focus { + background-color: #5cb85c; + border-color: #4cae4c; +} +.btn-success .badge { + color: #5cb85c; + background-color: #fff; +} +.btn-info { + color: #fff; + background-color: #5bc0de; + border-color: #46b8da; +} +.btn-info:focus, +.btn-info.focus { + color: #fff; + background-color: #31b0d5; + border-color: #1b6d85; +} +.btn-info:hover { + color: #fff; + background-color: #31b0d5; + border-color: #269abc; +} +.btn-info:active, +.btn-info.active, +.open > .dropdown-toggle.btn-info { + color: #fff; + background-color: #31b0d5; + border-color: #269abc; +} +.btn-info:active:hover, +.btn-info.active:hover, +.open > .dropdown-toggle.btn-info:hover, +.btn-info:active:focus, +.btn-info.active:focus, +.open > .dropdown-toggle.btn-info:focus, +.btn-info:active.focus, +.btn-info.active.focus, +.open > .dropdown-toggle.btn-info.focus { + color: #fff; + background-color: #269abc; + border-color: #1b6d85; +} +.btn-info:active, +.btn-info.active, +.open > .dropdown-toggle.btn-info { + background-image: none; +} +.btn-info.disabled:hover, +.btn-info[disabled]:hover, +fieldset[disabled] .btn-info:hover, +.btn-info.disabled:focus, +.btn-info[disabled]:focus, +fieldset[disabled] .btn-info:focus, +.btn-info.disabled.focus, +.btn-info[disabled].focus, +fieldset[disabled] .btn-info.focus { + background-color: #5bc0de; + border-color: #46b8da; +} +.btn-info .badge { + color: #5bc0de; + background-color: #fff; +} +.btn-warning { + color: #fff; + background-color: #f0ad4e; + border-color: #eea236; +} +.btn-warning:focus, +.btn-warning.focus { + color: #fff; + background-color: #ec971f; + border-color: #985f0d; +} +.btn-warning:hover { + color: #fff; + background-color: #ec971f; + border-color: #d58512; +} +.btn-warning:active, +.btn-warning.active, +.open > .dropdown-toggle.btn-warning { + color: #fff; + background-color: #ec971f; + border-color: #d58512; +} +.btn-warning:active:hover, +.btn-warning.active:hover, +.open > .dropdown-toggle.btn-warning:hover, +.btn-warning:active:focus, +.btn-warning.active:focus, +.open > .dropdown-toggle.btn-warning:focus, +.btn-warning:active.focus, +.btn-warning.active.focus, +.open > .dropdown-toggle.btn-warning.focus { + color: #fff; + background-color: #d58512; + border-color: #985f0d; +} +.btn-warning:active, +.btn-warning.active, +.open > .dropdown-toggle.btn-warning { + background-image: none; +} +.btn-warning.disabled:hover, +.btn-warning[disabled]:hover, +fieldset[disabled] .btn-warning:hover, +.btn-warning.disabled:focus, +.btn-warning[disabled]:focus, +fieldset[disabled] .btn-warning:focus, +.btn-warning.disabled.focus, +.btn-warning[disabled].focus, +fieldset[disabled] .btn-warning.focus { + background-color: #f0ad4e; + border-color: #eea236; +} +.btn-warning .badge { + color: #f0ad4e; + background-color: #fff; +} +.btn-danger { + color: #fff; + background-color: #d9534f; + border-color: #d43f3a; +} +.btn-danger:focus, +.btn-danger.focus { + color: #fff; + background-color: #c9302c; + border-color: #761c19; +} +.btn-danger:hover { + color: #fff; + background-color: #c9302c; + border-color: #ac2925; +} +.btn-danger:active, +.btn-danger.active, +.open > .dropdown-toggle.btn-danger { + color: #fff; + background-color: #c9302c; + border-color: #ac2925; +} +.btn-danger:active:hover, +.btn-danger.active:hover, +.open > .dropdown-toggle.btn-danger:hover, +.btn-danger:active:focus, +.btn-danger.active:focus, +.open > .dropdown-toggle.btn-danger:focus, +.btn-danger:active.focus, +.btn-danger.active.focus, +.open > .dropdown-toggle.btn-danger.focus { + color: #fff; + background-color: #ac2925; + border-color: #761c19; +} +.btn-danger:active, +.btn-danger.active, +.open > .dropdown-toggle.btn-danger { + background-image: none; +} +.btn-danger.disabled:hover, +.btn-danger[disabled]:hover, +fieldset[disabled] .btn-danger:hover, +.btn-danger.disabled:focus, +.btn-danger[disabled]:focus, +fieldset[disabled] .btn-danger:focus, +.btn-danger.disabled.focus, +.btn-danger[disabled].focus, +fieldset[disabled] .btn-danger.focus { + background-color: #d9534f; + border-color: #d43f3a; +} +.btn-danger .badge { + color: #d9534f; + background-color: #fff; +} +.btn-link { + font-weight: normal; + color: #337ab7; + border-radius: 0; +} +.btn-link, +.btn-link:active, +.btn-link.active, +.btn-link[disabled], +fieldset[disabled] .btn-link { + background-color: transparent; + -webkit-box-shadow: none; + box-shadow: none; +} +.btn-link, +.btn-link:hover, +.btn-link:focus, +.btn-link:active { + border-color: transparent; +} +.btn-link:hover, +.btn-link:focus { + color: #23527c; + text-decoration: underline; + background-color: transparent; +} +.btn-link[disabled]:hover, +fieldset[disabled] .btn-link:hover, +.btn-link[disabled]:focus, +fieldset[disabled] .btn-link:focus { + color: #777; + text-decoration: none; +} +.btn-lg, +.btn-group-lg > .btn { + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; + border-radius: 6px; +} +.btn-sm, +.btn-group-sm > .btn { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +.btn-xs, +.btn-group-xs > .btn { + padding: 1px 5px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +.btn-block { + display: block; + width: 100%; +} +.btn-block + .btn-block { + margin-top: 5px; +} +input[type="submit"].btn-block, +input[type="reset"].btn-block, +input[type="button"].btn-block { + width: 100%; +} +.fade { + opacity: 0; + -webkit-transition: opacity .15s linear; + -o-transition: opacity .15s linear; + transition: opacity .15s linear; +} +.fade.in { + opacity: 1; +} +.collapse { + display: none; +} +.collapse.in { + display: block; +} +tr.collapse.in { + display: table-row; +} +tbody.collapse.in { + display: table-row-group; +} +.collapsing { + position: relative; + height: 0; + overflow: hidden; + -webkit-transition-timing-function: ease; + -o-transition-timing-function: ease; + transition-timing-function: ease; + -webkit-transition-duration: .35s; + -o-transition-duration: .35s; + transition-duration: .35s; + -webkit-transition-property: height, visibility; + -o-transition-property: height, visibility; + transition-property: height, visibility; +} +.caret { + display: inline-block; + width: 0; + height: 0; + margin-left: 2px; + vertical-align: middle; + border-top: 4px dashed; + border-top: 4px solid \9; + border-right: 4px solid transparent; + border-left: 4px solid transparent; +} +.dropup, +.dropdown { + position: relative; +} +.dropdown-toggle:focus { + outline: 0; +} +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + font-size: 14px; + text-align: left; + list-style: none; + background-color: #fff; + -webkit-background-clip: padding-box; + background-clip: padding-box; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, .15); + border-radius: 4px; + -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175); + box-shadow: 0 6px 12px rgba(0, 0, 0, .175); +} +.dropdown-menu.pull-right { + right: 0; + left: auto; +} +.dropdown-menu .divider { + height: 1px; + margin: 9px 0; + overflow: hidden; + background-color: #e5e5e5; +} +.dropdown-menu > li > a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 1.42857143; + color: #333; + white-space: nowrap; +} +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus { + color: #262626; + text-decoration: none; + background-color: #f5f5f5; +} +.dropdown-menu > .active > a, +.dropdown-menu > .active > a:hover, +.dropdown-menu > .active > a:focus { + color: #fff; + text-decoration: none; + background-color: #337ab7; + outline: 0; +} +.dropdown-menu > .disabled > a, +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + color: #777; +} +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + text-decoration: none; + cursor: not-allowed; + background-color: transparent; + background-image: none; + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); +} +.open > .dropdown-menu { + display: block; +} +.open > a { + outline: 0; +} +.dropdown-menu-right { + right: 0; + left: auto; +} +.dropdown-menu-left { + right: auto; + left: 0; +} +.dropdown-header { + display: block; + padding: 3px 20px; + font-size: 12px; + line-height: 1.42857143; + color: #777; + white-space: nowrap; +} +.dropdown-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 990; +} +.pull-right > .dropdown-menu { + right: 0; + left: auto; +} +.dropup .caret, +.navbar-fixed-bottom .dropdown .caret { + content: ""; + border-top: 0; + border-bottom: 4px dashed; + border-bottom: 4px solid \9; +} +.dropup .dropdown-menu, +.navbar-fixed-bottom .dropdown .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 2px; +} +@media (min-width: 768px) { + .navbar-right .dropdown-menu { + right: 0; + left: auto; + } + .navbar-right .dropdown-menu-left { + right: auto; + left: 0; + } +} +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-block; + vertical-align: middle; +} +.btn-group > .btn, +.btn-group-vertical > .btn { + position: relative; + float: left; +} +.btn-group > .btn:hover, +.btn-group-vertical > .btn:hover, +.btn-group > .btn:focus, +.btn-group-vertical > .btn:focus, +.btn-group > .btn:active, +.btn-group-vertical > .btn:active, +.btn-group > .btn.active, +.btn-group-vertical > .btn.active { + z-index: 2; +} +.btn-group .btn + .btn, +.btn-group .btn + .btn-group, +.btn-group .btn-group + .btn, +.btn-group .btn-group + .btn-group { + margin-left: -1px; +} +.btn-toolbar { + margin-left: -5px; +} +.btn-toolbar .btn, +.btn-toolbar .btn-group, +.btn-toolbar .input-group { + float: left; +} +.btn-toolbar > .btn, +.btn-toolbar > .btn-group, +.btn-toolbar > .input-group { + margin-left: 5px; +} +.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { + border-radius: 0; +} +.btn-group > .btn:first-child { + margin-left: 0; +} +.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.btn-group > .btn:last-child:not(:first-child), +.btn-group > .dropdown-toggle:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group > .btn-group { + float: left; +} +.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child, +.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} +.btn-group > .btn + .dropdown-toggle { + padding-right: 8px; + padding-left: 8px; +} +.btn-group > .btn-lg + .dropdown-toggle { + padding-right: 12px; + padding-left: 12px; +} +.btn-group.open .dropdown-toggle { + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); +} +.btn-group.open .dropdown-toggle.btn-link { + -webkit-box-shadow: none; + box-shadow: none; +} +.btn .caret { + margin-left: 0; +} +.btn-lg .caret { + border-width: 5px 5px 0; + border-bottom-width: 0; +} +.dropup .btn-lg .caret { + border-width: 0 5px 5px; +} +.btn-group-vertical > .btn, +.btn-group-vertical > .btn-group, +.btn-group-vertical > .btn-group > .btn { + display: block; + float: none; + width: 100%; + max-width: 100%; +} +.btn-group-vertical > .btn-group > .btn { + float: none; +} +.btn-group-vertical > .btn + .btn, +.btn-group-vertical > .btn + .btn-group, +.btn-group-vertical > .btn-group + .btn, +.btn-group-vertical > .btn-group + .btn-group { + margin-top: -1px; + margin-left: 0; +} +.btn-group-vertical > .btn:not(:first-child):not(:last-child) { + border-radius: 0; +} +.btn-group-vertical > .btn:first-child:not(:last-child) { + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group-vertical > .btn:last-child:not(:first-child) { + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; +} +.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child, +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.btn-group-justified { + display: table; + width: 100%; + table-layout: fixed; + border-collapse: separate; +} +.btn-group-justified > .btn, +.btn-group-justified > .btn-group { + display: table-cell; + float: none; + width: 1%; +} +.btn-group-justified > .btn-group .btn { + width: 100%; +} +.btn-group-justified > .btn-group .dropdown-menu { + left: auto; +} +[data-toggle="buttons"] > .btn input[type="radio"], +[data-toggle="buttons"] > .btn-group > .btn input[type="radio"], +[data-toggle="buttons"] > .btn input[type="checkbox"], +[data-toggle="buttons"] > .btn-group > .btn input[type="checkbox"] { + position: absolute; + clip: rect(0, 0, 0, 0); + pointer-events: none; +} +.input-group { + position: relative; + display: table; + border-collapse: separate; +} +.input-group[class*="col-"] { + float: none; + padding-right: 0; + padding-left: 0; +} +.input-group .form-control { + position: relative; + z-index: 2; + float: left; + width: 100%; + margin-bottom: 0; +} +.input-group .form-control:focus { + z-index: 3; +} +.input-group-lg > .form-control, +.input-group-lg > .input-group-addon, +.input-group-lg > .input-group-btn > .btn { + height: 46px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; + border-radius: 6px; +} +select.input-group-lg > .form-control, +select.input-group-lg > .input-group-addon, +select.input-group-lg > .input-group-btn > .btn { + height: 46px; + line-height: 46px; +} +textarea.input-group-lg > .form-control, +textarea.input-group-lg > .input-group-addon, +textarea.input-group-lg > .input-group-btn > .btn, +select[multiple].input-group-lg > .form-control, +select[multiple].input-group-lg > .input-group-addon, +select[multiple].input-group-lg > .input-group-btn > .btn { + height: auto; +} +.input-group-sm > .form-control, +.input-group-sm > .input-group-addon, +.input-group-sm > .input-group-btn > .btn { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +select.input-group-sm > .form-control, +select.input-group-sm > .input-group-addon, +select.input-group-sm > .input-group-btn > .btn { + height: 30px; + line-height: 30px; +} +textarea.input-group-sm > .form-control, +textarea.input-group-sm > .input-group-addon, +textarea.input-group-sm > .input-group-btn > .btn, +select[multiple].input-group-sm > .form-control, +select[multiple].input-group-sm > .input-group-addon, +select[multiple].input-group-sm > .input-group-btn > .btn { + height: auto; +} +.input-group-addon, +.input-group-btn, +.input-group .form-control { + display: table-cell; +} +.input-group-addon:not(:first-child):not(:last-child), +.input-group-btn:not(:first-child):not(:last-child), +.input-group .form-control:not(:first-child):not(:last-child) { + border-radius: 0; +} +.input-group-addon, +.input-group-btn { + width: 1%; + white-space: nowrap; + vertical-align: middle; +} +.input-group-addon { + padding: 6px 12px; + font-size: 14px; + font-weight: normal; + line-height: 1; + color: #555; + text-align: center; + background-color: #eee; + border: 1px solid #ccc; + border-radius: 4px; +} +.input-group-addon.input-sm { + padding: 5px 10px; + font-size: 12px; + border-radius: 3px; +} +.input-group-addon.input-lg { + padding: 10px 16px; + font-size: 18px; + border-radius: 6px; +} +.input-group-addon input[type="radio"], +.input-group-addon input[type="checkbox"] { + margin-top: 0; +} +.input-group .form-control:first-child, +.input-group-addon:first-child, +.input-group-btn:first-child > .btn, +.input-group-btn:first-child > .btn-group > .btn, +.input-group-btn:first-child > .dropdown-toggle, +.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), +.input-group-btn:last-child > .btn-group:not(:last-child) > .btn { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.input-group-addon:first-child { + border-right: 0; +} +.input-group .form-control:last-child, +.input-group-addon:last-child, +.input-group-btn:last-child > .btn, +.input-group-btn:last-child > .btn-group > .btn, +.input-group-btn:last-child > .dropdown-toggle, +.input-group-btn:first-child > .btn:not(:first-child), +.input-group-btn:first-child > .btn-group:not(:first-child) > .btn { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.input-group-addon:last-child { + border-left: 0; +} +.input-group-btn { + position: relative; + font-size: 0; + white-space: nowrap; +} +.input-group-btn > .btn { + position: relative; +} +.input-group-btn > .btn + .btn { + margin-left: -1px; +} +.input-group-btn > .btn:hover, +.input-group-btn > .btn:focus, +.input-group-btn > .btn:active { + z-index: 2; +} +.input-group-btn:first-child > .btn, +.input-group-btn:first-child > .btn-group { + margin-right: -1px; +} +.input-group-btn:last-child > .btn, +.input-group-btn:last-child > .btn-group { + z-index: 2; + margin-left: -1px; +} +.nav { + padding-left: 0; + margin-bottom: 0; + list-style: none; +} +.nav > li { + position: relative; + display: block; +} +.nav > li > a { + position: relative; + display: block; + padding: 10px 15px; +} +.nav > li > a:hover, +.nav > li > a:focus { + text-decoration: none; + background-color: #eee; +} +.nav > li.disabled > a { + color: #777; +} +.nav > li.disabled > a:hover, +.nav > li.disabled > a:focus { + color: #777; + text-decoration: none; + cursor: not-allowed; + background-color: transparent; +} +.nav .open > a, +.nav .open > a:hover, +.nav .open > a:focus { + background-color: #eee; + border-color: #337ab7; +} +.nav .nav-divider { + height: 1px; + margin: 9px 0; + overflow: hidden; + background-color: #e5e5e5; +} +.nav > li > a > img { + max-width: none; +} +.nav-tabs { + border-bottom: 1px solid #ddd; +} +.nav-tabs > li { + float: left; + margin-bottom: -1px; +} +.nav-tabs > li > a { + margin-right: 2px; + line-height: 1.42857143; + border: 1px solid transparent; + border-radius: 4px 4px 0 0; +} +.nav-tabs > li > a:hover { + border-color: #eee #eee #ddd; +} +.nav-tabs > li.active > a, +.nav-tabs > li.active > a:hover, +.nav-tabs > li.active > a:focus { + color: #555; + cursor: default; + background-color: #fff; + border: 1px solid #ddd; + border-bottom-color: transparent; +} +.nav-tabs.nav-justified { + width: 100%; + border-bottom: 0; +} +.nav-tabs.nav-justified > li { + float: none; +} +.nav-tabs.nav-justified > li > a { + margin-bottom: 5px; + text-align: center; +} +.nav-tabs.nav-justified > .dropdown .dropdown-menu { + top: auto; + left: auto; +} +@media (min-width: 768px) { + .nav-tabs.nav-justified > li { + display: table-cell; + width: 1%; + } + .nav-tabs.nav-justified > li > a { + margin-bottom: 0; + } +} +.nav-tabs.nav-justified > li > a { + margin-right: 0; + border-radius: 4px; +} +.nav-tabs.nav-justified > .active > a, +.nav-tabs.nav-justified > .active > a:hover, +.nav-tabs.nav-justified > .active > a:focus { + border: 1px solid #ddd; +} +@media (min-width: 768px) { + .nav-tabs.nav-justified > li > a { + border-bottom: 1px solid #ddd; + border-radius: 4px 4px 0 0; + } + .nav-tabs.nav-justified > .active > a, + .nav-tabs.nav-justified > .active > a:hover, + .nav-tabs.nav-justified > .active > a:focus { + border-bottom-color: #fff; + } +} +.nav-pills > li { + float: left; +} +.nav-pills > li > a { + border-radius: 4px; +} +.nav-pills > li + li { + margin-left: 2px; +} +.nav-pills > li.active > a, +.nav-pills > li.active > a:hover, +.nav-pills > li.active > a:focus { + color: #fff; + background-color: #337ab7; +} +.nav-stacked > li { + float: none; +} +.nav-stacked > li + li { + margin-top: 2px; + margin-left: 0; +} +.nav-justified { + width: 100%; +} +.nav-justified > li { + float: none; +} +.nav-justified > li > a { + margin-bottom: 5px; + text-align: center; +} +.nav-justified > .dropdown .dropdown-menu { + top: auto; + left: auto; +} +@media (min-width: 768px) { + .nav-justified > li { + display: table-cell; + width: 1%; + } + .nav-justified > li > a { + margin-bottom: 0; + } +} +.nav-tabs-justified { + border-bottom: 0; +} +.nav-tabs-justified > li > a { + margin-right: 0; + border-radius: 4px; +} +.nav-tabs-justified > .active > a, +.nav-tabs-justified > .active > a:hover, +.nav-tabs-justified > .active > a:focus { + border: 1px solid #ddd; +} +@media (min-width: 768px) { + .nav-tabs-justified > li > a { + border-bottom: 1px solid #ddd; + border-radius: 4px 4px 0 0; + } + .nav-tabs-justified > .active > a, + .nav-tabs-justified > .active > a:hover, + .nav-tabs-justified > .active > a:focus { + border-bottom-color: #fff; + } +} +.tab-content > .tab-pane { + display: none; +} +.tab-content > .active { + display: block; +} +.nav-tabs .dropdown-menu { + margin-top: -1px; + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.navbar { + position: relative; + min-height: 50px; + margin-bottom: 20px; + border: 1px solid transparent; +} +@media (min-width: 768px) { + .navbar { + border-radius: 4px; + } +} +@media (min-width: 768px) { + .navbar-header { + float: left; + } +} +.navbar-collapse { + padding-right: 15px; + padding-left: 15px; + overflow-x: visible; + -webkit-overflow-scrolling: touch; + border-top: 1px solid transparent; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1); +} +.navbar-collapse.in { + overflow-y: auto; +} +@media (min-width: 768px) { + .navbar-collapse { + width: auto; + border-top: 0; + -webkit-box-shadow: none; + box-shadow: none; + } + .navbar-collapse.collapse { + display: block !important; + height: auto !important; + padding-bottom: 0; + overflow: visible !important; + } + .navbar-collapse.in { + overflow-y: visible; + } + .navbar-fixed-top .navbar-collapse, + .navbar-static-top .navbar-collapse, + .navbar-fixed-bottom .navbar-collapse { + padding-right: 0; + padding-left: 0; + } +} +.navbar-fixed-top .navbar-collapse, +.navbar-fixed-bottom .navbar-collapse { + max-height: 340px; +} +@media (max-device-width: 480px) and (orientation: landscape) { + .navbar-fixed-top .navbar-collapse, + .navbar-fixed-bottom .navbar-collapse { + max-height: 200px; + } +} +.container > .navbar-header, +.container-fluid > .navbar-header, +.container > .navbar-collapse, +.container-fluid > .navbar-collapse { + margin-right: -15px; + margin-left: -15px; +} +@media (min-width: 768px) { + .container > .navbar-header, + .container-fluid > .navbar-header, + .container > .navbar-collapse, + .container-fluid > .navbar-collapse { + margin-right: 0; + margin-left: 0; + } +} +.navbar-static-top { + z-index: 1000; + border-width: 0 0 1px; +} +@media (min-width: 768px) { + .navbar-static-top { + border-radius: 0; + } +} +.navbar-fixed-top, +.navbar-fixed-bottom { + position: fixed; + right: 0; + left: 0; + z-index: 1030; +} +@media (min-width: 768px) { + .navbar-fixed-top, + .navbar-fixed-bottom { + border-radius: 0; + } +} +.navbar-fixed-top { + top: 0; + border-width: 0 0 1px; +} +.navbar-fixed-bottom { + bottom: 0; + margin-bottom: 0; + border-width: 1px 0 0; +} +.navbar-brand { + float: left; + height: 50px; + padding: 15px 15px; + font-size: 18px; + line-height: 20px; +} +.navbar-brand:hover, +.navbar-brand:focus { + text-decoration: none; +} +.navbar-brand > img { + display: block; +} +@media (min-width: 768px) { + .navbar > .container .navbar-brand, + .navbar > .container-fluid .navbar-brand { + margin-left: -15px; + } +} +.navbar-toggle { + position: relative; + float: right; + padding: 9px 10px; + margin-top: 8px; + margin-right: 15px; + margin-bottom: 8px; + background-color: transparent; + background-image: none; + border: 1px solid transparent; + border-radius: 4px; +} +.navbar-toggle:focus { + outline: 0; +} +.navbar-toggle .icon-bar { + display: block; + width: 22px; + height: 2px; + border-radius: 1px; +} +.navbar-toggle .icon-bar + .icon-bar { + margin-top: 4px; +} +@media (min-width: 768px) { + .navbar-toggle { + display: none; + } +} +.navbar-nav { + margin: 7.5px -15px; +} +.navbar-nav > li > a { + padding-top: 10px; + padding-bottom: 10px; + line-height: 20px; +} +@media (max-width: 767px) { + .navbar-nav .open .dropdown-menu { + position: static; + float: none; + width: auto; + margin-top: 0; + background-color: transparent; + border: 0; + -webkit-box-shadow: none; + box-shadow: none; + } + .navbar-nav .open .dropdown-menu > li > a, + .navbar-nav .open .dropdown-menu .dropdown-header { + padding: 5px 15px 5px 25px; + } + .navbar-nav .open .dropdown-menu > li > a { + line-height: 20px; + } + .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-nav .open .dropdown-menu > li > a:focus { + background-image: none; + } +} +@media (min-width: 768px) { + .navbar-nav { + float: left; + margin: 0; + } + .navbar-nav > li { + float: left; + } + .navbar-nav > li > a { + padding-top: 15px; + padding-bottom: 15px; + } +} +.navbar-form { + padding: 10px 15px; + margin-top: 8px; + margin-right: -15px; + margin-bottom: 8px; + margin-left: -15px; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1); +} +@media (min-width: 768px) { + .navbar-form .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .form-control { + display: inline-block; + width: auto; + vertical-align: middle; + } + .navbar-form .form-control-static { + display: inline-block; + } + .navbar-form .input-group { + display: inline-table; + vertical-align: middle; + } + .navbar-form .input-group .input-group-addon, + .navbar-form .input-group .input-group-btn, + .navbar-form .input-group .form-control { + width: auto; + } + .navbar-form .input-group > .form-control { + width: 100%; + } + .navbar-form .control-label { + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .radio, + .navbar-form .checkbox { + display: inline-block; + margin-top: 0; + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .radio label, + .navbar-form .checkbox label { + padding-left: 0; + } + .navbar-form .radio input[type="radio"], + .navbar-form .checkbox input[type="checkbox"] { + position: relative; + margin-left: 0; + } + .navbar-form .has-feedback .form-control-feedback { + top: 0; + } +} +@media (max-width: 767px) { + .navbar-form .form-group { + margin-bottom: 5px; + } + .navbar-form .form-group:last-child { + margin-bottom: 0; + } +} +@media (min-width: 768px) { + .navbar-form { + width: auto; + padding-top: 0; + padding-bottom: 0; + margin-right: 0; + margin-left: 0; + border: 0; + -webkit-box-shadow: none; + box-shadow: none; + } +} +.navbar-nav > li > .dropdown-menu { + margin-top: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu { + margin-bottom: 0; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.navbar-btn { + margin-top: 8px; + margin-bottom: 8px; +} +.navbar-btn.btn-sm { + margin-top: 10px; + margin-bottom: 10px; +} +.navbar-btn.btn-xs { + margin-top: 14px; + margin-bottom: 14px; +} +.navbar-text { + margin-top: 15px; + margin-bottom: 15px; +} +@media (min-width: 768px) { + .navbar-text { + float: left; + margin-right: 15px; + margin-left: 15px; + } +} +@media (min-width: 768px) { + .navbar-left { + float: left !important; + } + .navbar-right { + float: right !important; + margin-right: -15px; + } + .navbar-right ~ .navbar-right { + margin-right: 0; + } +} +.navbar-default { + background-color: #f8f8f8; + border-color: #e7e7e7; +} +.navbar-default .navbar-brand { + color: #777; +} +.navbar-default .navbar-brand:hover, +.navbar-default .navbar-brand:focus { + color: #5e5e5e; + background-color: transparent; +} +.navbar-default .navbar-text { + color: #777; +} +.navbar-default .navbar-nav > li > a { + color: #777; +} +.navbar-default .navbar-nav > li > a:hover, +.navbar-default .navbar-nav > li > a:focus { + color: #333; + background-color: transparent; +} +.navbar-default .navbar-nav > .active > a, +.navbar-default .navbar-nav > .active > a:hover, +.navbar-default .navbar-nav > .active > a:focus { + color: #555; + background-color: #e7e7e7; +} +.navbar-default .navbar-nav > .disabled > a, +.navbar-default .navbar-nav > .disabled > a:hover, +.navbar-default .navbar-nav > .disabled > a:focus { + color: #ccc; + background-color: transparent; +} +.navbar-default .navbar-toggle { + border-color: #ddd; +} +.navbar-default .navbar-toggle:hover, +.navbar-default .navbar-toggle:focus { + background-color: #ddd; +} +.navbar-default .navbar-toggle .icon-bar { + background-color: #888; +} +.navbar-default .navbar-collapse, +.navbar-default .navbar-form { + border-color: #e7e7e7; +} +.navbar-default .navbar-nav > .open > a, +.navbar-default .navbar-nav > .open > a:hover, +.navbar-default .navbar-nav > .open > a:focus { + color: #555; + background-color: #e7e7e7; +} +@media (max-width: 767px) { + .navbar-default .navbar-nav .open .dropdown-menu > li > a { + color: #777; + } + .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus { + color: #333; + background-color: transparent; + } + .navbar-default .navbar-nav .open .dropdown-menu > .active > a, + .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #555; + background-color: #e7e7e7; + } + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a, + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus { + color: #ccc; + background-color: transparent; + } +} +.navbar-default .navbar-link { + color: #777; +} +.navbar-default .navbar-link:hover { + color: #333; +} +.navbar-default .btn-link { + color: #777; +} +.navbar-default .btn-link:hover, +.navbar-default .btn-link:focus { + color: #333; +} +.navbar-default .btn-link[disabled]:hover, +fieldset[disabled] .navbar-default .btn-link:hover, +.navbar-default .btn-link[disabled]:focus, +fieldset[disabled] .navbar-default .btn-link:focus { + color: #ccc; +} +.navbar-inverse { + background-color: #222; + border-color: #080808; +} +.navbar-inverse .navbar-brand { + color: #9d9d9d; +} +.navbar-inverse .navbar-brand:hover, +.navbar-inverse .navbar-brand:focus { + color: #fff; + background-color: transparent; +} +.navbar-inverse .navbar-text { + color: #9d9d9d; +} +.navbar-inverse .navbar-nav > li > a { + color: #9d9d9d; +} +.navbar-inverse .navbar-nav > li > a:hover, +.navbar-inverse .navbar-nav > li > a:focus { + color: #fff; + background-color: transparent; +} +.navbar-inverse .navbar-nav > .active > a, +.navbar-inverse .navbar-nav > .active > a:hover, +.navbar-inverse .navbar-nav > .active > a:focus { + color: #fff; + background-color: #080808; +} +.navbar-inverse .navbar-nav > .disabled > a, +.navbar-inverse .navbar-nav > .disabled > a:hover, +.navbar-inverse .navbar-nav > .disabled > a:focus { + color: #444; + background-color: transparent; +} +.navbar-inverse .navbar-toggle { + border-color: #333; +} +.navbar-inverse .navbar-toggle:hover, +.navbar-inverse .navbar-toggle:focus { + background-color: #333; +} +.navbar-inverse .navbar-toggle .icon-bar { + background-color: #fff; +} +.navbar-inverse .navbar-collapse, +.navbar-inverse .navbar-form { + border-color: #101010; +} +.navbar-inverse .navbar-nav > .open > a, +.navbar-inverse .navbar-nav > .open > a:hover, +.navbar-inverse .navbar-nav > .open > a:focus { + color: #fff; + background-color: #080808; +} +@media (max-width: 767px) { + .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header { + border-color: #080808; + } + .navbar-inverse .navbar-nav .open .dropdown-menu .divider { + background-color: #080808; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a { + color: #9d9d9d; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus { + color: #fff; + background-color: transparent; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a, + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #fff; + background-color: #080808; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a, + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus { + color: #444; + background-color: transparent; + } +} +.navbar-inverse .navbar-link { + color: #9d9d9d; +} +.navbar-inverse .navbar-link:hover { + color: #fff; +} +.navbar-inverse .btn-link { + color: #9d9d9d; +} +.navbar-inverse .btn-link:hover, +.navbar-inverse .btn-link:focus { + color: #fff; +} +.navbar-inverse .btn-link[disabled]:hover, +fieldset[disabled] .navbar-inverse .btn-link:hover, +.navbar-inverse .btn-link[disabled]:focus, +fieldset[disabled] .navbar-inverse .btn-link:focus { + color: #444; +} +.breadcrumb { + padding: 8px 15px; + margin-bottom: 20px; + list-style: none; + background-color: #f5f5f5; + border-radius: 4px; +} +.breadcrumb > li { + display: inline-block; +} +.breadcrumb > li + li:before { + padding: 0 5px; + color: #ccc; + content: "/\00a0"; +} +.breadcrumb > .active { + color: #777; +} +.pagination { + display: inline-block; + padding-left: 0; + margin: 20px 0; + border-radius: 4px; +} +.pagination > li { + display: inline; +} +.pagination > li > a, +.pagination > li > span { + position: relative; + float: left; + padding: 6px 12px; + margin-left: -1px; + line-height: 1.42857143; + color: #337ab7; + text-decoration: none; + background-color: #fff; + border: 1px solid #ddd; +} +.pagination > li:first-child > a, +.pagination > li:first-child > span { + margin-left: 0; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; +} +.pagination > li:last-child > a, +.pagination > li:last-child > span { + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; +} +.pagination > li > a:hover, +.pagination > li > span:hover, +.pagination > li > a:focus, +.pagination > li > span:focus { + z-index: 2; + color: #23527c; + background-color: #eee; + border-color: #ddd; +} +.pagination > .active > a, +.pagination > .active > span, +.pagination > .active > a:hover, +.pagination > .active > span:hover, +.pagination > .active > a:focus, +.pagination > .active > span:focus { + z-index: 3; + color: #fff; + cursor: default; + background-color: #337ab7; + border-color: #337ab7; +} +.pagination > .disabled > span, +.pagination > .disabled > span:hover, +.pagination > .disabled > span:focus, +.pagination > .disabled > a, +.pagination > .disabled > a:hover, +.pagination > .disabled > a:focus { + color: #777; + cursor: not-allowed; + background-color: #fff; + border-color: #ddd; +} +.pagination-lg > li > a, +.pagination-lg > li > span { + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; +} +.pagination-lg > li:first-child > a, +.pagination-lg > li:first-child > span { + border-top-left-radius: 6px; + border-bottom-left-radius: 6px; +} +.pagination-lg > li:last-child > a, +.pagination-lg > li:last-child > span { + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; +} +.pagination-sm > li > a, +.pagination-sm > li > span { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; +} +.pagination-sm > li:first-child > a, +.pagination-sm > li:first-child > span { + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; +} +.pagination-sm > li:last-child > a, +.pagination-sm > li:last-child > span { + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; +} +.pager { + padding-left: 0; + margin: 20px 0; + text-align: center; + list-style: none; +} +.pager li { + display: inline; +} +.pager li > a, +.pager li > span { + display: inline-block; + padding: 5px 14px; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 15px; +} +.pager li > a:hover, +.pager li > a:focus { + text-decoration: none; + background-color: #eee; +} +.pager .next > a, +.pager .next > span { + float: right; +} +.pager .previous > a, +.pager .previous > span { + float: left; +} +.pager .disabled > a, +.pager .disabled > a:hover, +.pager .disabled > a:focus, +.pager .disabled > span { + color: #777; + cursor: not-allowed; + background-color: #fff; +} +.label { + display: inline; + padding: .2em .6em .3em; + font-size: 75%; + font-weight: bold; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25em; +} +a.label:hover, +a.label:focus { + color: #fff; + text-decoration: none; + cursor: pointer; +} +.label:empty { + display: none; +} +.btn .label { + position: relative; + top: -1px; +} +.label-default { + background-color: #777; +} +.label-default[href]:hover, +.label-default[href]:focus { + background-color: #5e5e5e; +} +.label-primary { + background-color: #337ab7; +} +.label-primary[href]:hover, +.label-primary[href]:focus { + background-color: #286090; +} +.label-success { + background-color: #5cb85c; +} +.label-success[href]:hover, +.label-success[href]:focus { + background-color: #449d44; +} +.label-info { + background-color: #5bc0de; +} +.label-info[href]:hover, +.label-info[href]:focus { + background-color: #31b0d5; +} +.label-warning { + background-color: #f0ad4e; +} +.label-warning[href]:hover, +.label-warning[href]:focus { + background-color: #ec971f; +} +.label-danger { + background-color: #d9534f; +} +.label-danger[href]:hover, +.label-danger[href]:focus { + background-color: #c9302c; +} +.badge { + display: inline-block; + min-width: 10px; + padding: 3px 7px; + font-size: 12px; + font-weight: bold; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: middle; + background-color: #777; + border-radius: 10px; +} +.badge:empty { + display: none; +} +.btn .badge { + position: relative; + top: -1px; +} +.btn-xs .badge, +.btn-group-xs > .btn .badge { + top: 0; + padding: 1px 5px; +} +a.badge:hover, +a.badge:focus { + color: #fff; + text-decoration: none; + cursor: pointer; +} +.list-group-item.active > .badge, +.nav-pills > .active > a > .badge { + color: #337ab7; + background-color: #fff; +} +.list-group-item > .badge { + float: right; +} +.list-group-item > .badge + .badge { + margin-right: 5px; +} +.nav-pills > li > a > .badge { + margin-left: 3px; +} +.jumbotron { + padding-top: 30px; + padding-bottom: 30px; + margin-bottom: 30px; + color: inherit; + background-color: #eee; +} +.jumbotron h1, +.jumbotron .h1 { + color: inherit; +} +.jumbotron p { + margin-bottom: 15px; + font-size: 21px; + font-weight: 200; +} +.jumbotron > hr { + border-top-color: #d5d5d5; +} +.container .jumbotron, +.container-fluid .jumbotron { + padding-right: 15px; + padding-left: 15px; + border-radius: 6px; +} +.jumbotron .container { + max-width: 100%; +} +@media screen and (min-width: 768px) { + .jumbotron { + padding-top: 48px; + padding-bottom: 48px; + } + .container .jumbotron, + .container-fluid .jumbotron { + padding-right: 60px; + padding-left: 60px; + } + .jumbotron h1, + .jumbotron .h1 { + font-size: 63px; + } +} +.thumbnail { + display: block; + padding: 4px; + margin-bottom: 20px; + line-height: 1.42857143; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 4px; + -webkit-transition: border .2s ease-in-out; + -o-transition: border .2s ease-in-out; + transition: border .2s ease-in-out; +} +.thumbnail > img, +.thumbnail a > img { + margin-right: auto; + margin-left: auto; +} +a.thumbnail:hover, +a.thumbnail:focus, +a.thumbnail.active { + border-color: #337ab7; +} +.thumbnail .caption { + padding: 9px; + color: #333; +} +.alert { + padding: 15px; + margin-bottom: 20px; + border: 1px solid transparent; + border-radius: 4px; +} +.alert h4 { + margin-top: 0; + color: inherit; +} +.alert .alert-link { + font-weight: bold; +} +.alert > p, +.alert > ul { + margin-bottom: 0; +} +.alert > p + p { + margin-top: 5px; +} +.alert-dismissable, +.alert-dismissible { + padding-right: 35px; +} +.alert-dismissable .close, +.alert-dismissible .close { + position: relative; + top: -2px; + right: -21px; + color: inherit; +} +.alert-success { + color: #3c763d; + background-color: #dff0d8; + border-color: #d6e9c6; +} +.alert-success hr { + border-top-color: #c9e2b3; +} +.alert-success .alert-link { + color: #2b542c; +} +.alert-info { + color: #31708f; + background-color: #d9edf7; + border-color: #bce8f1; +} +.alert-info hr { + border-top-color: #a6e1ec; +} +.alert-info .alert-link { + color: #245269; +} +.alert-warning { + color: #8a6d3b; + background-color: #fcf8e3; + border-color: #faebcc; +} +.alert-warning hr { + border-top-color: #f7e1b5; +} +.alert-warning .alert-link { + color: #66512c; +} +.alert-danger { + color: #a94442; + background-color: #f2dede; + border-color: #ebccd1; +} +.alert-danger hr { + border-top-color: #e4b9c0; +} +.alert-danger .alert-link { + color: #843534; +} +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +@-o-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +@keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +.progress { + height: 20px; + margin-bottom: 20px; + overflow: hidden; + background-color: #f5f5f5; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1); +} +.progress-bar { + float: left; + width: 0; + height: 100%; + font-size: 12px; + line-height: 20px; + color: #fff; + text-align: center; + background-color: #337ab7; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15); + -webkit-transition: width .6s ease; + -o-transition: width .6s ease; + transition: width .6s ease; +} +.progress-striped .progress-bar, +.progress-bar-striped { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + -webkit-background-size: 40px 40px; + background-size: 40px 40px; +} +.progress.active .progress-bar, +.progress-bar.active { + -webkit-animation: progress-bar-stripes 2s linear infinite; + -o-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; +} +.progress-bar-success { + background-color: #5cb85c; +} +.progress-striped .progress-bar-success { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +.progress-bar-info { + background-color: #5bc0de; +} +.progress-striped .progress-bar-info { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +.progress-bar-warning { + background-color: #f0ad4e; +} +.progress-striped .progress-bar-warning { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +.progress-bar-danger { + background-color: #d9534f; +} +.progress-striped .progress-bar-danger { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +.media { + margin-top: 15px; +} +.media:first-child { + margin-top: 0; +} +.media, +.media-body { + overflow: hidden; + zoom: 1; +} +.media-body { + width: 10000px; +} +.media-object { + display: block; +} +.media-object.img-thumbnail { + max-width: none; +} +.media-right, +.media > .pull-right { + padding-left: 10px; +} +.media-left, +.media > .pull-left { + padding-right: 10px; +} +.media-left, +.media-right, +.media-body { + display: table-cell; + vertical-align: top; +} +.media-middle { + vertical-align: middle; +} +.media-bottom { + vertical-align: bottom; +} +.media-heading { + margin-top: 0; + margin-bottom: 5px; +} +.media-list { + padding-left: 0; + list-style: none; +} +.list-group { + padding-left: 0; + margin-bottom: 20px; +} +.list-group-item { + position: relative; + display: block; + padding: 10px 15px; + margin-bottom: -1px; + background-color: #fff; + border: 1px solid #ddd; +} +.list-group-item:first-child { + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} +.list-group-item:last-child { + margin-bottom: 0; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; +} +a.list-group-item, +button.list-group-item { + color: #555; +} +a.list-group-item .list-group-item-heading, +button.list-group-item .list-group-item-heading { + color: #333; +} +a.list-group-item:hover, +button.list-group-item:hover, +a.list-group-item:focus, +button.list-group-item:focus { + color: #555; + text-decoration: none; + background-color: #f5f5f5; +} +button.list-group-item { + width: 100%; + text-align: left; +} +.list-group-item.disabled, +.list-group-item.disabled:hover, +.list-group-item.disabled:focus { + color: #777; + cursor: not-allowed; + background-color: #eee; +} +.list-group-item.disabled .list-group-item-heading, +.list-group-item.disabled:hover .list-group-item-heading, +.list-group-item.disabled:focus .list-group-item-heading { + color: inherit; +} +.list-group-item.disabled .list-group-item-text, +.list-group-item.disabled:hover .list-group-item-text, +.list-group-item.disabled:focus .list-group-item-text { + color: #777; +} +.list-group-item.active, +.list-group-item.active:hover, +.list-group-item.active:focus { + z-index: 2; + color: #fff; + background-color: #337ab7; + border-color: #337ab7; +} +.list-group-item.active .list-group-item-heading, +.list-group-item.active:hover .list-group-item-heading, +.list-group-item.active:focus .list-group-item-heading, +.list-group-item.active .list-group-item-heading > small, +.list-group-item.active:hover .list-group-item-heading > small, +.list-group-item.active:focus .list-group-item-heading > small, +.list-group-item.active .list-group-item-heading > .small, +.list-group-item.active:hover .list-group-item-heading > .small, +.list-group-item.active:focus .list-group-item-heading > .small { + color: inherit; +} +.list-group-item.active .list-group-item-text, +.list-group-item.active:hover .list-group-item-text, +.list-group-item.active:focus .list-group-item-text { + color: #c7ddef; +} +.list-group-item-success { + color: #3c763d; + background-color: #dff0d8; +} +a.list-group-item-success, +button.list-group-item-success { + color: #3c763d; +} +a.list-group-item-success .list-group-item-heading, +button.list-group-item-success .list-group-item-heading { + color: inherit; +} +a.list-group-item-success:hover, +button.list-group-item-success:hover, +a.list-group-item-success:focus, +button.list-group-item-success:focus { + color: #3c763d; + background-color: #d0e9c6; +} +a.list-group-item-success.active, +button.list-group-item-success.active, +a.list-group-item-success.active:hover, +button.list-group-item-success.active:hover, +a.list-group-item-success.active:focus, +button.list-group-item-success.active:focus { + color: #fff; + background-color: #3c763d; + border-color: #3c763d; +} +.list-group-item-info { + color: #31708f; + background-color: #d9edf7; +} +a.list-group-item-info, +button.list-group-item-info { + color: #31708f; +} +a.list-group-item-info .list-group-item-heading, +button.list-group-item-info .list-group-item-heading { + color: inherit; +} +a.list-group-item-info:hover, +button.list-group-item-info:hover, +a.list-group-item-info:focus, +button.list-group-item-info:focus { + color: #31708f; + background-color: #c4e3f3; +} +a.list-group-item-info.active, +button.list-group-item-info.active, +a.list-group-item-info.active:hover, +button.list-group-item-info.active:hover, +a.list-group-item-info.active:focus, +button.list-group-item-info.active:focus { + color: #fff; + background-color: #31708f; + border-color: #31708f; +} +.list-group-item-warning { + color: #8a6d3b; + background-color: #fcf8e3; +} +a.list-group-item-warning, +button.list-group-item-warning { + color: #8a6d3b; +} +a.list-group-item-warning .list-group-item-heading, +button.list-group-item-warning .list-group-item-heading { + color: inherit; +} +a.list-group-item-warning:hover, +button.list-group-item-warning:hover, +a.list-group-item-warning:focus, +button.list-group-item-warning:focus { + color: #8a6d3b; + background-color: #faf2cc; +} +a.list-group-item-warning.active, +button.list-group-item-warning.active, +a.list-group-item-warning.active:hover, +button.list-group-item-warning.active:hover, +a.list-group-item-warning.active:focus, +button.list-group-item-warning.active:focus { + color: #fff; + background-color: #8a6d3b; + border-color: #8a6d3b; +} +.list-group-item-danger { + color: #a94442; + background-color: #f2dede; +} +a.list-group-item-danger, +button.list-group-item-danger { + color: #a94442; +} +a.list-group-item-danger .list-group-item-heading, +button.list-group-item-danger .list-group-item-heading { + color: inherit; +} +a.list-group-item-danger:hover, +button.list-group-item-danger:hover, +a.list-group-item-danger:focus, +button.list-group-item-danger:focus { + color: #a94442; + background-color: #ebcccc; +} +a.list-group-item-danger.active, +button.list-group-item-danger.active, +a.list-group-item-danger.active:hover, +button.list-group-item-danger.active:hover, +a.list-group-item-danger.active:focus, +button.list-group-item-danger.active:focus { + color: #fff; + background-color: #a94442; + border-color: #a94442; +} +.list-group-item-heading { + margin-top: 0; + margin-bottom: 5px; +} +.list-group-item-text { + margin-bottom: 0; + line-height: 1.3; +} +.panel { + margin-bottom: 20px; + background-color: #fff; + border: 1px solid transparent; + border-radius: 4px; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .05); + box-shadow: 0 1px 1px rgba(0, 0, 0, .05); +} +.panel-body { + padding: 15px; +} +.panel-heading { + padding: 10px 15px; + border-bottom: 1px solid transparent; + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel-heading > .dropdown .dropdown-toggle { + color: inherit; +} +.panel-title { + margin-top: 0; + margin-bottom: 0; + font-size: 16px; + color: inherit; +} +.panel-title > a, +.panel-title > small, +.panel-title > .small, +.panel-title > small > a, +.panel-title > .small > a { + color: inherit; +} +.panel-footer { + padding: 10px 15px; + background-color: #f5f5f5; + border-top: 1px solid #ddd; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel > .list-group, +.panel > .panel-collapse > .list-group { + margin-bottom: 0; +} +.panel > .list-group .list-group-item, +.panel > .panel-collapse > .list-group .list-group-item { + border-width: 1px 0; + border-radius: 0; +} +.panel > .list-group:first-child .list-group-item:first-child, +.panel > .panel-collapse > .list-group:first-child .list-group-item:first-child { + border-top: 0; + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel > .list-group:last-child .list-group-item:last-child, +.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child { + border-bottom: 0; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel > .panel-heading + .panel-collapse > .list-group .list-group-item:first-child { + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.panel-heading + .list-group .list-group-item:first-child { + border-top-width: 0; +} +.list-group + .panel-footer { + border-top-width: 0; +} +.panel > .table, +.panel > .table-responsive > .table, +.panel > .panel-collapse > .table { + margin-bottom: 0; +} +.panel > .table caption, +.panel > .table-responsive > .table caption, +.panel > .panel-collapse > .table caption { + padding-right: 15px; + padding-left: 15px; +} +.panel > .table:first-child, +.panel > .table-responsive:first-child > .table:first-child { + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel > .table:first-child > thead:first-child > tr:first-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child { + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel > .table:first-child > thead:first-child > tr:first-child td:first-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child, +.panel > .table:first-child > thead:first-child > tr:first-child th:first-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child { + border-top-left-radius: 3px; +} +.panel > .table:first-child > thead:first-child > tr:first-child td:last-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child, +.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child, +.panel > .table:first-child > thead:first-child > tr:first-child th:last-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child, +.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child { + border-top-right-radius: 3px; +} +.panel > .table:last-child, +.panel > .table-responsive:last-child > .table:last-child { + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel > .table:last-child > tbody:last-child > tr:last-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child { + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child, +.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child { + border-bottom-left-radius: 3px; +} +.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child, +.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child { + border-bottom-right-radius: 3px; +} +.panel > .panel-body + .table, +.panel > .panel-body + .table-responsive, +.panel > .table + .panel-body, +.panel > .table-responsive + .panel-body { + border-top: 1px solid #ddd; +} +.panel > .table > tbody:first-child > tr:first-child th, +.panel > .table > tbody:first-child > tr:first-child td { + border-top: 0; +} +.panel > .table-bordered, +.panel > .table-responsive > .table-bordered { + border: 0; +} +.panel > .table-bordered > thead > tr > th:first-child, +.panel > .table-responsive > .table-bordered > thead > tr > th:first-child, +.panel > .table-bordered > tbody > tr > th:first-child, +.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child, +.panel > .table-bordered > tfoot > tr > th:first-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child, +.panel > .table-bordered > thead > tr > td:first-child, +.panel > .table-responsive > .table-bordered > thead > tr > td:first-child, +.panel > .table-bordered > tbody > tr > td:first-child, +.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child, +.panel > .table-bordered > tfoot > tr > td:first-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child { + border-left: 0; +} +.panel > .table-bordered > thead > tr > th:last-child, +.panel > .table-responsive > .table-bordered > thead > tr > th:last-child, +.panel > .table-bordered > tbody > tr > th:last-child, +.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child, +.panel > .table-bordered > tfoot > tr > th:last-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child, +.panel > .table-bordered > thead > tr > td:last-child, +.panel > .table-responsive > .table-bordered > thead > tr > td:last-child, +.panel > .table-bordered > tbody > tr > td:last-child, +.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child, +.panel > .table-bordered > tfoot > tr > td:last-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child { + border-right: 0; +} +.panel > .table-bordered > thead > tr:first-child > td, +.panel > .table-responsive > .table-bordered > thead > tr:first-child > td, +.panel > .table-bordered > tbody > tr:first-child > td, +.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td, +.panel > .table-bordered > thead > tr:first-child > th, +.panel > .table-responsive > .table-bordered > thead > tr:first-child > th, +.panel > .table-bordered > tbody > tr:first-child > th, +.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th { + border-bottom: 0; +} +.panel > .table-bordered > tbody > tr:last-child > td, +.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td, +.panel > .table-bordered > tfoot > tr:last-child > td, +.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td, +.panel > .table-bordered > tbody > tr:last-child > th, +.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th, +.panel > .table-bordered > tfoot > tr:last-child > th, +.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th { + border-bottom: 0; +} +.panel > .table-responsive { + margin-bottom: 0; + border: 0; +} +.panel-group { + margin-bottom: 20px; +} +.panel-group .panel { + margin-bottom: 0; + border-radius: 4px; +} +.panel-group .panel + .panel { + margin-top: 5px; +} +.panel-group .panel-heading { + border-bottom: 0; +} +.panel-group .panel-heading + .panel-collapse > .panel-body, +.panel-group .panel-heading + .panel-collapse > .list-group { + border-top: 1px solid #ddd; +} +.panel-group .panel-footer { + border-top: 0; +} +.panel-group .panel-footer + .panel-collapse .panel-body { + border-bottom: 1px solid #ddd; +} +.panel-default { + border-color: #ddd; +} +.panel-default > .panel-heading { + color: #333; + background-color: #f5f5f5; + border-color: #ddd; +} +.panel-default > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #ddd; +} +.panel-default > .panel-heading .badge { + color: #f5f5f5; + background-color: #333; +} +.panel-default > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #ddd; +} +.panel-primary { + border-color: #337ab7; +} +.panel-primary > .panel-heading { + color: #fff; + background-color: #337ab7; + border-color: #337ab7; +} +.panel-primary > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #337ab7; +} +.panel-primary > .panel-heading .badge { + color: #337ab7; + background-color: #fff; +} +.panel-primary > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #337ab7; +} +.panel-success { + border-color: #d6e9c6; +} +.panel-success > .panel-heading { + color: #3c763d; + background-color: #dff0d8; + border-color: #d6e9c6; +} +.panel-success > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #d6e9c6; +} +.panel-success > .panel-heading .badge { + color: #dff0d8; + background-color: #3c763d; +} +.panel-success > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #d6e9c6; +} +.panel-info { + border-color: #bce8f1; +} +.panel-info > .panel-heading { + color: #31708f; + background-color: #d9edf7; + border-color: #bce8f1; +} +.panel-info > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #bce8f1; +} +.panel-info > .panel-heading .badge { + color: #d9edf7; + background-color: #31708f; +} +.panel-info > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #bce8f1; +} +.panel-warning { + border-color: #faebcc; +} +.panel-warning > .panel-heading { + color: #8a6d3b; + background-color: #fcf8e3; + border-color: #faebcc; +} +.panel-warning > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #faebcc; +} +.panel-warning > .panel-heading .badge { + color: #fcf8e3; + background-color: #8a6d3b; +} +.panel-warning > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #faebcc; +} +.panel-danger { + border-color: #ebccd1; +} +.panel-danger > .panel-heading { + color: #a94442; + background-color: #f2dede; + border-color: #ebccd1; +} +.panel-danger > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #ebccd1; +} +.panel-danger > .panel-heading .badge { + color: #f2dede; + background-color: #a94442; +} +.panel-danger > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #ebccd1; +} +.embed-responsive { + position: relative; + display: block; + height: 0; + padding: 0; + overflow: hidden; +} +.embed-responsive .embed-responsive-item, +.embed-responsive iframe, +.embed-responsive embed, +.embed-responsive object, +.embed-responsive video { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 100%; + height: 100%; + border: 0; +} +.embed-responsive-16by9 { + padding-bottom: 56.25%; +} +.embed-responsive-4by3 { + padding-bottom: 75%; +} +.well { + min-height: 20px; + padding: 19px; + margin-bottom: 20px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); +} +.well blockquote { + border-color: #ddd; + border-color: rgba(0, 0, 0, .15); +} +.well-lg { + padding: 24px; + border-radius: 6px; +} +.well-sm { + padding: 9px; + border-radius: 3px; +} +.close { + float: right; + font-size: 21px; + font-weight: bold; + line-height: 1; + color: #000; + text-shadow: 0 1px 0 #fff; + filter: alpha(opacity=20); + opacity: .2; +} +.close:hover, +.close:focus { + color: #000; + text-decoration: none; + cursor: pointer; + filter: alpha(opacity=50); + opacity: .5; +} +button.close { + -webkit-appearance: none; + padding: 0; + cursor: pointer; + background: transparent; + border: 0; +} +.modal-open { + overflow: hidden; +} +.modal { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1050; + display: none; + overflow: hidden; + -webkit-overflow-scrolling: touch; + outline: 0; +} +.modal.fade .modal-dialog { + -webkit-transition: -webkit-transform .3s ease-out; + -o-transition: -o-transform .3s ease-out; + transition: transform .3s ease-out; + -webkit-transform: translate(0, -25%); + -ms-transform: translate(0, -25%); + -o-transform: translate(0, -25%); + transform: translate(0, -25%); +} +.modal.in .modal-dialog { + -webkit-transform: translate(0, 0); + -ms-transform: translate(0, 0); + -o-transform: translate(0, 0); + transform: translate(0, 0); +} +.modal-open .modal { + overflow-x: hidden; + overflow-y: auto; +} +.modal-dialog { + position: relative; + width: auto; + margin: 10px; +} +.modal-content { + position: relative; + background-color: #fff; + -webkit-background-clip: padding-box; + background-clip: padding-box; + border: 1px solid #999; + border: 1px solid rgba(0, 0, 0, .2); + border-radius: 6px; + outline: 0; + -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, .5); + box-shadow: 0 3px 9px rgba(0, 0, 0, .5); +} +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + background-color: #000; +} +.modal-backdrop.fade { + filter: alpha(opacity=0); + opacity: 0; +} +.modal-backdrop.in { + filter: alpha(opacity=50); + opacity: .5; +} +.modal-header { + padding: 15px; + border-bottom: 1px solid #e5e5e5; +} +.modal-header .close { + margin-top: -2px; +} +.modal-title { + margin: 0; + line-height: 1.42857143; +} +.modal-body { + position: relative; + padding: 15px; +} +.modal-footer { + padding: 15px; + text-align: right; + border-top: 1px solid #e5e5e5; +} +.modal-footer .btn + .btn { + margin-bottom: 0; + margin-left: 5px; +} +.modal-footer .btn-group .btn + .btn { + margin-left: -1px; +} +.modal-footer .btn-block + .btn-block { + margin-left: 0; +} +.modal-scrollbar-measure { + position: absolute; + top: -9999px; + width: 50px; + height: 50px; + overflow: scroll; +} +@media (min-width: 768px) { + .modal-dialog { + width: 600px; + margin: 30px auto; + } + .modal-content { + -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, .5); + box-shadow: 0 5px 15px rgba(0, 0, 0, .5); + } + .modal-sm { + width: 300px; + } +} +@media (min-width: 992px) { + .modal-lg { + width: 900px; + } +} +.tooltip { + position: absolute; + z-index: 1070; + display: block; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 12px; + font-style: normal; + font-weight: normal; + line-height: 1.42857143; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + word-spacing: normal; + word-wrap: normal; + white-space: normal; + filter: alpha(opacity=0); + opacity: 0; + + line-break: auto; +} +.tooltip.in { + filter: alpha(opacity=90); + opacity: .9; +} +.tooltip.top { + padding: 5px 0; + margin-top: -3px; +} +.tooltip.right { + padding: 0 5px; + margin-left: 3px; +} +.tooltip.bottom { + padding: 5px 0; + margin-top: 3px; +} +.tooltip.left { + padding: 0 5px; + margin-left: -3px; +} +.tooltip-inner { + max-width: 200px; + padding: 3px 8px; + color: #fff; + text-align: center; + background-color: #000; + border-radius: 4px; +} +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} +.tooltip.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-width: 5px 5px 0; + border-top-color: #000; +} +.tooltip.top-left .tooltip-arrow { + right: 5px; + bottom: 0; + margin-bottom: -5px; + border-width: 5px 5px 0; + border-top-color: #000; +} +.tooltip.top-right .tooltip-arrow { + bottom: 0; + left: 5px; + margin-bottom: -5px; + border-width: 5px 5px 0; + border-top-color: #000; +} +.tooltip.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-width: 5px 5px 5px 0; + border-right-color: #000; +} +.tooltip.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-width: 5px 0 5px 5px; + border-left-color: #000; +} +.tooltip.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} +.tooltip.bottom-left .tooltip-arrow { + top: 0; + right: 5px; + margin-top: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} +.tooltip.bottom-right .tooltip-arrow { + top: 0; + left: 5px; + margin-top: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1060; + display: none; + max-width: 276px; + padding: 1px; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: normal; + line-height: 1.42857143; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + word-spacing: normal; + word-wrap: normal; + white-space: normal; + background-color: #fff; + -webkit-background-clip: padding-box; + background-clip: padding-box; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, .2); + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, .2); + box-shadow: 0 5px 10px rgba(0, 0, 0, .2); + + line-break: auto; +} +.popover.top { + margin-top: -10px; +} +.popover.right { + margin-left: 10px; +} +.popover.bottom { + margin-top: 10px; +} +.popover.left { + margin-left: -10px; +} +.popover-title { + padding: 8px 14px; + margin: 0; + font-size: 14px; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + border-radius: 5px 5px 0 0; +} +.popover-content { + padding: 9px 14px; +} +.popover > .arrow, +.popover > .arrow:after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} +.popover > .arrow { + border-width: 11px; +} +.popover > .arrow:after { + content: ""; + border-width: 10px; +} +.popover.top > .arrow { + bottom: -11px; + left: 50%; + margin-left: -11px; + border-top-color: #999; + border-top-color: rgba(0, 0, 0, .25); + border-bottom-width: 0; +} +.popover.top > .arrow:after { + bottom: 1px; + margin-left: -10px; + content: " "; + border-top-color: #fff; + border-bottom-width: 0; +} +.popover.right > .arrow { + top: 50%; + left: -11px; + margin-top: -11px; + border-right-color: #999; + border-right-color: rgba(0, 0, 0, .25); + border-left-width: 0; +} +.popover.right > .arrow:after { + bottom: -10px; + left: 1px; + content: " "; + border-right-color: #fff; + border-left-width: 0; +} +.popover.bottom > .arrow { + top: -11px; + left: 50%; + margin-left: -11px; + border-top-width: 0; + border-bottom-color: #999; + border-bottom-color: rgba(0, 0, 0, .25); +} +.popover.bottom > .arrow:after { + top: 1px; + margin-left: -10px; + content: " "; + border-top-width: 0; + border-bottom-color: #fff; +} +.popover.left > .arrow { + top: 50%; + right: -11px; + margin-top: -11px; + border-right-width: 0; + border-left-color: #999; + border-left-color: rgba(0, 0, 0, .25); +} +.popover.left > .arrow:after { + right: 1px; + bottom: -10px; + content: " "; + border-right-width: 0; + border-left-color: #fff; +} +.carousel { + position: relative; +} +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; +} +.carousel-inner > .item { + position: relative; + display: none; + -webkit-transition: .6s ease-in-out left; + -o-transition: .6s ease-in-out left; + transition: .6s ease-in-out left; +} +.carousel-inner > .item > img, +.carousel-inner > .item > a > img { + line-height: 1; +} +@media all and (transform-3d), (-webkit-transform-3d) { + .carousel-inner > .item { + -webkit-transition: -webkit-transform .6s ease-in-out; + -o-transition: -o-transform .6s ease-in-out; + transition: transform .6s ease-in-out; + + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + -webkit-perspective: 1000px; + perspective: 1000px; + } + .carousel-inner > .item.next, + .carousel-inner > .item.active.right { + left: 0; + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } + .carousel-inner > .item.prev, + .carousel-inner > .item.active.left { + left: 0; + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } + .carousel-inner > .item.next.left, + .carousel-inner > .item.prev.right, + .carousel-inner > .item.active { + left: 0; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} +.carousel-inner > .active, +.carousel-inner > .next, +.carousel-inner > .prev { + display: block; +} +.carousel-inner > .active { + left: 0; +} +.carousel-inner > .next, +.carousel-inner > .prev { + position: absolute; + top: 0; + width: 100%; +} +.carousel-inner > .next { + left: 100%; +} +.carousel-inner > .prev { + left: -100%; +} +.carousel-inner > .next.left, +.carousel-inner > .prev.right { + left: 0; +} +.carousel-inner > .active.left { + left: -100%; +} +.carousel-inner > .active.right { + left: 100%; +} +.carousel-control { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 15%; + font-size: 20px; + color: #fff; + text-align: center; + text-shadow: 0 1px 2px rgba(0, 0, 0, .6); + background-color: rgba(0, 0, 0, 0); + filter: alpha(opacity=50); + opacity: .5; +} +.carousel-control.left { + background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); + background-image: -o-linear-gradient(left, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); + background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, .5)), to(rgba(0, 0, 0, .0001))); + background-image: linear-gradient(to right, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1); + background-repeat: repeat-x; +} +.carousel-control.right { + right: 0; + left: auto; + background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); + background-image: -o-linear-gradient(left, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); + background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, .0001)), to(rgba(0, 0, 0, .5))); + background-image: linear-gradient(to right, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1); + background-repeat: repeat-x; +} +.carousel-control:hover, +.carousel-control:focus { + color: #fff; + text-decoration: none; + filter: alpha(opacity=90); + outline: 0; + opacity: .9; +} +.carousel-control .icon-prev, +.carousel-control .icon-next, +.carousel-control .glyphicon-chevron-left, +.carousel-control .glyphicon-chevron-right { + position: absolute; + top: 50%; + z-index: 5; + display: inline-block; + margin-top: -10px; +} +.carousel-control .icon-prev, +.carousel-control .glyphicon-chevron-left { + left: 50%; + margin-left: -10px; +} +.carousel-control .icon-next, +.carousel-control .glyphicon-chevron-right { + right: 50%; + margin-right: -10px; +} +.carousel-control .icon-prev, +.carousel-control .icon-next { + width: 20px; + height: 20px; + font-family: serif; + line-height: 1; +} +.carousel-control .icon-prev:before { + content: '\2039'; +} +.carousel-control .icon-next:before { + content: '\203a'; +} +.carousel-indicators { + position: absolute; + bottom: 10px; + left: 50%; + z-index: 15; + width: 60%; + padding-left: 0; + margin-left: -30%; + text-align: center; + list-style: none; +} +.carousel-indicators li { + display: inline-block; + width: 10px; + height: 10px; + margin: 1px; + text-indent: -999px; + cursor: pointer; + background-color: #000 \9; + background-color: rgba(0, 0, 0, 0); + border: 1px solid #fff; + border-radius: 10px; +} +.carousel-indicators .active { + width: 12px; + height: 12px; + margin: 0; + background-color: #fff; +} +.carousel-caption { + position: absolute; + right: 15%; + bottom: 20px; + left: 15%; + z-index: 10; + padding-top: 20px; + padding-bottom: 20px; + color: #fff; + text-align: center; + text-shadow: 0 1px 2px rgba(0, 0, 0, .6); +} +.carousel-caption .btn { + text-shadow: none; +} +@media screen and (min-width: 768px) { + .carousel-control .glyphicon-chevron-left, + .carousel-control .glyphicon-chevron-right, + .carousel-control .icon-prev, + .carousel-control .icon-next { + width: 30px; + height: 30px; + margin-top: -10px; + font-size: 30px; + } + .carousel-control .glyphicon-chevron-left, + .carousel-control .icon-prev { + margin-left: -10px; + } + .carousel-control .glyphicon-chevron-right, + .carousel-control .icon-next { + margin-right: -10px; + } + .carousel-caption { + right: 20%; + left: 20%; + padding-bottom: 30px; + } + .carousel-indicators { + bottom: 20px; + } +} +.clearfix:before, +.clearfix:after, +.dl-horizontal dd:before, +.dl-horizontal dd:after, +.container:before, +.container:after, +.container-fluid:before, +.container-fluid:after, +.row:before, +.row:after, +.form-horizontal .form-group:before, +.form-horizontal .form-group:after, +.btn-toolbar:before, +.btn-toolbar:after, +.btn-group-vertical > .btn-group:before, +.btn-group-vertical > .btn-group:after, +.nav:before, +.nav:after, +.navbar:before, +.navbar:after, +.navbar-header:before, +.navbar-header:after, +.navbar-collapse:before, +.navbar-collapse:after, +.pager:before, +.pager:after, +.panel-body:before, +.panel-body:after, +.modal-header:before, +.modal-header:after, +.modal-footer:before, +.modal-footer:after { + display: table; + content: " "; +} +.clearfix:after, +.dl-horizontal dd:after, +.container:after, +.container-fluid:after, +.row:after, +.form-horizontal .form-group:after, +.btn-toolbar:after, +.btn-group-vertical > .btn-group:after, +.nav:after, +.navbar:after, +.navbar-header:after, +.navbar-collapse:after, +.pager:after, +.panel-body:after, +.modal-header:after, +.modal-footer:after { + clear: both; +} +.center-block { + display: block; + margin-right: auto; + margin-left: auto; +} +.pull-right { + float: right !important; +} +.pull-left { + float: left !important; +} +.hide { + display: none !important; +} +.show { + display: block !important; +} +.invisible { + visibility: hidden; +} +.text-hide { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} +.hidden { + display: none !important; +} +.affix { + position: fixed; +} +@-ms-viewport { + width: device-width; +} +.visible-xs, +.visible-sm, +.visible-md, +.visible-lg { + display: none !important; +} +.visible-xs-block, +.visible-xs-inline, +.visible-xs-inline-block, +.visible-sm-block, +.visible-sm-inline, +.visible-sm-inline-block, +.visible-md-block, +.visible-md-inline, +.visible-md-inline-block, +.visible-lg-block, +.visible-lg-inline, +.visible-lg-inline-block { + display: none !important; +} +@media (max-width: 767px) { + .visible-xs { + display: block !important; + } + table.visible-xs { + display: table !important; + } + tr.visible-xs { + display: table-row !important; + } + th.visible-xs, + td.visible-xs { + display: table-cell !important; + } +} +@media (max-width: 767px) { + .visible-xs-block { + display: block !important; + } +} +@media (max-width: 767px) { + .visible-xs-inline { + display: inline !important; + } +} +@media (max-width: 767px) { + .visible-xs-inline-block { + display: inline-block !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm { + display: block !important; + } + table.visible-sm { + display: table !important; + } + tr.visible-sm { + display: table-row !important; + } + th.visible-sm, + td.visible-sm { + display: table-cell !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm-block { + display: block !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm-inline { + display: inline !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm-inline-block { + display: inline-block !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md { + display: block !important; + } + table.visible-md { + display: table !important; + } + tr.visible-md { + display: table-row !important; + } + th.visible-md, + td.visible-md { + display: table-cell !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md-block { + display: block !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md-inline { + display: inline !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md-inline-block { + display: inline-block !important; + } +} +@media (min-width: 1200px) { + .visible-lg { + display: block !important; + } + table.visible-lg { + display: table !important; + } + tr.visible-lg { + display: table-row !important; + } + th.visible-lg, + td.visible-lg { + display: table-cell !important; + } +} +@media (min-width: 1200px) { + .visible-lg-block { + display: block !important; + } +} +@media (min-width: 1200px) { + .visible-lg-inline { + display: inline !important; + } +} +@media (min-width: 1200px) { + .visible-lg-inline-block { + display: inline-block !important; + } +} +@media (max-width: 767px) { + .hidden-xs { + display: none !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .hidden-sm { + display: none !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .hidden-md { + display: none !important; + } +} +@media (min-width: 1200px) { + .hidden-lg { + display: none !important; + } +} +.visible-print { + display: none !important; +} +@media print { + .visible-print { + display: block !important; + } + table.visible-print { + display: table !important; + } + tr.visible-print { + display: table-row !important; + } + th.visible-print, + td.visible-print { + display: table-cell !important; + } +} +.visible-print-block { + display: none !important; +} +@media print { + .visible-print-block { + display: block !important; + } +} +.visible-print-inline { + display: none !important; +} +@media print { + .visible-print-inline { + display: inline !important; + } +} +.visible-print-inline-block { + display: none !important; +} +@media print { + .visible-print-inline-block { + display: inline-block !important; + } +} +@media print { + .hidden-print { + display: none !important; + } +} +/*# sourceMappingURL=bootstrap.css.map */ diff --git a/themes/boots/bootstrap/css/bootstrap.css.map b/themes/boots/bootstrap/css/bootstrap.css.map new file mode 100644 index 000000000..09f8cda78 --- /dev/null +++ b/themes/boots/bootstrap/css/bootstrap.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["bootstrap.css","less/normalize.less","less/print.less","less/glyphicons.less","less/scaffolding.less","less/mixins/vendor-prefixes.less","less/mixins/tab-focus.less","less/mixins/image.less","less/type.less","less/mixins/text-emphasis.less","less/mixins/background-variant.less","less/mixins/text-overflow.less","less/code.less","less/grid.less","less/mixins/grid.less","less/mixins/grid-framework.less","less/tables.less","less/mixins/table-row.less","less/forms.less","less/mixins/forms.less","less/buttons.less","less/mixins/buttons.less","less/mixins/opacity.less","less/component-animations.less","less/dropdowns.less","less/mixins/nav-divider.less","less/mixins/reset-filter.less","less/button-groups.less","less/mixins/border-radius.less","less/input-groups.less","less/navs.less","less/navbar.less","less/mixins/nav-vertical-align.less","less/utilities.less","less/breadcrumbs.less","less/pagination.less","less/mixins/pagination.less","less/pager.less","less/labels.less","less/mixins/labels.less","less/badges.less","less/jumbotron.less","less/thumbnails.less","less/alerts.less","less/mixins/alerts.less","less/progress-bars.less","less/mixins/gradients.less","less/mixins/progress-bar.less","less/media.less","less/list-group.less","less/mixins/list-group.less","less/panels.less","less/mixins/panels.less","less/responsive-embed.less","less/wells.less","less/close.less","less/modals.less","less/tooltip.less","less/mixins/reset-text.less","less/popovers.less","less/carousel.less","less/mixins/clearfix.less","less/mixins/center-block.less","less/mixins/hide-text.less","less/responsive-utilities.less","less/mixins/responsive-visibility.less"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,4EAA4E;ACG5E;EACE,wBAAA;EACA,2BAAA;EACA,+BAAA;CDDD;ACQD;EACE,UAAA;CDND;ACmBD;;;;;;;;;;;;;EAaE,eAAA;CDjBD;ACyBD;;;;EAIE,sBAAA;EACA,yBAAA;CDvBD;AC+BD;EACE,cAAA;EACA,UAAA;CD7BD;ACqCD;;EAEE,cAAA;CDnCD;AC6CD;EACE,8BAAA;CD3CD;ACmDD;;EAEE,WAAA;CDjDD;AC2DD;EACE,0BAAA;CDzDD;ACgED;;EAEE,kBAAA;CD9DD;ACqED;EACE,mBAAA;CDnED;AC2ED;EACE,eAAA;EACA,iBAAA;CDzED;ACgFD;EACE,iBAAA;EACA,YAAA;CD9ED;ACqFD;EACE,eAAA;CDnFD;AC0FD;;EAEE,eAAA;EACA,eAAA;EACA,mBAAA;EACA,yBAAA;CDxFD;AC2FD;EACE,YAAA;CDzFD;AC4FD;EACE,gBAAA;CD1FD;ACoGD;EACE,UAAA;CDlGD;ACyGD;EACE,iBAAA;CDvGD;ACiHD;EACE,iBAAA;CD/GD;ACsHD;EACE,gCAAA;KAAA,6BAAA;UAAA,wBAAA;EACA,UAAA;CDpHD;AC2HD;EACE,eAAA;CDzHD;ACgID;;;;EAIE,kCAAA;EACA,eAAA;CD9HD;ACgJD;;;;;EAKE,eAAA;EACA,cAAA;EACA,UAAA;CD9ID;ACqJD;EACE,kBAAA;CDnJD;AC6JD;;EAEE,qBAAA;CD3JD;ACsKD;;;;EAIE,2BAAA;EACA,gBAAA;CDpKD;AC2KD;;EAEE,gBAAA;CDzKD;ACgLD;;EAEE,UAAA;EACA,WAAA;CD9KD;ACsLD;EACE,oBAAA;CDpLD;AC+LD;;EAEE,+BAAA;KAAA,4BAAA;UAAA,uBAAA;EACA,WAAA;CD7LD;ACsMD;;EAEE,aAAA;CDpMD;AC4MD;EACE,8BAAA;EACA,gCAAA;KAAA,6BAAA;UAAA,wBAAA;CD1MD;ACmND;;EAEE,yBAAA;CDjND;ACwND;EACE,0BAAA;EACA,cAAA;EACA,+BAAA;CDtND;AC8ND;EACE,UAAA;EACA,WAAA;CD5ND;ACmOD;EACE,eAAA;CDjOD;ACyOD;EACE,kBAAA;CDvOD;ACiPD;EACE,0BAAA;EACA,kBAAA;CD/OD;ACkPD;;EAEE,WAAA;CDhPD;AACD,qFAAqF;AElFrF;EA7FI;;;IAGI,mCAAA;IACA,uBAAA;IACA,oCAAA;YAAA,4BAAA;IACA,6BAAA;GFkLL;EE/KC;;IAEI,2BAAA;GFiLL;EE9KC;IACI,6BAAA;GFgLL;EE7KC;IACI,8BAAA;GF+KL;EE1KC;;IAEI,YAAA;GF4KL;EEzKC;;IAEI,uBAAA;IACA,yBAAA;GF2KL;EExKC;IACI,4BAAA;GF0KL;EEvKC;;IAEI,yBAAA;GFyKL;EEtKC;IACI,2BAAA;GFwKL;EErKC;;;IAGI,WAAA;IACA,UAAA;GFuKL;EEpKC;;IAEI,wBAAA;GFsKL;EEhKC;IACI,cAAA;GFkKL;EEhKC;;IAGQ,kCAAA;GFiKT;EE9JC;IACI,uBAAA;GFgKL;EE7JC;IACI,qCAAA;GF+JL;EEhKC;;IAKQ,kCAAA;GF+JT;EE5JC;;IAGQ,kCAAA;GF6JT;CACF;AGnPD;EACE,oCAAA;EACA,sDAAA;EACA,gYAAA;CHqPD;AG7OD;EACE,mBAAA;EACA,SAAA;EACA,sBAAA;EACA,oCAAA;EACA,mBAAA;EACA,oBAAA;EACA,eAAA;EACA,oCAAA;EACA,mCAAA;CH+OD;AG3OmC;EAAW,iBAAA;CH8O9C;AG7OmC;EAAW,iBAAA;CHgP9C;AG9OmC;;EAAW,iBAAA;CHkP9C;AGjPmC;EAAW,iBAAA;CHoP9C;AGnPmC;EAAW,iBAAA;CHsP9C;AGrPmC;EAAW,iBAAA;CHwP9C;AGvPmC;EAAW,iBAAA;CH0P9C;AGzPmC;EAAW,iBAAA;CH4P9C;AG3PmC;EAAW,iBAAA;CH8P9C;AG7PmC;EAAW,iBAAA;CHgQ9C;AG/PmC;EAAW,iBAAA;CHkQ9C;AGjQmC;EAAW,iBAAA;CHoQ9C;AGnQmC;EAAW,iBAAA;CHsQ9C;AGrQmC;EAAW,iBAAA;CHwQ9C;AGvQmC;EAAW,iBAAA;CH0Q9C;AGzQmC;EAAW,iBAAA;CH4Q9C;AG3QmC;EAAW,iBAAA;CH8Q9C;AG7QmC;EAAW,iBAAA;CHgR9C;AG/QmC;EAAW,iBAAA;CHkR9C;AGjRmC;EAAW,iBAAA;CHoR9C;AGnRmC;EAAW,iBAAA;CHsR9C;AGrRmC;EAAW,iBAAA;CHwR9C;AGvRmC;EAAW,iBAAA;CH0R9C;AGzRmC;EAAW,iBAAA;CH4R9C;AG3RmC;EAAW,iBAAA;CH8R9C;AG7RmC;EAAW,iBAAA;CHgS9C;AG/RmC;EAAW,iBAAA;CHkS9C;AGjSmC;EAAW,iBAAA;CHoS9C;AGnSmC;EAAW,iBAAA;CHsS9C;AGrSmC;EAAW,iBAAA;CHwS9C;AGvSmC;EAAW,iBAAA;CH0S9C;AGzSmC;EAAW,iBAAA;CH4S9C;AG3SmC;EAAW,iBAAA;CH8S9C;AG7SmC;EAAW,iBAAA;CHgT9C;AG/SmC;EAAW,iBAAA;CHkT9C;AGjTmC;EAAW,iBAAA;CHoT9C;AGnTmC;EAAW,iBAAA;CHsT9C;AGrTmC;EAAW,iBAAA;CHwT9C;AGvTmC;EAAW,iBAAA;CH0T9C;AGzTmC;EAAW,iBAAA;CH4T9C;AG3TmC;EAAW,iBAAA;CH8T9C;AG7TmC;EAAW,iBAAA;CHgU9C;AG/TmC;EAAW,iBAAA;CHkU9C;AGjUmC;EAAW,iBAAA;CHoU9C;AGnUmC;EAAW,iBAAA;CHsU9C;AGrUmC;EAAW,iBAAA;CHwU9C;AGvUmC;EAAW,iBAAA;CH0U9C;AGzUmC;EAAW,iBAAA;CH4U9C;AG3UmC;EAAW,iBAAA;CH8U9C;AG7UmC;EAAW,iBAAA;CHgV9C;AG/UmC;EAAW,iBAAA;CHkV9C;AGjVmC;EAAW,iBAAA;CHoV9C;AGnVmC;EAAW,iBAAA;CHsV9C;AGrVmC;EAAW,iBAAA;CHwV9C;AGvVmC;EAAW,iBAAA;CH0V9C;AGzVmC;EAAW,iBAAA;CH4V9C;AG3VmC;EAAW,iBAAA;CH8V9C;AG7VmC;EAAW,iBAAA;CHgW9C;AG/VmC;EAAW,iBAAA;CHkW9C;AGjWmC;EAAW,iBAAA;CHoW9C;AGnWmC;EAAW,iBAAA;CHsW9C;AGrWmC;EAAW,iBAAA;CHwW9C;AGvWmC;EAAW,iBAAA;CH0W9C;AGzWmC;EAAW,iBAAA;CH4W9C;AG3WmC;EAAW,iBAAA;CH8W9C;AG7WmC;EAAW,iBAAA;CHgX9C;AG/WmC;EAAW,iBAAA;CHkX9C;AGjXmC;EAAW,iBAAA;CHoX9C;AGnXmC;EAAW,iBAAA;CHsX9C;AGrXmC;EAAW,iBAAA;CHwX9C;AGvXmC;EAAW,iBAAA;CH0X9C;AGzXmC;EAAW,iBAAA;CH4X9C;AG3XmC;EAAW,iBAAA;CH8X9C;AG7XmC;EAAW,iBAAA;CHgY9C;AG/XmC;EAAW,iBAAA;CHkY9C;AGjYmC;EAAW,iBAAA;CHoY9C;AGnYmC;EAAW,iBAAA;CHsY9C;AGrYmC;EAAW,iBAAA;CHwY9C;AGvYmC;EAAW,iBAAA;CH0Y9C;AGzYmC;EAAW,iBAAA;CH4Y9C;AG3YmC;EAAW,iBAAA;CH8Y9C;AG7YmC;EAAW,iBAAA;CHgZ9C;AG/YmC;EAAW,iBAAA;CHkZ9C;AGjZmC;EAAW,iBAAA;CHoZ9C;AGnZmC;EAAW,iBAAA;CHsZ9C;AGrZmC;EAAW,iBAAA;CHwZ9C;AGvZmC;EAAW,iBAAA;CH0Z9C;AGzZmC;EAAW,iBAAA;CH4Z9C;AG3ZmC;EAAW,iBAAA;CH8Z9C;AG7ZmC;EAAW,iBAAA;CHga9C;AG/ZmC;EAAW,iBAAA;CHka9C;AGjamC;EAAW,iBAAA;CHoa9C;AGnamC;EAAW,iBAAA;CHsa9C;AGramC;EAAW,iBAAA;CHwa9C;AGvamC;EAAW,iBAAA;CH0a9C;AGzamC;EAAW,iBAAA;CH4a9C;AG3amC;EAAW,iBAAA;CH8a9C;AG7amC;EAAW,iBAAA;CHgb9C;AG/amC;EAAW,iBAAA;CHkb9C;AGjbmC;EAAW,iBAAA;CHob9C;AGnbmC;EAAW,iBAAA;CHsb9C;AGrbmC;EAAW,iBAAA;CHwb9C;AGvbmC;EAAW,iBAAA;CH0b9C;AGzbmC;EAAW,iBAAA;CH4b9C;AG3bmC;EAAW,iBAAA;CH8b9C;AG7bmC;EAAW,iBAAA;CHgc9C;AG/bmC;EAAW,iBAAA;CHkc9C;AGjcmC;EAAW,iBAAA;CHoc9C;AGncmC;EAAW,iBAAA;CHsc9C;AGrcmC;EAAW,iBAAA;CHwc9C;AGvcmC;EAAW,iBAAA;CH0c9C;AGzcmC;EAAW,iBAAA;CH4c9C;AG3cmC;EAAW,iBAAA;CH8c9C;AG7cmC;EAAW,iBAAA;CHgd9C;AG/cmC;EAAW,iBAAA;CHkd9C;AGjdmC;EAAW,iBAAA;CHod9C;AGndmC;EAAW,iBAAA;CHsd9C;AGrdmC;EAAW,iBAAA;CHwd9C;AGvdmC;EAAW,iBAAA;CH0d9C;AGzdmC;EAAW,iBAAA;CH4d9C;AG3dmC;EAAW,iBAAA;CH8d9C;AG7dmC;EAAW,iBAAA;CHge9C;AG/dmC;EAAW,iBAAA;CHke9C;AGjemC;EAAW,iBAAA;CHoe9C;AGnemC;EAAW,iBAAA;CHse9C;AGremC;EAAW,iBAAA;CHwe9C;AGvemC;EAAW,iBAAA;CH0e9C;AGzemC;EAAW,iBAAA;CH4e9C;AG3emC;EAAW,iBAAA;CH8e9C;AG7emC;EAAW,iBAAA;CHgf9C;AG/emC;EAAW,iBAAA;CHkf9C;AGjfmC;EAAW,iBAAA;CHof9C;AGnfmC;EAAW,iBAAA;CHsf9C;AGrfmC;EAAW,iBAAA;CHwf9C;AGvfmC;EAAW,iBAAA;CH0f9C;AGzfmC;EAAW,iBAAA;CH4f9C;AG3fmC;EAAW,iBAAA;CH8f9C;AG7fmC;EAAW,iBAAA;CHggB9C;AG/fmC;EAAW,iBAAA;CHkgB9C;AGjgBmC;EAAW,iBAAA;CHogB9C;AGngBmC;EAAW,iBAAA;CHsgB9C;AGrgBmC;EAAW,iBAAA;CHwgB9C;AGvgBmC;EAAW,iBAAA;CH0gB9C;AGzgBmC;EAAW,iBAAA;CH4gB9C;AG3gBmC;EAAW,iBAAA;CH8gB9C;AG7gBmC;EAAW,iBAAA;CHghB9C;AG/gBmC;EAAW,iBAAA;CHkhB9C;AGjhBmC;EAAW,iBAAA;CHohB9C;AGnhBmC;EAAW,iBAAA;CHshB9C;AGrhBmC;EAAW,iBAAA;CHwhB9C;AGvhBmC;EAAW,iBAAA;CH0hB9C;AGzhBmC;EAAW,iBAAA;CH4hB9C;AG3hBmC;EAAW,iBAAA;CH8hB9C;AG7hBmC;EAAW,iBAAA;CHgiB9C;AG/hBmC;EAAW,iBAAA;CHkiB9C;AGjiBmC;EAAW,iBAAA;CHoiB9C;AGniBmC;EAAW,iBAAA;CHsiB9C;AGriBmC;EAAW,iBAAA;CHwiB9C;AGviBmC;EAAW,iBAAA;CH0iB9C;AGziBmC;EAAW,iBAAA;CH4iB9C;AG3iBmC;EAAW,iBAAA;CH8iB9C;AG7iBmC;EAAW,iBAAA;CHgjB9C;AG/iBmC;EAAW,iBAAA;CHkjB9C;AGjjBmC;EAAW,iBAAA;CHojB9C;AGnjBmC;EAAW,iBAAA;CHsjB9C;AGrjBmC;EAAW,iBAAA;CHwjB9C;AGvjBmC;EAAW,iBAAA;CH0jB9C;AGzjBmC;EAAW,iBAAA;CH4jB9C;AG3jBmC;EAAW,iBAAA;CH8jB9C;AG7jBmC;EAAW,iBAAA;CHgkB9C;AG/jBmC;EAAW,iBAAA;CHkkB9C;AGjkBmC;EAAW,iBAAA;CHokB9C;AGnkBmC;EAAW,iBAAA;CHskB9C;AGrkBmC;EAAW,iBAAA;CHwkB9C;AGvkBmC;EAAW,iBAAA;CH0kB9C;AGzkBmC;EAAW,iBAAA;CH4kB9C;AG3kBmC;EAAW,iBAAA;CH8kB9C;AG7kBmC;EAAW,iBAAA;CHglB9C;AG/kBmC;EAAW,iBAAA;CHklB9C;AGjlBmC;EAAW,iBAAA;CHolB9C;AGnlBmC;EAAW,iBAAA;CHslB9C;AGrlBmC;EAAW,iBAAA;CHwlB9C;AGvlBmC;EAAW,iBAAA;CH0lB9C;AGzlBmC;EAAW,iBAAA;CH4lB9C;AG3lBmC;EAAW,iBAAA;CH8lB9C;AG7lBmC;EAAW,iBAAA;CHgmB9C;AG/lBmC;EAAW,iBAAA;CHkmB9C;AGjmBmC;EAAW,iBAAA;CHomB9C;AGnmBmC;EAAW,iBAAA;CHsmB9C;AGrmBmC;EAAW,iBAAA;CHwmB9C;AGvmBmC;EAAW,iBAAA;CH0mB9C;AGzmBmC;EAAW,iBAAA;CH4mB9C;AG3mBmC;EAAW,iBAAA;CH8mB9C;AG7mBmC;EAAW,iBAAA;CHgnB9C;AG/mBmC;EAAW,iBAAA;CHknB9C;AGjnBmC;EAAW,iBAAA;CHonB9C;AGnnBmC;EAAW,iBAAA;CHsnB9C;AGrnBmC;EAAW,iBAAA;CHwnB9C;AGvnBmC;EAAW,iBAAA;CH0nB9C;AGznBmC;EAAW,iBAAA;CH4nB9C;AG3nBmC;EAAW,iBAAA;CH8nB9C;AG7nBmC;EAAW,iBAAA;CHgoB9C;AG/nBmC;EAAW,iBAAA;CHkoB9C;AGjoBmC;EAAW,iBAAA;CHooB9C;AGnoBmC;EAAW,iBAAA;CHsoB9C;AGroBmC;EAAW,iBAAA;CHwoB9C;AG/nBmC;EAAW,iBAAA;CHkoB9C;AGjoBmC;EAAW,iBAAA;CHooB9C;AGnoBmC;EAAW,iBAAA;CHsoB9C;AGroBmC;EAAW,iBAAA;CHwoB9C;AGvoBmC;EAAW,iBAAA;CH0oB9C;AGzoBmC;EAAW,iBAAA;CH4oB9C;AG3oBmC;EAAW,iBAAA;CH8oB9C;AG7oBmC;EAAW,iBAAA;CHgpB9C;AG/oBmC;EAAW,iBAAA;CHkpB9C;AGjpBmC;EAAW,iBAAA;CHopB9C;AGnpBmC;EAAW,iBAAA;CHspB9C;AGrpBmC;EAAW,iBAAA;CHwpB9C;AGvpBmC;EAAW,iBAAA;CH0pB9C;AGzpBmC;EAAW,iBAAA;CH4pB9C;AG3pBmC;EAAW,iBAAA;CH8pB9C;AG7pBmC;EAAW,iBAAA;CHgqB9C;AG/pBmC;EAAW,iBAAA;CHkqB9C;AGjqBmC;EAAW,iBAAA;CHoqB9C;AGnqBmC;EAAW,iBAAA;CHsqB9C;AGrqBmC;EAAW,iBAAA;CHwqB9C;AGvqBmC;EAAW,iBAAA;CH0qB9C;AGzqBmC;EAAW,iBAAA;CH4qB9C;AG3qBmC;EAAW,iBAAA;CH8qB9C;AG7qBmC;EAAW,iBAAA;CHgrB9C;AG/qBmC;EAAW,iBAAA;CHkrB9C;AGjrBmC;EAAW,iBAAA;CHorB9C;AGnrBmC;EAAW,iBAAA;CHsrB9C;AGrrBmC;EAAW,iBAAA;CHwrB9C;AGvrBmC;EAAW,iBAAA;CH0rB9C;AGzrBmC;EAAW,iBAAA;CH4rB9C;AG3rBmC;EAAW,iBAAA;CH8rB9C;AG7rBmC;EAAW,iBAAA;CHgsB9C;AG/rBmC;EAAW,iBAAA;CHksB9C;AGjsBmC;EAAW,iBAAA;CHosB9C;AGnsBmC;EAAW,iBAAA;CHssB9C;AGrsBmC;EAAW,iBAAA;CHwsB9C;AGvsBmC;EAAW,iBAAA;CH0sB9C;AGzsBmC;EAAW,iBAAA;CH4sB9C;AG3sBmC;EAAW,iBAAA;CH8sB9C;AG7sBmC;EAAW,iBAAA;CHgtB9C;AG/sBmC;EAAW,iBAAA;CHktB9C;AGjtBmC;EAAW,iBAAA;CHotB9C;AGntBmC;EAAW,iBAAA;CHstB9C;AGrtBmC;EAAW,iBAAA;CHwtB9C;AGvtBmC;EAAW,iBAAA;CH0tB9C;AGztBmC;EAAW,iBAAA;CH4tB9C;AG3tBmC;EAAW,iBAAA;CH8tB9C;AG7tBmC;EAAW,iBAAA;CHguB9C;AG/tBmC;EAAW,iBAAA;CHkuB9C;AGjuBmC;EAAW,iBAAA;CHouB9C;AGnuBmC;EAAW,iBAAA;CHsuB9C;AGruBmC;EAAW,iBAAA;CHwuB9C;AGvuBmC;EAAW,iBAAA;CH0uB9C;AGzuBmC;EAAW,iBAAA;CH4uB9C;AG3uBmC;EAAW,iBAAA;CH8uB9C;AG7uBmC;EAAW,iBAAA;CHgvB9C;AIthCD;ECgEE,+BAAA;EACG,4BAAA;EACK,uBAAA;CLy9BT;AIxhCD;;EC6DE,+BAAA;EACG,4BAAA;EACK,uBAAA;CL+9BT;AIthCD;EACE,gBAAA;EACA,8CAAA;CJwhCD;AIrhCD;EACE,4DAAA;EACA,gBAAA;EACA,wBAAA;EACA,eAAA;EACA,uBAAA;CJuhCD;AInhCD;;;;EAIE,qBAAA;EACA,mBAAA;EACA,qBAAA;CJqhCD;AI/gCD;EACE,eAAA;EACA,sBAAA;CJihCD;AI/gCC;;EAEE,eAAA;EACA,2BAAA;CJihCH;AI9gCC;EErDA,qBAAA;EAEA,2CAAA;EACA,qBAAA;CNqkCD;AIxgCD;EACE,UAAA;CJ0gCD;AIpgCD;EACE,uBAAA;CJsgCD;AIlgCD;;;;;EGvEE,eAAA;EACA,gBAAA;EACA,aAAA;CPglCD;AItgCD;EACE,mBAAA;CJwgCD;AIlgCD;EACE,aAAA;EACA,wBAAA;EACA,uBAAA;EACA,uBAAA;EACA,mBAAA;EC6FA,yCAAA;EACK,oCAAA;EACG,iCAAA;EEvLR,sBAAA;EACA,gBAAA;EACA,aAAA;CPgmCD;AIlgCD;EACE,mBAAA;CJogCD;AI9/BD;EACE,iBAAA;EACA,oBAAA;EACA,UAAA;EACA,8BAAA;CJggCD;AIx/BD;EACE,mBAAA;EACA,WAAA;EACA,YAAA;EACA,aAAA;EACA,WAAA;EACA,iBAAA;EACA,uBAAA;EACA,UAAA;CJ0/BD;AIl/BC;;EAEE,iBAAA;EACA,YAAA;EACA,aAAA;EACA,UAAA;EACA,kBAAA;EACA,WAAA;CJo/BH;AIz+BD;EACE,gBAAA;CJ2+BD;AQloCD;;;;;;;;;;;;EAEE,qBAAA;EACA,iBAAA;EACA,iBAAA;EACA,eAAA;CR8oCD;AQnpCD;;;;;;;;;;;;;;;;;;;;;;;;EASI,oBAAA;EACA,eAAA;EACA,eAAA;CRoqCH;AQhqCD;;;;;;EAGE,iBAAA;EACA,oBAAA;CRqqCD;AQzqCD;;;;;;;;;;;;EAQI,eAAA;CR+qCH;AQ5qCD;;;;;;EAGE,iBAAA;EACA,oBAAA;CRirCD;AQrrCD;;;;;;;;;;;;EAQI,eAAA;CR2rCH;AQvrCD;;EAAU,gBAAA;CR2rCT;AQ1rCD;;EAAU,gBAAA;CR8rCT;AQ7rCD;;EAAU,gBAAA;CRisCT;AQhsCD;;EAAU,gBAAA;CRosCT;AQnsCD;;EAAU,gBAAA;CRusCT;AQtsCD;;EAAU,gBAAA;CR0sCT;AQpsCD;EACE,iBAAA;CRssCD;AQnsCD;EACE,oBAAA;EACA,gBAAA;EACA,iBAAA;EACA,iBAAA;CRqsCD;AQhsCD;EAwOA;IA1OI,gBAAA;GRssCD;CACF;AQ9rCD;;EAEE,eAAA;CRgsCD;AQ7rCD;;EAEE,0BAAA;EACA,cAAA;CR+rCD;AQ3rCD;EAAuB,iBAAA;CR8rCtB;AQ7rCD;EAAuB,kBAAA;CRgsCtB;AQ/rCD;EAAuB,mBAAA;CRksCtB;AQjsCD;EAAuB,oBAAA;CRosCtB;AQnsCD;EAAuB,oBAAA;CRssCtB;AQnsCD;EAAuB,0BAAA;CRssCtB;AQrsCD;EAAuB,0BAAA;CRwsCtB;AQvsCD;EAAuB,2BAAA;CR0sCtB;AQvsCD;EACE,eAAA;CRysCD;AQvsCD;ECrGE,eAAA;CT+yCD;AS9yCC;;EAEE,eAAA;CTgzCH;AQ3sCD;ECxGE,eAAA;CTszCD;ASrzCC;;EAEE,eAAA;CTuzCH;AQ/sCD;EC3GE,eAAA;CT6zCD;AS5zCC;;EAEE,eAAA;CT8zCH;AQntCD;EC9GE,eAAA;CTo0CD;ASn0CC;;EAEE,eAAA;CTq0CH;AQvtCD;ECjHE,eAAA;CT20CD;AS10CC;;EAEE,eAAA;CT40CH;AQvtCD;EAGE,YAAA;EE3HA,0BAAA;CVm1CD;AUl1CC;;EAEE,0BAAA;CVo1CH;AQztCD;EE9HE,0BAAA;CV01CD;AUz1CC;;EAEE,0BAAA;CV21CH;AQ7tCD;EEjIE,0BAAA;CVi2CD;AUh2CC;;EAEE,0BAAA;CVk2CH;AQjuCD;EEpIE,0BAAA;CVw2CD;AUv2CC;;EAEE,0BAAA;CVy2CH;AQruCD;EEvIE,0BAAA;CV+2CD;AU92CC;;EAEE,0BAAA;CVg3CH;AQpuCD;EACE,oBAAA;EACA,oBAAA;EACA,iCAAA;CRsuCD;AQ9tCD;;EAEE,cAAA;EACA,oBAAA;CRguCD;AQnuCD;;;;EAMI,iBAAA;CRmuCH;AQ5tCD;EACE,gBAAA;EACA,iBAAA;CR8tCD;AQ1tCD;EALE,gBAAA;EACA,iBAAA;EAMA,kBAAA;CR6tCD;AQ/tCD;EAKI,sBAAA;EACA,kBAAA;EACA,mBAAA;CR6tCH;AQxtCD;EACE,cAAA;EACA,oBAAA;CR0tCD;AQxtCD;;EAEE,wBAAA;CR0tCD;AQxtCD;EACE,kBAAA;CR0tCD;AQxtCD;EACE,eAAA;CR0tCD;AQjsCD;EA6EA;IAvFM,YAAA;IACA,aAAA;IACA,YAAA;IACA,kBAAA;IGtNJ,iBAAA;IACA,wBAAA;IACA,oBAAA;GXs6CC;EQ9nCH;IAhFM,mBAAA;GRitCH;CACF;AQxsCD;;EAGE,aAAA;EACA,kCAAA;CRysCD;AQvsCD;EACE,eAAA;EA9IqB,0BAAA;CRw1CtB;AQrsCD;EACE,mBAAA;EACA,iBAAA;EACA,kBAAA;EACA,+BAAA;CRusCD;AQlsCG;;;EACE,iBAAA;CRssCL;AQhtCD;;;EAmBI,eAAA;EACA,eAAA;EACA,wBAAA;EACA,eAAA;CRksCH;AQhsCG;;;EACE,uBAAA;CRosCL;AQ5rCD;;EAEE,oBAAA;EACA,gBAAA;EACA,gCAAA;EACA,eAAA;EACA,kBAAA;CR8rCD;AQxrCG;;;;;;EAAW,YAAA;CRgsCd;AQ/rCG;;;;;;EACE,uBAAA;CRssCL;AQhsCD;EACE,oBAAA;EACA,mBAAA;EACA,wBAAA;CRksCD;AYx+CD;;;;EAIE,+DAAA;CZ0+CD;AYt+CD;EACE,iBAAA;EACA,eAAA;EACA,eAAA;EACA,0BAAA;EACA,mBAAA;CZw+CD;AYp+CD;EACE,iBAAA;EACA,eAAA;EACA,YAAA;EACA,uBAAA;EACA,mBAAA;EACA,uDAAA;UAAA,+CAAA;CZs+CD;AY5+CD;EASI,WAAA;EACA,gBAAA;EACA,kBAAA;EACA,yBAAA;UAAA,iBAAA;CZs+CH;AYj+CD;EACE,eAAA;EACA,eAAA;EACA,iBAAA;EACA,gBAAA;EACA,wBAAA;EACA,sBAAA;EACA,sBAAA;EACA,eAAA;EACA,0BAAA;EACA,uBAAA;EACA,mBAAA;CZm+CD;AY9+CD;EAeI,WAAA;EACA,mBAAA;EACA,eAAA;EACA,sBAAA;EACA,8BAAA;EACA,iBAAA;CZk+CH;AY79CD;EACE,kBAAA;EACA,mBAAA;CZ+9CD;AazhDD;ECHE,mBAAA;EACA,kBAAA;EACA,mBAAA;EACA,oBAAA;Cd+hDD;AazhDC;EAqEF;IAvEI,aAAA;Gb+hDD;CACF;Aa3hDC;EAkEF;IApEI,aAAA;GbiiDD;CACF;Aa7hDD;EA+DA;IAjEI,cAAA;GbmiDD;CACF;Aa1hDD;ECvBE,mBAAA;EACA,kBAAA;EACA,mBAAA;EACA,oBAAA;CdojDD;AavhDD;ECvBE,mBAAA;EACA,oBAAA;CdijDD;AejjDG;EACE,mBAAA;EAEA,gBAAA;EAEA,mBAAA;EACA,oBAAA;CfijDL;AejiDG;EACE,YAAA;CfmiDL;Ae5hDC;EACE,YAAA;Cf8hDH;Ae/hDC;EACE,oBAAA;CfiiDH;AeliDC;EACE,oBAAA;CfoiDH;AeriDC;EACE,WAAA;CfuiDH;AexiDC;EACE,oBAAA;Cf0iDH;Ae3iDC;EACE,oBAAA;Cf6iDH;Ae9iDC;EACE,WAAA;CfgjDH;AejjDC;EACE,oBAAA;CfmjDH;AepjDC;EACE,oBAAA;CfsjDH;AevjDC;EACE,WAAA;CfyjDH;Ae1jDC;EACE,oBAAA;Cf4jDH;Ae7jDC;EACE,mBAAA;Cf+jDH;AejjDC;EACE,YAAA;CfmjDH;AepjDC;EACE,oBAAA;CfsjDH;AevjDC;EACE,oBAAA;CfyjDH;Ae1jDC;EACE,WAAA;Cf4jDH;Ae7jDC;EACE,oBAAA;Cf+jDH;AehkDC;EACE,oBAAA;CfkkDH;AenkDC;EACE,WAAA;CfqkDH;AetkDC;EACE,oBAAA;CfwkDH;AezkDC;EACE,oBAAA;Cf2kDH;Ae5kDC;EACE,WAAA;Cf8kDH;Ae/kDC;EACE,oBAAA;CfilDH;AellDC;EACE,mBAAA;CfolDH;AehlDC;EACE,YAAA;CfklDH;AelmDC;EACE,WAAA;CfomDH;AermDC;EACE,mBAAA;CfumDH;AexmDC;EACE,mBAAA;Cf0mDH;Ae3mDC;EACE,UAAA;Cf6mDH;Ae9mDC;EACE,mBAAA;CfgnDH;AejnDC;EACE,mBAAA;CfmnDH;AepnDC;EACE,UAAA;CfsnDH;AevnDC;EACE,mBAAA;CfynDH;Ae1nDC;EACE,mBAAA;Cf4nDH;Ae7nDC;EACE,UAAA;Cf+nDH;AehoDC;EACE,mBAAA;CfkoDH;AenoDC;EACE,kBAAA;CfqoDH;AejoDC;EACE,WAAA;CfmoDH;AernDC;EACE,kBAAA;CfunDH;AexnDC;EACE,0BAAA;Cf0nDH;Ae3nDC;EACE,0BAAA;Cf6nDH;Ae9nDC;EACE,iBAAA;CfgoDH;AejoDC;EACE,0BAAA;CfmoDH;AepoDC;EACE,0BAAA;CfsoDH;AevoDC;EACE,iBAAA;CfyoDH;Ae1oDC;EACE,0BAAA;Cf4oDH;Ae7oDC;EACE,0BAAA;Cf+oDH;AehpDC;EACE,iBAAA;CfkpDH;AenpDC;EACE,0BAAA;CfqpDH;AetpDC;EACE,yBAAA;CfwpDH;AezpDC;EACE,gBAAA;Cf2pDH;Aa3pDD;EElCI;IACE,YAAA;GfgsDH;EezrDD;IACE,YAAA;Gf2rDD;Ee5rDD;IACE,oBAAA;Gf8rDD;Ee/rDD;IACE,oBAAA;GfisDD;EelsDD;IACE,WAAA;GfosDD;EersDD;IACE,oBAAA;GfusDD;EexsDD;IACE,oBAAA;Gf0sDD;Ee3sDD;IACE,WAAA;Gf6sDD;Ee9sDD;IACE,oBAAA;GfgtDD;EejtDD;IACE,oBAAA;GfmtDD;EeptDD;IACE,WAAA;GfstDD;EevtDD;IACE,oBAAA;GfytDD;Ee1tDD;IACE,mBAAA;Gf4tDD;Ee9sDD;IACE,YAAA;GfgtDD;EejtDD;IACE,oBAAA;GfmtDD;EeptDD;IACE,oBAAA;GfstDD;EevtDD;IACE,WAAA;GfytDD;Ee1tDD;IACE,oBAAA;Gf4tDD;Ee7tDD;IACE,oBAAA;Gf+tDD;EehuDD;IACE,WAAA;GfkuDD;EenuDD;IACE,oBAAA;GfquDD;EetuDD;IACE,oBAAA;GfwuDD;EezuDD;IACE,WAAA;Gf2uDD;Ee5uDD;IACE,oBAAA;Gf8uDD;Ee/uDD;IACE,mBAAA;GfivDD;Ee7uDD;IACE,YAAA;Gf+uDD;Ee/vDD;IACE,WAAA;GfiwDD;EelwDD;IACE,mBAAA;GfowDD;EerwDD;IACE,mBAAA;GfuwDD;EexwDD;IACE,UAAA;Gf0wDD;Ee3wDD;IACE,mBAAA;Gf6wDD;Ee9wDD;IACE,mBAAA;GfgxDD;EejxDD;IACE,UAAA;GfmxDD;EepxDD;IACE,mBAAA;GfsxDD;EevxDD;IACE,mBAAA;GfyxDD;Ee1xDD;IACE,UAAA;Gf4xDD;Ee7xDD;IACE,mBAAA;Gf+xDD;EehyDD;IACE,kBAAA;GfkyDD;Ee9xDD;IACE,WAAA;GfgyDD;EelxDD;IACE,kBAAA;GfoxDD;EerxDD;IACE,0BAAA;GfuxDD;EexxDD;IACE,0BAAA;Gf0xDD;Ee3xDD;IACE,iBAAA;Gf6xDD;Ee9xDD;IACE,0BAAA;GfgyDD;EejyDD;IACE,0BAAA;GfmyDD;EepyDD;IACE,iBAAA;GfsyDD;EevyDD;IACE,0BAAA;GfyyDD;Ee1yDD;IACE,0BAAA;Gf4yDD;Ee7yDD;IACE,iBAAA;Gf+yDD;EehzDD;IACE,0BAAA;GfkzDD;EenzDD;IACE,yBAAA;GfqzDD;EetzDD;IACE,gBAAA;GfwzDD;CACF;AahzDD;EE3CI;IACE,YAAA;Gf81DH;Eev1DD;IACE,YAAA;Gfy1DD;Ee11DD;IACE,oBAAA;Gf41DD;Ee71DD;IACE,oBAAA;Gf+1DD;Eeh2DD;IACE,WAAA;Gfk2DD;Een2DD;IACE,oBAAA;Gfq2DD;Eet2DD;IACE,oBAAA;Gfw2DD;Eez2DD;IACE,WAAA;Gf22DD;Ee52DD;IACE,oBAAA;Gf82DD;Ee/2DD;IACE,oBAAA;Gfi3DD;Eel3DD;IACE,WAAA;Gfo3DD;Eer3DD;IACE,oBAAA;Gfu3DD;Eex3DD;IACE,mBAAA;Gf03DD;Ee52DD;IACE,YAAA;Gf82DD;Ee/2DD;IACE,oBAAA;Gfi3DD;Eel3DD;IACE,oBAAA;Gfo3DD;Eer3DD;IACE,WAAA;Gfu3DD;Eex3DD;IACE,oBAAA;Gf03DD;Ee33DD;IACE,oBAAA;Gf63DD;Ee93DD;IACE,WAAA;Gfg4DD;Eej4DD;IACE,oBAAA;Gfm4DD;Eep4DD;IACE,oBAAA;Gfs4DD;Eev4DD;IACE,WAAA;Gfy4DD;Ee14DD;IACE,oBAAA;Gf44DD;Ee74DD;IACE,mBAAA;Gf+4DD;Ee34DD;IACE,YAAA;Gf64DD;Ee75DD;IACE,WAAA;Gf+5DD;Eeh6DD;IACE,mBAAA;Gfk6DD;Een6DD;IACE,mBAAA;Gfq6DD;Eet6DD;IACE,UAAA;Gfw6DD;Eez6DD;IACE,mBAAA;Gf26DD;Ee56DD;IACE,mBAAA;Gf86DD;Ee/6DD;IACE,UAAA;Gfi7DD;Eel7DD;IACE,mBAAA;Gfo7DD;Eer7DD;IACE,mBAAA;Gfu7DD;Eex7DD;IACE,UAAA;Gf07DD;Ee37DD;IACE,mBAAA;Gf67DD;Ee97DD;IACE,kBAAA;Gfg8DD;Ee57DD;IACE,WAAA;Gf87DD;Eeh7DD;IACE,kBAAA;Gfk7DD;Een7DD;IACE,0BAAA;Gfq7DD;Eet7DD;IACE,0BAAA;Gfw7DD;Eez7DD;IACE,iBAAA;Gf27DD;Ee57DD;IACE,0BAAA;Gf87DD;Ee/7DD;IACE,0BAAA;Gfi8DD;Eel8DD;IACE,iBAAA;Gfo8DD;Eer8DD;IACE,0BAAA;Gfu8DD;Eex8DD;IACE,0BAAA;Gf08DD;Ee38DD;IACE,iBAAA;Gf68DD;Ee98DD;IACE,0BAAA;Gfg9DD;Eej9DD;IACE,yBAAA;Gfm9DD;Eep9DD;IACE,gBAAA;Gfs9DD;CACF;Aa38DD;EE9CI;IACE,YAAA;Gf4/DH;Eer/DD;IACE,YAAA;Gfu/DD;Eex/DD;IACE,oBAAA;Gf0/DD;Ee3/DD;IACE,oBAAA;Gf6/DD;Ee9/DD;IACE,WAAA;GfggED;EejgED;IACE,oBAAA;GfmgED;EepgED;IACE,oBAAA;GfsgED;EevgED;IACE,WAAA;GfygED;Ee1gED;IACE,oBAAA;Gf4gED;Ee7gED;IACE,oBAAA;Gf+gED;EehhED;IACE,WAAA;GfkhED;EenhED;IACE,oBAAA;GfqhED;EethED;IACE,mBAAA;GfwhED;Ee1gED;IACE,YAAA;Gf4gED;Ee7gED;IACE,oBAAA;Gf+gED;EehhED;IACE,oBAAA;GfkhED;EenhED;IACE,WAAA;GfqhED;EethED;IACE,oBAAA;GfwhED;EezhED;IACE,oBAAA;Gf2hED;Ee5hED;IACE,WAAA;Gf8hED;Ee/hED;IACE,oBAAA;GfiiED;EeliED;IACE,oBAAA;GfoiED;EeriED;IACE,WAAA;GfuiED;EexiED;IACE,oBAAA;Gf0iED;Ee3iED;IACE,mBAAA;Gf6iED;EeziED;IACE,YAAA;Gf2iED;Ee3jED;IACE,WAAA;Gf6jED;Ee9jED;IACE,mBAAA;GfgkED;EejkED;IACE,mBAAA;GfmkED;EepkED;IACE,UAAA;GfskED;EevkED;IACE,mBAAA;GfykED;Ee1kED;IACE,mBAAA;Gf4kED;Ee7kED;IACE,UAAA;Gf+kED;EehlED;IACE,mBAAA;GfklED;EenlED;IACE,mBAAA;GfqlED;EetlED;IACE,UAAA;GfwlED;EezlED;IACE,mBAAA;Gf2lED;Ee5lED;IACE,kBAAA;Gf8lED;Ee1lED;IACE,WAAA;Gf4lED;Ee9kED;IACE,kBAAA;GfglED;EejlED;IACE,0BAAA;GfmlED;EeplED;IACE,0BAAA;GfslED;EevlED;IACE,iBAAA;GfylED;Ee1lED;IACE,0BAAA;Gf4lED;Ee7lED;IACE,0BAAA;Gf+lED;EehmED;IACE,iBAAA;GfkmED;EenmED;IACE,0BAAA;GfqmED;EetmED;IACE,0BAAA;GfwmED;EezmED;IACE,iBAAA;Gf2mED;Ee5mED;IACE,0BAAA;Gf8mED;Ee/mED;IACE,yBAAA;GfinED;EelnED;IACE,gBAAA;GfonED;CACF;AgBxrED;EACE,8BAAA;ChB0rED;AgBxrED;EACE,iBAAA;EACA,oBAAA;EACA,eAAA;EACA,iBAAA;ChB0rED;AgBxrED;EACE,iBAAA;ChB0rED;AgBprED;EACE,YAAA;EACA,gBAAA;EACA,oBAAA;ChBsrED;AgBzrED;;;;;;EAWQ,aAAA;EACA,wBAAA;EACA,oBAAA;EACA,2BAAA;ChBsrEP;AgBpsED;EAoBI,uBAAA;EACA,8BAAA;ChBmrEH;AgBxsED;;;;;;EA8BQ,cAAA;ChBkrEP;AgBhtED;EAoCI,2BAAA;ChB+qEH;AgBntED;EAyCI,uBAAA;ChB6qEH;AgBtqED;;;;;;EAOQ,aAAA;ChBuqEP;AgB5pED;EACE,uBAAA;ChB8pED;AgB/pED;;;;;;EAQQ,uBAAA;ChB+pEP;AgBvqED;;EAeM,yBAAA;ChB4pEL;AgBlpED;EAEI,0BAAA;ChBmpEH;AgB1oED;EAEI,0BAAA;ChB2oEH;AgBloED;EACE,iBAAA;EACA,YAAA;EACA,sBAAA;ChBooED;AgB/nEG;;EACE,iBAAA;EACA,YAAA;EACA,oBAAA;ChBkoEL;AiB9wEC;;;;;;;;;;;;EAOI,0BAAA;CjBqxEL;AiB/wEC;;;;;EAMI,0BAAA;CjBgxEL;AiBnyEC;;;;;;;;;;;;EAOI,0BAAA;CjB0yEL;AiBpyEC;;;;;EAMI,0BAAA;CjBqyEL;AiBxzEC;;;;;;;;;;;;EAOI,0BAAA;CjB+zEL;AiBzzEC;;;;;EAMI,0BAAA;CjB0zEL;AiB70EC;;;;;;;;;;;;EAOI,0BAAA;CjBo1EL;AiB90EC;;;;;EAMI,0BAAA;CjB+0EL;AiBl2EC;;;;;;;;;;;;EAOI,0BAAA;CjBy2EL;AiBn2EC;;;;;EAMI,0BAAA;CjBo2EL;AgBltED;EACE,iBAAA;EACA,kBAAA;ChBotED;AgBvpED;EACA;IA3DI,YAAA;IACA,oBAAA;IACA,mBAAA;IACA,6CAAA;IACA,uBAAA;GhBqtED;EgB9pEH;IAnDM,iBAAA;GhBotEH;EgBjqEH;;;;;;IA1CY,oBAAA;GhBmtET;EgBzqEH;IAlCM,UAAA;GhB8sEH;EgB5qEH;;;;;;IAzBY,eAAA;GhB6sET;EgBprEH;;;;;;IArBY,gBAAA;GhBitET;EgB5rEH;;;;IARY,iBAAA;GhB0sET;CACF;AkBp6ED;EACE,WAAA;EACA,UAAA;EACA,UAAA;EAIA,aAAA;ClBm6ED;AkBh6ED;EACE,eAAA;EACA,YAAA;EACA,WAAA;EACA,oBAAA;EACA,gBAAA;EACA,qBAAA;EACA,eAAA;EACA,UAAA;EACA,iCAAA;ClBk6ED;AkB/5ED;EACE,sBAAA;EACA,gBAAA;EACA,mBAAA;EACA,kBAAA;ClBi6ED;AkBt5ED;Eb4BE,+BAAA;EACG,4BAAA;EACK,uBAAA;CL63ET;AkBt5ED;;EAEE,gBAAA;EACA,mBAAA;EACA,oBAAA;ClBw5ED;AkBr5ED;EACE,eAAA;ClBu5ED;AkBn5ED;EACE,eAAA;EACA,YAAA;ClBq5ED;AkBj5ED;;EAEE,aAAA;ClBm5ED;AkB/4ED;;;EZvEE,qBAAA;EAEA,2CAAA;EACA,qBAAA;CN09ED;AkB/4ED;EACE,eAAA;EACA,iBAAA;EACA,gBAAA;EACA,wBAAA;EACA,eAAA;ClBi5ED;AkBv3ED;EACE,eAAA;EACA,YAAA;EACA,aAAA;EACA,kBAAA;EACA,gBAAA;EACA,wBAAA;EACA,eAAA;EACA,uBAAA;EACA,uBAAA;EACA,uBAAA;EACA,mBAAA;EbxDA,yDAAA;EACQ,iDAAA;EAyHR,uFAAA;EACK,0EAAA;EACG,uEAAA;CL0zET;AmBl8EC;EACE,sBAAA;EACA,WAAA;EdUF,uFAAA;EACQ,+EAAA;CL27ET;AK15EC;EACE,YAAA;EACA,WAAA;CL45EH;AK15EC;EAA0B,YAAA;CL65E3B;AK55EC;EAAgC,YAAA;CL+5EjC;AkBn4EC;EACE,UAAA;EACA,8BAAA;ClBq4EH;AkB73EC;;;EAGE,0BAAA;EACA,WAAA;ClB+3EH;AkB53EC;;EAEE,oBAAA;ClB83EH;AkB13EC;EACE,aAAA;ClB43EH;AkBh3ED;EACE,yBAAA;ClBk3ED;AkB10ED;EAtBI;;;;IACE,kBAAA;GlBs2EH;EkBn2EC;;;;;;;;IAEE,kBAAA;GlB22EH;EkBx2EC;;;;;;;;IAEE,kBAAA;GlBg3EH;CACF;AkBt2ED;EACE,oBAAA;ClBw2ED;AkBh2ED;;EAEE,mBAAA;EACA,eAAA;EACA,iBAAA;EACA,oBAAA;ClBk2ED;AkBv2ED;;EAQI,iBAAA;EACA,mBAAA;EACA,iBAAA;EACA,oBAAA;EACA,gBAAA;ClBm2EH;AkBh2ED;;;;EAIE,mBAAA;EACA,mBAAA;EACA,mBAAA;ClBk2ED;AkB/1ED;;EAEE,iBAAA;ClBi2ED;AkB71ED;;EAEE,mBAAA;EACA,sBAAA;EACA,mBAAA;EACA,iBAAA;EACA,uBAAA;EACA,oBAAA;EACA,gBAAA;ClB+1ED;AkB71ED;;EAEE,cAAA;EACA,kBAAA;ClB+1ED;AkBt1EC;;;;;;EAGE,oBAAA;ClB21EH;AkBr1EC;;;;EAEE,oBAAA;ClBy1EH;AkBn1EC;;;;EAGI,oBAAA;ClBs1EL;AkB30ED;EAEE,iBAAA;EACA,oBAAA;EAEA,iBAAA;EACA,iBAAA;ClB20ED;AkBz0EC;;EAEE,gBAAA;EACA,iBAAA;ClB20EH;AkB9zED;ECnQE,aAAA;EACA,kBAAA;EACA,gBAAA;EACA,iBAAA;EACA,mBAAA;CnBokFD;AmBlkFC;EACE,aAAA;EACA,kBAAA;CnBokFH;AmBjkFC;;EAEE,aAAA;CnBmkFH;AkB10ED;EAEI,aAAA;EACA,kBAAA;EACA,gBAAA;EACA,iBAAA;EACA,mBAAA;ClB20EH;AkBj1ED;EASI,aAAA;EACA,kBAAA;ClB20EH;AkBr1ED;;EAcI,aAAA;ClB20EH;AkBz1ED;EAiBI,aAAA;EACA,iBAAA;EACA,kBAAA;EACA,gBAAA;EACA,iBAAA;ClB20EH;AkBv0ED;EC/RE,aAAA;EACA,mBAAA;EACA,gBAAA;EACA,uBAAA;EACA,mBAAA;CnBymFD;AmBvmFC;EACE,aAAA;EACA,kBAAA;CnBymFH;AmBtmFC;;EAEE,aAAA;CnBwmFH;AkBn1ED;EAEI,aAAA;EACA,mBAAA;EACA,gBAAA;EACA,uBAAA;EACA,mBAAA;ClBo1EH;AkB11ED;EASI,aAAA;EACA,kBAAA;ClBo1EH;AkB91ED;;EAcI,aAAA;ClBo1EH;AkBl2ED;EAiBI,aAAA;EACA,iBAAA;EACA,mBAAA;EACA,gBAAA;EACA,uBAAA;ClBo1EH;AkB30ED;EAEE,mBAAA;ClB40ED;AkB90ED;EAMI,sBAAA;ClB20EH;AkBv0ED;EACE,mBAAA;EACA,OAAA;EACA,SAAA;EACA,WAAA;EACA,eAAA;EACA,YAAA;EACA,aAAA;EACA,kBAAA;EACA,mBAAA;EACA,qBAAA;ClBy0ED;AkBv0ED;;;EAGE,YAAA;EACA,aAAA;EACA,kBAAA;ClBy0ED;AkBv0ED;;;EAGE,YAAA;EACA,aAAA;EACA,kBAAA;ClBy0ED;AkBr0ED;;;;;;;;;;EC1ZI,eAAA;CnB2uFH;AkBj1ED;ECtZI,sBAAA;Ed+CF,yDAAA;EACQ,iDAAA;CL4rFT;AmB1uFG;EACE,sBAAA;Ed4CJ,0EAAA;EACQ,kEAAA;CLisFT;AkB31ED;EC5YI,eAAA;EACA,sBAAA;EACA,0BAAA;CnB0uFH;AkBh2ED;ECtYI,eAAA;CnByuFH;AkBh2ED;;;;;;;;;;EC7ZI,eAAA;CnBywFH;AkB52ED;ECzZI,sBAAA;Ed+CF,yDAAA;EACQ,iDAAA;CL0tFT;AmBxwFG;EACE,sBAAA;Ed4CJ,0EAAA;EACQ,kEAAA;CL+tFT;AkBt3ED;EC/YI,eAAA;EACA,sBAAA;EACA,0BAAA;CnBwwFH;AkB33ED;ECzYI,eAAA;CnBuwFH;AkB33ED;;;;;;;;;;EChaI,eAAA;CnBuyFH;AkBv4ED;EC5ZI,sBAAA;Ed+CF,yDAAA;EACQ,iDAAA;CLwvFT;AmBtyFG;EACE,sBAAA;Ed4CJ,0EAAA;EACQ,kEAAA;CL6vFT;AkBj5ED;EClZI,eAAA;EACA,sBAAA;EACA,0BAAA;CnBsyFH;AkBt5ED;EC5YI,eAAA;CnBqyFH;AkBl5EC;EACE,UAAA;ClBo5EH;AkBl5EC;EACE,OAAA;ClBo5EH;AkB14ED;EACE,eAAA;EACA,gBAAA;EACA,oBAAA;EACA,eAAA;ClB44ED;AkBzzED;EAwEA;IAtIM,sBAAA;IACA,iBAAA;IACA,uBAAA;GlB23EH;EkBvvEH;IA/HM,sBAAA;IACA,YAAA;IACA,uBAAA;GlBy3EH;EkB5vEH;IAxHM,sBAAA;GlBu3EH;EkB/vEH;IApHM,sBAAA;IACA,uBAAA;GlBs3EH;EkBnwEH;;;IA9GQ,YAAA;GlBs3EL;EkBxwEH;IAxGM,YAAA;GlBm3EH;EkB3wEH;IApGM,iBAAA;IACA,uBAAA;GlBk3EH;EkB/wEH;;IA5FM,sBAAA;IACA,cAAA;IACA,iBAAA;IACA,uBAAA;GlB+2EH;EkBtxEH;;IAtFQ,gBAAA;GlBg3EL;EkB1xEH;;IAjFM,mBAAA;IACA,eAAA;GlB+2EH;EkB/xEH;IA3EM,OAAA;GlB62EH;CACF;AkBn2ED;;;;EASI,cAAA;EACA,iBAAA;EACA,iBAAA;ClBg2EH;AkB32ED;;EAiBI,iBAAA;ClB81EH;AkB/2ED;EJthBE,mBAAA;EACA,oBAAA;Cdw4FD;AkB50EC;EAyBF;IAnCM,kBAAA;IACA,iBAAA;IACA,iBAAA;GlB01EH;CACF;AkB13ED;EAwCI,YAAA;ClBq1EH;AkBv0EC;EAUF;IAdQ,kBAAA;IACA,gBAAA;GlB+0EL;CACF;AkBr0EC;EAEF;IANQ,iBAAA;IACA,gBAAA;GlB60EL;CACF;AoBt6FD;EACE,sBAAA;EACA,iBAAA;EACA,oBAAA;EACA,mBAAA;EACA,uBAAA;EACA,+BAAA;MAAA,2BAAA;EACA,gBAAA;EACA,uBAAA;EACA,8BAAA;EACA,oBAAA;EC0CA,kBAAA;EACA,gBAAA;EACA,wBAAA;EACA,mBAAA;EhB+JA,0BAAA;EACG,uBAAA;EACC,sBAAA;EACI,kBAAA;CLiuFT;AoBz6FG;;;;;;EdrBF,qBAAA;EAEA,2CAAA;EACA,qBAAA;CNq8FD;AoB76FC;;;EAGE,YAAA;EACA,sBAAA;CpB+6FH;AoB56FC;;EAEE,WAAA;EACA,uBAAA;Ef2BF,yDAAA;EACQ,iDAAA;CLo5FT;AoB56FC;;;EAGE,oBAAA;EE7CF,cAAA;EAGA,0BAAA;EjB8DA,yBAAA;EACQ,iBAAA;CL65FT;AoB56FG;;EAEE,qBAAA;CpB86FL;AoBr6FD;EC3DE,YAAA;EACA,uBAAA;EACA,mBAAA;CrBm+FD;AqBj+FC;;EAEE,YAAA;EACA,0BAAA;EACI,sBAAA;CrBm+FP;AqBj+FC;EACE,YAAA;EACA,0BAAA;EACI,sBAAA;CrBm+FP;AqBj+FC;;;EAGE,YAAA;EACA,0BAAA;EACI,sBAAA;CrBm+FP;AqBj+FG;;;;;;;;;EAGE,YAAA;EACA,0BAAA;EACI,sBAAA;CrBy+FT;AqBt+FC;;;EAGE,uBAAA;CrBw+FH;AqBn+FG;;;;;;;;;EAGE,uBAAA;EACI,mBAAA;CrB2+FT;AoB19FD;ECZI,YAAA;EACA,uBAAA;CrBy+FH;AoB39FD;EC9DE,YAAA;EACA,0BAAA;EACA,sBAAA;CrB4hGD;AqB1hGC;;EAEE,YAAA;EACA,0BAAA;EACI,sBAAA;CrB4hGP;AqB1hGC;EACE,YAAA;EACA,0BAAA;EACI,sBAAA;CrB4hGP;AqB1hGC;;;EAGE,YAAA;EACA,0BAAA;EACI,sBAAA;CrB4hGP;AqB1hGG;;;;;;;;;EAGE,YAAA;EACA,0BAAA;EACI,sBAAA;CrBkiGT;AqB/hGC;;;EAGE,uBAAA;CrBiiGH;AqB5hGG;;;;;;;;;EAGE,0BAAA;EACI,sBAAA;CrBoiGT;AoBhhGD;ECfI,eAAA;EACA,uBAAA;CrBkiGH;AoBhhGD;EClEE,YAAA;EACA,0BAAA;EACA,sBAAA;CrBqlGD;AqBnlGC;;EAEE,YAAA;EACA,0BAAA;EACI,sBAAA;CrBqlGP;AqBnlGC;EACE,YAAA;EACA,0BAAA;EACI,sBAAA;CrBqlGP;AqBnlGC;;;EAGE,YAAA;EACA,0BAAA;EACI,sBAAA;CrBqlGP;AqBnlGG;;;;;;;;;EAGE,YAAA;EACA,0BAAA;EACI,sBAAA;CrB2lGT;AqBxlGC;;;EAGE,uBAAA;CrB0lGH;AqBrlGG;;;;;;;;;EAGE,0BAAA;EACI,sBAAA;CrB6lGT;AoBrkGD;ECnBI,eAAA;EACA,uBAAA;CrB2lGH;AoBrkGD;ECtEE,YAAA;EACA,0BAAA;EACA,sBAAA;CrB8oGD;AqB5oGC;;EAEE,YAAA;EACA,0BAAA;EACI,sBAAA;CrB8oGP;AqB5oGC;EACE,YAAA;EACA,0BAAA;EACI,sBAAA;CrB8oGP;AqB5oGC;;;EAGE,YAAA;EACA,0BAAA;EACI,sBAAA;CrB8oGP;AqB5oGG;;;;;;;;;EAGE,YAAA;EACA,0BAAA;EACI,sBAAA;CrBopGT;AqBjpGC;;;EAGE,uBAAA;CrBmpGH;AqB9oGG;;;;;;;;;EAGE,0BAAA;EACI,sBAAA;CrBspGT;AoB1nGD;ECvBI,eAAA;EACA,uBAAA;CrBopGH;AoB1nGD;EC1EE,YAAA;EACA,0BAAA;EACA,sBAAA;CrBusGD;AqBrsGC;;EAEE,YAAA;EACA,0BAAA;EACI,sBAAA;CrBusGP;AqBrsGC;EACE,YAAA;EACA,0BAAA;EACI,sBAAA;CrBusGP;AqBrsGC;;;EAGE,YAAA;EACA,0BAAA;EACI,sBAAA;CrBusGP;AqBrsGG;;;;;;;;;EAGE,YAAA;EACA,0BAAA;EACI,sBAAA;CrB6sGT;AqB1sGC;;;EAGE,uBAAA;CrB4sGH;AqBvsGG;;;;;;;;;EAGE,0BAAA;EACI,sBAAA;CrB+sGT;AoB/qGD;EC3BI,eAAA;EACA,uBAAA;CrB6sGH;AoB/qGD;EC9EE,YAAA;EACA,0BAAA;EACA,sBAAA;CrBgwGD;AqB9vGC;;EAEE,YAAA;EACA,0BAAA;EACI,sBAAA;CrBgwGP;AqB9vGC;EACE,YAAA;EACA,0BAAA;EACI,sBAAA;CrBgwGP;AqB9vGC;;;EAGE,YAAA;EACA,0BAAA;EACI,sBAAA;CrBgwGP;AqB9vGG;;;;;;;;;EAGE,YAAA;EACA,0BAAA;EACI,sBAAA;CrBswGT;AqBnwGC;;;EAGE,uBAAA;CrBqwGH;AqBhwGG;;;;;;;;;EAGE,0BAAA;EACI,sBAAA;CrBwwGT;AoBpuGD;EC/BI,eAAA;EACA,uBAAA;CrBswGH;AoB/tGD;EACE,eAAA;EACA,oBAAA;EACA,iBAAA;CpBiuGD;AoB/tGC;;;;;EAKE,8BAAA;EfnCF,yBAAA;EACQ,iBAAA;CLqwGT;AoBhuGC;;;;EAIE,0BAAA;CpBkuGH;AoBhuGC;;EAEE,eAAA;EACA,2BAAA;EACA,8BAAA;CpBkuGH;AoB9tGG;;;;EAEE,eAAA;EACA,sBAAA;CpBkuGL;AoBztGD;;ECxEE,mBAAA;EACA,gBAAA;EACA,uBAAA;EACA,mBAAA;CrBqyGD;AoB5tGD;;EC5EE,kBAAA;EACA,gBAAA;EACA,iBAAA;EACA,mBAAA;CrB4yGD;AoB/tGD;;EChFE,iBAAA;EACA,gBAAA;EACA,iBAAA;EACA,mBAAA;CrBmzGD;AoB9tGD;EACE,eAAA;EACA,YAAA;CpBguGD;AoB5tGD;EACE,gBAAA;CpB8tGD;AoBvtGC;;;EACE,YAAA;CpB2tGH;AuBr3GD;EACE,WAAA;ElBoLA,yCAAA;EACK,oCAAA;EACG,iCAAA;CLosGT;AuBx3GC;EACE,WAAA;CvB03GH;AuBt3GD;EACE,cAAA;CvBw3GD;AuBt3GC;EAAY,eAAA;CvBy3Gb;AuBx3GC;EAAY,mBAAA;CvB23Gb;AuB13GC;EAAY,yBAAA;CvB63Gb;AuB13GD;EACE,mBAAA;EACA,UAAA;EACA,iBAAA;ElBuKA,gDAAA;EACQ,2CAAA;KAAA,wCAAA;EAOR,mCAAA;EACQ,8BAAA;KAAA,2BAAA;EAGR,yCAAA;EACQ,oCAAA;KAAA,iCAAA;CL8sGT;AwBx5GD;EACE,sBAAA;EACA,SAAA;EACA,UAAA;EACA,iBAAA;EACA,uBAAA;EACA,uBAAA;EACA,yBAAA;EACA,oCAAA;EACA,mCAAA;CxB05GD;AwBt5GD;;EAEE,mBAAA;CxBw5GD;AwBp5GD;EACE,WAAA;CxBs5GD;AwBl5GD;EACE,mBAAA;EACA,UAAA;EACA,QAAA;EACA,cAAA;EACA,cAAA;EACA,YAAA;EACA,iBAAA;EACA,eAAA;EACA,gBAAA;EACA,iBAAA;EACA,gBAAA;EACA,iBAAA;EACA,uBAAA;EACA,uBAAA;EACA,sCAAA;EACA,mBAAA;EnBsBA,oDAAA;EACQ,4CAAA;EmBrBR,qCAAA;UAAA,6BAAA;CxBq5GD;AwBh5GC;EACE,SAAA;EACA,WAAA;CxBk5GH;AwB36GD;ECzBE,YAAA;EACA,cAAA;EACA,iBAAA;EACA,0BAAA;CzBu8GD;AwBj7GD;EAmCI,eAAA;EACA,kBAAA;EACA,YAAA;EACA,oBAAA;EACA,wBAAA;EACA,eAAA;EACA,oBAAA;CxBi5GH;AwB34GC;;EAEE,sBAAA;EACA,eAAA;EACA,0BAAA;CxB64GH;AwBv4GC;;;EAGE,YAAA;EACA,sBAAA;EACA,WAAA;EACA,0BAAA;CxBy4GH;AwBh4GC;;;EAGE,eAAA;CxBk4GH;AwB93GC;;EAEE,sBAAA;EACA,8BAAA;EACA,uBAAA;EE3GF,oEAAA;EF6GE,oBAAA;CxBg4GH;AwB33GD;EAGI,eAAA;CxB23GH;AwB93GD;EAQI,WAAA;CxBy3GH;AwBj3GD;EACE,WAAA;EACA,SAAA;CxBm3GD;AwB32GD;EACE,QAAA;EACA,YAAA;CxB62GD;AwBz2GD;EACE,eAAA;EACA,kBAAA;EACA,gBAAA;EACA,wBAAA;EACA,eAAA;EACA,oBAAA;CxB22GD;AwBv2GD;EACE,gBAAA;EACA,QAAA;EACA,SAAA;EACA,UAAA;EACA,OAAA;EACA,aAAA;CxBy2GD;AwBr2GD;EACE,SAAA;EACA,WAAA;CxBu2GD;AwB/1GD;;EAII,cAAA;EACA,0BAAA;EACA,4BAAA;EACA,YAAA;CxB+1GH;AwBt2GD;;EAWI,UAAA;EACA,aAAA;EACA,mBAAA;CxB+1GH;AwB10GD;EAXE;IApEA,WAAA;IACA,SAAA;GxB65GC;EwB11GD;IA1DA,QAAA;IACA,YAAA;GxBu5GC;CACF;A2BviHD;;EAEE,mBAAA;EACA,sBAAA;EACA,uBAAA;C3ByiHD;A2B7iHD;;EAMI,mBAAA;EACA,YAAA;C3B2iHH;A2BziHG;;;;;;;;EAIE,WAAA;C3B+iHL;A2BziHD;;;;EAKI,kBAAA;C3B0iHH;A2BriHD;EACE,kBAAA;C3BuiHD;A2BxiHD;;;EAOI,YAAA;C3BsiHH;A2B7iHD;;;EAYI,iBAAA;C3BsiHH;A2BliHD;EACE,iBAAA;C3BoiHD;A2BhiHD;EACE,eAAA;C3BkiHD;A2BjiHC;EClDA,8BAAA;EACG,2BAAA;C5BslHJ;A2BhiHD;;EC/CE,6BAAA;EACG,0BAAA;C5BmlHJ;A2B/hHD;EACE,YAAA;C3BiiHD;A2B/hHD;EACE,iBAAA;C3BiiHD;A2B/hHD;;ECnEE,8BAAA;EACG,2BAAA;C5BsmHJ;A2B9hHD;ECjEE,6BAAA;EACG,0BAAA;C5BkmHJ;A2B7hHD;;EAEE,WAAA;C3B+hHD;A2B9gHD;EACE,kBAAA;EACA,mBAAA;C3BghHD;A2B9gHD;EACE,mBAAA;EACA,oBAAA;C3BghHD;A2B3gHD;EtB/CE,yDAAA;EACQ,iDAAA;CL6jHT;A2B3gHC;EtBnDA,yBAAA;EACQ,iBAAA;CLikHT;A2BxgHD;EACE,eAAA;C3B0gHD;A2BvgHD;EACE,wBAAA;EACA,uBAAA;C3BygHD;A2BtgHD;EACE,wBAAA;C3BwgHD;A2BjgHD;;;EAII,eAAA;EACA,YAAA;EACA,YAAA;EACA,gBAAA;C3BkgHH;A2BzgHD;EAcM,YAAA;C3B8/GL;A2B5gHD;;;;EAsBI,iBAAA;EACA,eAAA;C3B4/GH;A2Bv/GC;EACE,iBAAA;C3By/GH;A2Bv/GC;EC3KA,6BAAA;EACC,4BAAA;EAOD,8BAAA;EACC,6BAAA;C5B+pHF;A2Bz/GC;EC/KA,2BAAA;EACC,0BAAA;EAOD,gCAAA;EACC,+BAAA;C5BqqHF;A2B1/GD;EACE,iBAAA;C3B4/GD;A2B1/GD;;EC/KE,8BAAA;EACC,6BAAA;C5B6qHF;A2Bz/GD;EC7LE,2BAAA;EACC,0BAAA;C5ByrHF;A2Br/GD;EACE,eAAA;EACA,YAAA;EACA,oBAAA;EACA,0BAAA;C3Bu/GD;A2B3/GD;;EAOI,YAAA;EACA,oBAAA;EACA,UAAA;C3Bw/GH;A2BjgHD;EAYI,YAAA;C3Bw/GH;A2BpgHD;EAgBI,WAAA;C3Bu/GH;A2Bt+GD;;;;EAKM,mBAAA;EACA,uBAAA;EACA,qBAAA;C3Bu+GL;A6BjtHD;EACE,mBAAA;EACA,eAAA;EACA,0BAAA;C7BmtHD;A6BhtHC;EACE,YAAA;EACA,gBAAA;EACA,iBAAA;C7BktHH;A6B3tHD;EAeI,mBAAA;EACA,WAAA;EAKA,YAAA;EAEA,YAAA;EACA,iBAAA;C7B0sHH;A6BxsHG;EACE,WAAA;C7B0sHL;A6BhsHD;;;EV0BE,aAAA;EACA,mBAAA;EACA,gBAAA;EACA,uBAAA;EACA,mBAAA;CnB2qHD;AmBzqHC;;;EACE,aAAA;EACA,kBAAA;CnB6qHH;AmB1qHC;;;;;;EAEE,aAAA;CnBgrHH;A6BltHD;;;EVqBE,aAAA;EACA,kBAAA;EACA,gBAAA;EACA,iBAAA;EACA,mBAAA;CnBksHD;AmBhsHC;;;EACE,aAAA;EACA,kBAAA;CnBosHH;AmBjsHC;;;;;;EAEE,aAAA;CnBusHH;A6BhuHD;;;EAGE,oBAAA;C7BkuHD;A6BhuHC;;;EACE,iBAAA;C7BouHH;A6BhuHD;;EAEE,UAAA;EACA,oBAAA;EACA,uBAAA;C7BkuHD;A6B7tHD;EACE,kBAAA;EACA,gBAAA;EACA,oBAAA;EACA,eAAA;EACA,eAAA;EACA,mBAAA;EACA,0BAAA;EACA,uBAAA;EACA,mBAAA;C7B+tHD;A6B5tHC;EACE,kBAAA;EACA,gBAAA;EACA,mBAAA;C7B8tHH;A6B5tHC;EACE,mBAAA;EACA,gBAAA;EACA,mBAAA;C7B8tHH;A6BlvHD;;EA0BI,cAAA;C7B4tHH;A6BvtHD;;;;;;;EDpGE,8BAAA;EACG,2BAAA;C5Bo0HJ;A6BxtHD;EACE,gBAAA;C7B0tHD;A6BxtHD;;;;;;;EDxGE,6BAAA;EACG,0BAAA;C5By0HJ;A6BztHD;EACE,eAAA;C7B2tHD;A6BttHD;EACE,mBAAA;EAGA,aAAA;EACA,oBAAA;C7BstHD;A6B3tHD;EAUI,mBAAA;C7BotHH;A6B9tHD;EAYM,kBAAA;C7BqtHL;A6BltHG;;;EAGE,WAAA;C7BotHL;A6B/sHC;;EAGI,mBAAA;C7BgtHL;A6B7sHC;;EAGI,WAAA;EACA,kBAAA;C7B8sHL;A8B72HD;EACE,iBAAA;EACA,gBAAA;EACA,iBAAA;C9B+2HD;A8Bl3HD;EAOI,mBAAA;EACA,eAAA;C9B82HH;A8Bt3HD;EAWM,mBAAA;EACA,eAAA;EACA,mBAAA;C9B82HL;A8B72HK;;EAEE,sBAAA;EACA,0BAAA;C9B+2HP;A8B12HG;EACE,eAAA;C9B42HL;A8B12HK;;EAEE,eAAA;EACA,sBAAA;EACA,8BAAA;EACA,oBAAA;C9B42HP;A8Br2HG;;;EAGE,0BAAA;EACA,sBAAA;C9Bu2HL;A8Bh5HD;ELHE,YAAA;EACA,cAAA;EACA,iBAAA;EACA,0BAAA;CzBs5HD;A8Bt5HD;EA0DI,gBAAA;C9B+1HH;A8Bt1HD;EACE,8BAAA;C9Bw1HD;A8Bz1HD;EAGI,YAAA;EAEA,oBAAA;C9Bw1HH;A8B71HD;EASM,kBAAA;EACA,wBAAA;EACA,8BAAA;EACA,2BAAA;C9Bu1HL;A8Bt1HK;EACE,mCAAA;C9Bw1HP;A8Bl1HK;;;EAGE,eAAA;EACA,uBAAA;EACA,uBAAA;EACA,iCAAA;EACA,gBAAA;C9Bo1HP;A8B/0HC;EAqDA,YAAA;EA8BA,iBAAA;C9BgwHD;A8Bn1HC;EAwDE,YAAA;C9B8xHH;A8Bt1HC;EA0DI,mBAAA;EACA,mBAAA;C9B+xHL;A8B11HC;EAgEE,UAAA;EACA,WAAA;C9B6xHH;A8BjxHD;EA0DA;IAjEM,oBAAA;IACA,UAAA;G9B4xHH;E8B5tHH;IA9DQ,iBAAA;G9B6xHL;CACF;A8Bv2HC;EAuFE,gBAAA;EACA,mBAAA;C9BmxHH;A8B32HC;;;EA8FE,uBAAA;C9BkxHH;A8BpwHD;EA2BA;IApCM,8BAAA;IACA,2BAAA;G9BixHH;E8B9uHH;;;IA9BM,0BAAA;G9BixHH;CACF;A8Bl3HD;EAEI,YAAA;C9Bm3HH;A8Br3HD;EAMM,mBAAA;C9Bk3HL;A8Bx3HD;EASM,iBAAA;C9Bk3HL;A8B72HK;;;EAGE,YAAA;EACA,0BAAA;C9B+2HP;A8Bv2HD;EAEI,YAAA;C9Bw2HH;A8B12HD;EAIM,gBAAA;EACA,eAAA;C9By2HL;A8B71HD;EACE,YAAA;C9B+1HD;A8Bh2HD;EAII,YAAA;C9B+1HH;A8Bn2HD;EAMM,mBAAA;EACA,mBAAA;C9Bg2HL;A8Bv2HD;EAYI,UAAA;EACA,WAAA;C9B81HH;A8Bl1HD;EA0DA;IAjEM,oBAAA;IACA,UAAA;G9B61HH;E8B7xHH;IA9DQ,iBAAA;G9B81HL;CACF;A8Bt1HD;EACE,iBAAA;C9Bw1HD;A8Bz1HD;EAKI,gBAAA;EACA,mBAAA;C9Bu1HH;A8B71HD;;;EAYI,uBAAA;C9Bs1HH;A8Bx0HD;EA2BA;IApCM,8BAAA;IACA,2BAAA;G9Bq1HH;E8BlzHH;;;IA9BM,0BAAA;G9Bq1HH;CACF;A8B50HD;EAEI,cAAA;C9B60HH;A8B/0HD;EAKI,eAAA;C9B60HH;A8Bp0HD;EAEE,iBAAA;EF3OA,2BAAA;EACC,0BAAA;C5BijIF;A+B3iID;EACE,mBAAA;EACA,iBAAA;EACA,oBAAA;EACA,8BAAA;C/B6iID;A+BriID;EA8nBA;IAhoBI,mBAAA;G/B2iID;CACF;A+B5hID;EAgnBA;IAlnBI,YAAA;G/BkiID;CACF;A+BphID;EACE,oBAAA;EACA,oBAAA;EACA,mBAAA;EACA,kCAAA;EACA,2DAAA;UAAA,mDAAA;EAEA,kCAAA;C/BqhID;A+BnhIC;EACE,iBAAA;C/BqhIH;A+Bz/HD;EA6jBA;IArlBI,YAAA;IACA,cAAA;IACA,yBAAA;YAAA,iBAAA;G/BqhID;E+BnhIC;IACE,0BAAA;IACA,wBAAA;IACA,kBAAA;IACA,6BAAA;G/BqhIH;E+BlhIC;IACE,oBAAA;G/BohIH;E+B/gIC;;;IAGE,gBAAA;IACA,iBAAA;G/BihIH;CACF;A+B7gID;;EAGI,kBAAA;C/B8gIH;A+BzgIC;EAmjBF;;IArjBM,kBAAA;G/BghIH;CACF;A+BvgID;;;;EAII,oBAAA;EACA,mBAAA;C/BygIH;A+BngIC;EAgiBF;;;;IAniBM,gBAAA;IACA,eAAA;G/B6gIH;CACF;A+BjgID;EACE,cAAA;EACA,sBAAA;C/BmgID;A+B9/HD;EA8gBA;IAhhBI,iBAAA;G/BogID;CACF;A+BhgID;;EAEE,gBAAA;EACA,SAAA;EACA,QAAA;EACA,cAAA;C/BkgID;A+B5/HD;EAggBA;;IAlgBI,iBAAA;G/BmgID;CACF;A+BjgID;EACE,OAAA;EACA,sBAAA;C/BmgID;A+BjgID;EACE,UAAA;EACA,iBAAA;EACA,sBAAA;C/BmgID;A+B7/HD;EACE,YAAA;EACA,mBAAA;EACA,gBAAA;EACA,kBAAA;EACA,aAAA;C/B+/HD;A+B7/HC;;EAEE,sBAAA;C/B+/HH;A+BxgID;EAaI,eAAA;C/B8/HH;A+Br/HD;EALI;;IAEE,mBAAA;G/B6/HH;CACF;A+Bn/HD;EACE,mBAAA;EACA,aAAA;EACA,mBAAA;EACA,kBAAA;EC9LA,gBAAA;EACA,mBAAA;ED+LA,8BAAA;EACA,uBAAA;EACA,8BAAA;EACA,mBAAA;C/Bs/HD;A+Bl/HC;EACE,WAAA;C/Bo/HH;A+BlgID;EAmBI,eAAA;EACA,YAAA;EACA,YAAA;EACA,mBAAA;C/Bk/HH;A+BxgID;EAyBI,gBAAA;C/Bk/HH;A+B5+HD;EAqbA;IAvbI,cAAA;G/Bk/HD;CACF;A+Bz+HD;EACE,oBAAA;C/B2+HD;A+B5+HD;EAII,kBAAA;EACA,qBAAA;EACA,kBAAA;C/B2+HH;A+B/8HC;EA2YF;IAjaM,iBAAA;IACA,YAAA;IACA,YAAA;IACA,cAAA;IACA,8BAAA;IACA,UAAA;IACA,yBAAA;YAAA,iBAAA;G/By+HH;E+B9kHH;;IAxZQ,2BAAA;G/B0+HL;E+BllHH;IArZQ,kBAAA;G/B0+HL;E+Bz+HK;;IAEE,uBAAA;G/B2+HP;CACF;A+Bz9HD;EA+XA;IA1YI,YAAA;IACA,UAAA;G/Bw+HD;E+B/lHH;IAtYM,YAAA;G/Bw+HH;E+BlmHH;IApYQ,kBAAA;IACA,qBAAA;G/By+HL;CACF;A+B99HD;EACE,mBAAA;EACA,oBAAA;EACA,mBAAA;EACA,kCAAA;EACA,qCAAA;E1B9NA,6FAAA;EACQ,qFAAA;E2B/DR,gBAAA;EACA,mBAAA;ChC+vID;AkBzuHD;EAwEA;IAtIM,sBAAA;IACA,iBAAA;IACA,uBAAA;GlB2yHH;EkBvqHH;IA/HM,sBAAA;IACA,YAAA;IACA,uBAAA;GlByyHH;EkB5qHH;IAxHM,sBAAA;GlBuyHH;EkB/qHH;IApHM,sBAAA;IACA,uBAAA;GlBsyHH;EkBnrHH;;;IA9GQ,YAAA;GlBsyHL;EkBxrHH;IAxGM,YAAA;GlBmyHH;EkB3rHH;IApGM,iBAAA;IACA,uBAAA;GlBkyHH;EkB/rHH;;IA5FM,sBAAA;IACA,cAAA;IACA,iBAAA;IACA,uBAAA;GlB+xHH;EkBtsHH;;IAtFQ,gBAAA;GlBgyHL;EkB1sHH;;IAjFM,mBAAA;IACA,eAAA;GlB+xHH;EkB/sHH;IA3EM,OAAA;GlB6xHH;CACF;A+BvgIC;EAmWF;IAzWM,mBAAA;G/BihIH;E+B/gIG;IACE,iBAAA;G/BihIL;CACF;A+BhgID;EAoVA;IA5VI,YAAA;IACA,UAAA;IACA,eAAA;IACA,gBAAA;IACA,eAAA;IACA,kBAAA;I1BzPF,yBAAA;IACQ,iBAAA;GLswIP;CACF;A+BtgID;EACE,cAAA;EHpUA,2BAAA;EACC,0BAAA;C5B60IF;A+BtgID;EACE,iBAAA;EHzUA,6BAAA;EACC,4BAAA;EAOD,8BAAA;EACC,6BAAA;C5B40IF;A+BlgID;EChVE,gBAAA;EACA,mBAAA;ChCq1ID;A+BngIC;ECnVA,iBAAA;EACA,oBAAA;ChCy1ID;A+BpgIC;ECtVA,iBAAA;EACA,oBAAA;ChC61ID;A+B9/HD;EChWE,iBAAA;EACA,oBAAA;ChCi2ID;A+B1/HD;EAsSA;IA1SI,YAAA;IACA,kBAAA;IACA,mBAAA;G/BkgID;CACF;A+Br+HD;EAhBE;IExWA,uBAAA;GjCi2IC;E+Bx/HD;IE5WA,wBAAA;IF8WE,oBAAA;G/B0/HD;E+B5/HD;IAKI,gBAAA;G/B0/HH;CACF;A+Bj/HD;EACE,0BAAA;EACA,sBAAA;C/Bm/HD;A+Br/HD;EAKI,YAAA;C/Bm/HH;A+Bl/HG;;EAEE,eAAA;EACA,8BAAA;C/Bo/HL;A+B7/HD;EAcI,YAAA;C/Bk/HH;A+BhgID;EAmBM,YAAA;C/Bg/HL;A+B9+HK;;EAEE,YAAA;EACA,8BAAA;C/Bg/HP;A+B5+HK;;;EAGE,YAAA;EACA,0BAAA;C/B8+HP;A+B1+HK;;;EAGE,YAAA;EACA,8BAAA;C/B4+HP;A+BphID;EA8CI,mBAAA;C/By+HH;A+Bx+HG;;EAEE,uBAAA;C/B0+HL;A+B3hID;EAoDM,uBAAA;C/B0+HL;A+B9hID;;EA0DI,sBAAA;C/Bw+HH;A+Bj+HK;;;EAGE,0BAAA;EACA,YAAA;C/Bm+HP;A+Bl8HC;EAoKF;IA7LU,YAAA;G/B+9HP;E+B99HO;;IAEE,YAAA;IACA,8BAAA;G/Bg+HT;E+B59HO;;;IAGE,YAAA;IACA,0BAAA;G/B89HT;E+B19HO;;;IAGE,YAAA;IACA,8BAAA;G/B49HT;CACF;A+B9jID;EA8GI,YAAA;C/Bm9HH;A+Bl9HG;EACE,YAAA;C/Bo9HL;A+BpkID;EAqHI,YAAA;C/Bk9HH;A+Bj9HG;;EAEE,YAAA;C/Bm9HL;A+B/8HK;;;;EAEE,YAAA;C/Bm9HP;A+B38HD;EACE,uBAAA;EACA,sBAAA;C/B68HD;A+B/8HD;EAKI,eAAA;C/B68HH;A+B58HG;;EAEE,YAAA;EACA,8BAAA;C/B88HL;A+Bv9HD;EAcI,eAAA;C/B48HH;A+B19HD;EAmBM,eAAA;C/B08HL;A+Bx8HK;;EAEE,YAAA;EACA,8BAAA;C/B08HP;A+Bt8HK;;;EAGE,YAAA;EACA,0BAAA;C/Bw8HP;A+Bp8HK;;;EAGE,YAAA;EACA,8BAAA;C/Bs8HP;A+B9+HD;EA+CI,mBAAA;C/Bk8HH;A+Bj8HG;;EAEE,uBAAA;C/Bm8HL;A+Br/HD;EAqDM,uBAAA;C/Bm8HL;A+Bx/HD;;EA2DI,sBAAA;C/Bi8HH;A+B37HK;;;EAGE,0BAAA;EACA,YAAA;C/B67HP;A+Bt5HC;EAwBF;IAvDU,sBAAA;G/By7HP;E+Bl4HH;IApDU,0BAAA;G/By7HP;E+Br4HH;IAjDU,eAAA;G/By7HP;E+Bx7HO;;IAEE,YAAA;IACA,8BAAA;G/B07HT;E+Bt7HO;;;IAGE,YAAA;IACA,0BAAA;G/Bw7HT;E+Bp7HO;;;IAGE,YAAA;IACA,8BAAA;G/Bs7HT;CACF;A+B9hID;EA+GI,eAAA;C/Bk7HH;A+Bj7HG;EACE,YAAA;C/Bm7HL;A+BpiID;EAsHI,eAAA;C/Bi7HH;A+Bh7HG;;EAEE,YAAA;C/Bk7HL;A+B96HK;;;;EAEE,YAAA;C/Bk7HP;AkC5jJD;EACE,kBAAA;EACA,oBAAA;EACA,iBAAA;EACA,0BAAA;EACA,mBAAA;ClC8jJD;AkCnkJD;EAQI,sBAAA;ClC8jJH;AkCtkJD;EAWM,kBAAA;EACA,eAAA;EACA,YAAA;ClC8jJL;AkC3kJD;EAkBI,eAAA;ClC4jJH;AmChlJD;EACE,sBAAA;EACA,gBAAA;EACA,eAAA;EACA,mBAAA;CnCklJD;AmCtlJD;EAOI,gBAAA;CnCklJH;AmCzlJD;;EAUM,mBAAA;EACA,YAAA;EACA,kBAAA;EACA,wBAAA;EACA,sBAAA;EACA,eAAA;EACA,uBAAA;EACA,uBAAA;EACA,kBAAA;CnCmlJL;AmCjlJG;;EAGI,eAAA;EPXN,+BAAA;EACG,4BAAA;C5B8lJJ;AmChlJG;;EPvBF,gCAAA;EACG,6BAAA;C5B2mJJ;AmC3kJG;;;;EAEE,WAAA;EACA,eAAA;EACA,0BAAA;EACA,mBAAA;CnC+kJL;AmCzkJG;;;;;;EAGE,WAAA;EACA,YAAA;EACA,0BAAA;EACA,sBAAA;EACA,gBAAA;CnC8kJL;AmCroJD;;;;;;EAkEM,eAAA;EACA,uBAAA;EACA,mBAAA;EACA,oBAAA;CnC2kJL;AmClkJD;;EC3EM,mBAAA;EACA,gBAAA;EACA,uBAAA;CpCipJL;AoC/oJG;;ERKF,+BAAA;EACG,4BAAA;C5B8oJJ;AoC9oJG;;ERTF,gCAAA;EACG,6BAAA;C5B2pJJ;AmC7kJD;;EChFM,kBAAA;EACA,gBAAA;EACA,iBAAA;CpCiqJL;AoC/pJG;;ERKF,+BAAA;EACG,4BAAA;C5B8pJJ;AoC9pJG;;ERTF,gCAAA;EACG,6BAAA;C5B2qJJ;AqC9qJD;EACE,gBAAA;EACA,eAAA;EACA,iBAAA;EACA,mBAAA;CrCgrJD;AqCprJD;EAOI,gBAAA;CrCgrJH;AqCvrJD;;EAUM,sBAAA;EACA,kBAAA;EACA,uBAAA;EACA,uBAAA;EACA,oBAAA;CrCirJL;AqC/rJD;;EAmBM,sBAAA;EACA,0BAAA;CrCgrJL;AqCpsJD;;EA2BM,aAAA;CrC6qJL;AqCxsJD;;EAkCM,YAAA;CrC0qJL;AqC5sJD;;;;EA2CM,eAAA;EACA,uBAAA;EACA,oBAAA;CrCuqJL;AsCrtJD;EACE,gBAAA;EACA,wBAAA;EACA,eAAA;EACA,kBAAA;EACA,eAAA;EACA,YAAA;EACA,mBAAA;EACA,oBAAA;EACA,yBAAA;EACA,qBAAA;CtCutJD;AsCntJG;;EAEE,YAAA;EACA,sBAAA;EACA,gBAAA;CtCqtJL;AsChtJC;EACE,cAAA;CtCktJH;AsC9sJC;EACE,mBAAA;EACA,UAAA;CtCgtJH;AsCzsJD;ECtCE,0BAAA;CvCkvJD;AuC/uJG;;EAEE,0BAAA;CvCivJL;AsC5sJD;EC1CE,0BAAA;CvCyvJD;AuCtvJG;;EAEE,0BAAA;CvCwvJL;AsC/sJD;EC9CE,0BAAA;CvCgwJD;AuC7vJG;;EAEE,0BAAA;CvC+vJL;AsCltJD;EClDE,0BAAA;CvCuwJD;AuCpwJG;;EAEE,0BAAA;CvCswJL;AsCrtJD;ECtDE,0BAAA;CvC8wJD;AuC3wJG;;EAEE,0BAAA;CvC6wJL;AsCxtJD;EC1DE,0BAAA;CvCqxJD;AuClxJG;;EAEE,0BAAA;CvCoxJL;AwCtxJD;EACE,sBAAA;EACA,gBAAA;EACA,iBAAA;EACA,gBAAA;EACA,kBAAA;EACA,YAAA;EACA,eAAA;EACA,uBAAA;EACA,oBAAA;EACA,mBAAA;EACA,0BAAA;EACA,oBAAA;CxCwxJD;AwCrxJC;EACE,cAAA;CxCuxJH;AwCnxJC;EACE,mBAAA;EACA,UAAA;CxCqxJH;AwClxJC;;EAEE,OAAA;EACA,iBAAA;CxCoxJH;AwC/wJG;;EAEE,YAAA;EACA,sBAAA;EACA,gBAAA;CxCixJL;AwC5wJC;;EAEE,eAAA;EACA,uBAAA;CxC8wJH;AwC3wJC;EACE,aAAA;CxC6wJH;AwC1wJC;EACE,kBAAA;CxC4wJH;AwCzwJC;EACE,iBAAA;CxC2wJH;AyCr0JD;EACE,kBAAA;EACA,qBAAA;EACA,oBAAA;EACA,eAAA;EACA,0BAAA;CzCu0JD;AyC50JD;;EASI,eAAA;CzCu0JH;AyCh1JD;EAaI,oBAAA;EACA,gBAAA;EACA,iBAAA;CzCs0JH;AyCr1JD;EAmBI,0BAAA;CzCq0JH;AyCl0JC;;EAEE,mBAAA;EACA,mBAAA;EACA,oBAAA;CzCo0JH;AyC91JD;EA8BI,gBAAA;CzCm0JH;AyCjzJD;EACA;IAfI,kBAAA;IACA,qBAAA;GzCm0JD;EyCj0JC;;IAEE,mBAAA;IACA,oBAAA;GzCm0JH;EyC1zJH;;IAJM,gBAAA;GzCk0JH;CACF;A0C/2JD;EACE,eAAA;EACA,aAAA;EACA,oBAAA;EACA,wBAAA;EACA,uBAAA;EACA,uBAAA;EACA,mBAAA;ErCiLA,4CAAA;EACK,uCAAA;EACG,oCAAA;CLisJT;A0C33JD;;EAaI,kBAAA;EACA,mBAAA;C1Ck3JH;A0C92JC;;;EAGE,sBAAA;C1Cg3JH;A0Cr4JD;EA0BI,aAAA;EACA,eAAA;C1C82JH;A2Cv4JD;EACE,cAAA;EACA,oBAAA;EACA,8BAAA;EACA,mBAAA;C3Cy4JD;A2C74JD;EAQI,cAAA;EAEA,eAAA;C3Cu4JH;A2Cj5JD;EAeI,kBAAA;C3Cq4JH;A2Cp5JD;;EAqBI,iBAAA;C3Cm4JH;A2Cx5JD;EAyBI,gBAAA;C3Ck4JH;A2C13JD;;EAEE,oBAAA;C3C43JD;A2C93JD;;EAMI,mBAAA;EACA,UAAA;EACA,aAAA;EACA,eAAA;C3C43JH;A2Cp3JD;ECvDE,0BAAA;EACA,sBAAA;EACA,eAAA;C5C86JD;A2Cz3JD;EClDI,0BAAA;C5C86JH;A2C53JD;EC/CI,eAAA;C5C86JH;A2C33JD;EC3DE,0BAAA;EACA,sBAAA;EACA,eAAA;C5Cy7JD;A2Ch4JD;ECtDI,0BAAA;C5Cy7JH;A2Cn4JD;ECnDI,eAAA;C5Cy7JH;A2Cl4JD;EC/DE,0BAAA;EACA,sBAAA;EACA,eAAA;C5Co8JD;A2Cv4JD;EC1DI,0BAAA;C5Co8JH;A2C14JD;ECvDI,eAAA;C5Co8JH;A2Cz4JD;ECnEE,0BAAA;EACA,sBAAA;EACA,eAAA;C5C+8JD;A2C94JD;EC9DI,0BAAA;C5C+8JH;A2Cj5JD;EC3DI,eAAA;C5C+8JH;A6Cj9JD;EACE;IAAQ,4BAAA;G7Co9JP;E6Cn9JD;IAAQ,yBAAA;G7Cs9JP;CACF;A6Cn9JD;EACE;IAAQ,4BAAA;G7Cs9JP;E6Cr9JD;IAAQ,yBAAA;G7Cw9JP;CACF;A6C39JD;EACE;IAAQ,4BAAA;G7Cs9JP;E6Cr9JD;IAAQ,yBAAA;G7Cw9JP;CACF;A6Cj9JD;EACE,iBAAA;EACA,aAAA;EACA,oBAAA;EACA,0BAAA;EACA,mBAAA;ExCsCA,uDAAA;EACQ,+CAAA;CL86JT;A6Ch9JD;EACE,YAAA;EACA,UAAA;EACA,aAAA;EACA,gBAAA;EACA,kBAAA;EACA,YAAA;EACA,mBAAA;EACA,0BAAA;ExCyBA,uDAAA;EACQ,+CAAA;EAyHR,oCAAA;EACK,+BAAA;EACG,4BAAA;CLk0JT;A6C78JD;;ECCI,8MAAA;EACA,yMAAA;EACA,sMAAA;EDAF,mCAAA;UAAA,2BAAA;C7Ci9JD;A6C18JD;;ExC5CE,2DAAA;EACK,sDAAA;EACG,mDAAA;CL0/JT;A6Cv8JD;EErEE,0BAAA;C/C+gKD;A+C5gKC;EDgDE,8MAAA;EACA,yMAAA;EACA,sMAAA;C9C+9JH;A6C38JD;EEzEE,0BAAA;C/CuhKD;A+CphKC;EDgDE,8MAAA;EACA,yMAAA;EACA,sMAAA;C9Cu+JH;A6C/8JD;EE7EE,0BAAA;C/C+hKD;A+C5hKC;EDgDE,8MAAA;EACA,yMAAA;EACA,sMAAA;C9C++JH;A6Cn9JD;EEjFE,0BAAA;C/CuiKD;A+CpiKC;EDgDE,8MAAA;EACA,yMAAA;EACA,sMAAA;C9Cu/JH;AgD/iKD;EAEE,iBAAA;ChDgjKD;AgD9iKC;EACE,cAAA;ChDgjKH;AgD5iKD;;EAEE,QAAA;EACA,iBAAA;ChD8iKD;AgD3iKD;EACE,eAAA;ChD6iKD;AgD1iKD;EACE,eAAA;ChD4iKD;AgDziKC;EACE,gBAAA;ChD2iKH;AgDviKD;;EAEE,mBAAA;ChDyiKD;AgDtiKD;;EAEE,oBAAA;ChDwiKD;AgDriKD;;;EAGE,oBAAA;EACA,oBAAA;ChDuiKD;AgDpiKD;EACE,uBAAA;ChDsiKD;AgDniKD;EACE,uBAAA;ChDqiKD;AgDjiKD;EACE,cAAA;EACA,mBAAA;ChDmiKD;AgD7hKD;EACE,gBAAA;EACA,iBAAA;ChD+hKD;AiDtlKD;EAEE,oBAAA;EACA,gBAAA;CjDulKD;AiD/kKD;EACE,mBAAA;EACA,eAAA;EACA,mBAAA;EAEA,oBAAA;EACA,uBAAA;EACA,uBAAA;CjDglKD;AiD7kKC;ErB3BA,6BAAA;EACC,4BAAA;C5B2mKF;AiD9kKC;EACE,iBAAA;ErBvBF,gCAAA;EACC,+BAAA;C5BwmKF;AiDvkKD;;EAEE,YAAA;CjDykKD;AiD3kKD;;EAKI,YAAA;CjD0kKH;AiDtkKC;;;;EAEE,sBAAA;EACA,YAAA;EACA,0BAAA;CjD0kKH;AiDtkKD;EACE,YAAA;EACA,iBAAA;CjDwkKD;AiDnkKC;;;EAGE,0BAAA;EACA,eAAA;EACA,oBAAA;CjDqkKH;AiD1kKC;;;EASI,eAAA;CjDskKL;AiD/kKC;;;EAYI,eAAA;CjDwkKL;AiDnkKC;;;EAGE,WAAA;EACA,YAAA;EACA,0BAAA;EACA,sBAAA;CjDqkKH;AiD3kKC;;;;;;;;;EAYI,eAAA;CjD0kKL;AiDtlKC;;;EAeI,eAAA;CjD4kKL;AkD9qKC;EACE,eAAA;EACA,0BAAA;ClDgrKH;AkD9qKG;;EAEE,eAAA;ClDgrKL;AkDlrKG;;EAKI,eAAA;ClDirKP;AkD9qKK;;;;EAEE,eAAA;EACA,0BAAA;ClDkrKP;AkDhrKK;;;;;;EAGE,YAAA;EACA,0BAAA;EACA,sBAAA;ClDqrKP;AkD3sKC;EACE,eAAA;EACA,0BAAA;ClD6sKH;AkD3sKG;;EAEE,eAAA;ClD6sKL;AkD/sKG;;EAKI,eAAA;ClD8sKP;AkD3sKK;;;;EAEE,eAAA;EACA,0BAAA;ClD+sKP;AkD7sKK;;;;;;EAGE,YAAA;EACA,0BAAA;EACA,sBAAA;ClDktKP;AkDxuKC;EACE,eAAA;EACA,0BAAA;ClD0uKH;AkDxuKG;;EAEE,eAAA;ClD0uKL;AkD5uKG;;EAKI,eAAA;ClD2uKP;AkDxuKK;;;;EAEE,eAAA;EACA,0BAAA;ClD4uKP;AkD1uKK;;;;;;EAGE,YAAA;EACA,0BAAA;EACA,sBAAA;ClD+uKP;AkDrwKC;EACE,eAAA;EACA,0BAAA;ClDuwKH;AkDrwKG;;EAEE,eAAA;ClDuwKL;AkDzwKG;;EAKI,eAAA;ClDwwKP;AkDrwKK;;;;EAEE,eAAA;EACA,0BAAA;ClDywKP;AkDvwKK;;;;;;EAGE,YAAA;EACA,0BAAA;EACA,sBAAA;ClD4wKP;AiD3qKD;EACE,cAAA;EACA,mBAAA;CjD6qKD;AiD3qKD;EACE,iBAAA;EACA,iBAAA;CjD6qKD;AmDvyKD;EACE,oBAAA;EACA,uBAAA;EACA,8BAAA;EACA,mBAAA;E9C0DA,kDAAA;EACQ,0CAAA;CLgvKT;AmDtyKD;EACE,cAAA;CnDwyKD;AmDnyKD;EACE,mBAAA;EACA,qCAAA;EvBpBA,6BAAA;EACC,4BAAA;C5B0zKF;AmDzyKD;EAMI,eAAA;CnDsyKH;AmDjyKD;EACE,cAAA;EACA,iBAAA;EACA,gBAAA;EACA,eAAA;CnDmyKD;AmDvyKD;;;;;EAWI,eAAA;CnDmyKH;AmD9xKD;EACE,mBAAA;EACA,0BAAA;EACA,2BAAA;EvBxCA,gCAAA;EACC,+BAAA;C5By0KF;AmDxxKD;;EAGI,iBAAA;CnDyxKH;AmD5xKD;;EAMM,oBAAA;EACA,iBAAA;CnD0xKL;AmDtxKG;;EAEI,cAAA;EvBvEN,6BAAA;EACC,4BAAA;C5Bg2KF;AmDpxKG;;EAEI,iBAAA;EvBvEN,gCAAA;EACC,+BAAA;C5B81KF;AmD7yKD;EvB1DE,2BAAA;EACC,0BAAA;C5B02KF;AmDhxKD;EAEI,oBAAA;CnDixKH;AmD9wKD;EACE,oBAAA;CnDgxKD;AmDxwKD;;;EAII,iBAAA;CnDywKH;AmD7wKD;;;EAOM,mBAAA;EACA,oBAAA;CnD2wKL;AmDnxKD;;EvBzGE,6BAAA;EACC,4BAAA;C5Bg4KF;AmDxxKD;;;;EAmBQ,4BAAA;EACA,6BAAA;CnD2wKP;AmD/xKD;;;;;;;;EAwBU,4BAAA;CnDixKT;AmDzyKD;;;;;;;;EA4BU,6BAAA;CnDuxKT;AmDnzKD;;EvBjGE,gCAAA;EACC,+BAAA;C5Bw5KF;AmDxzKD;;;;EAyCQ,+BAAA;EACA,gCAAA;CnDqxKP;AmD/zKD;;;;;;;;EA8CU,+BAAA;CnD2xKT;AmDz0KD;;;;;;;;EAkDU,gCAAA;CnDiyKT;AmDn1KD;;;;EA2DI,2BAAA;CnD8xKH;AmDz1KD;;EA+DI,cAAA;CnD8xKH;AmD71KD;;EAmEI,UAAA;CnD8xKH;AmDj2KD;;;;;;;;;;;;EA0EU,eAAA;CnDqyKT;AmD/2KD;;;;;;;;;;;;EA8EU,gBAAA;CnD+yKT;AmD73KD;;;;;;;;EAuFU,iBAAA;CnDgzKT;AmDv4KD;;;;;;;;EAgGU,iBAAA;CnDizKT;AmDj5KD;EAsGI,UAAA;EACA,iBAAA;CnD8yKH;AmDpyKD;EACE,oBAAA;CnDsyKD;AmDvyKD;EAKI,iBAAA;EACA,mBAAA;CnDqyKH;AmD3yKD;EASM,gBAAA;CnDqyKL;AmD9yKD;EAcI,iBAAA;CnDmyKH;AmDjzKD;;EAkBM,2BAAA;CnDmyKL;AmDrzKD;EAuBI,cAAA;CnDiyKH;AmDxzKD;EAyBM,8BAAA;CnDkyKL;AmD3xKD;EC1PE,mBAAA;CpDwhLD;AoDthLC;EACE,eAAA;EACA,0BAAA;EACA,mBAAA;CpDwhLH;AoD3hLC;EAMI,uBAAA;CpDwhLL;AoD9hLC;EASI,eAAA;EACA,0BAAA;CpDwhLL;AoDrhLC;EAEI,0BAAA;CpDshLL;AmD1yKD;EC7PE,sBAAA;CpD0iLD;AoDxiLC;EACE,YAAA;EACA,0BAAA;EACA,sBAAA;CpD0iLH;AoD7iLC;EAMI,0BAAA;CpD0iLL;AoDhjLC;EASI,eAAA;EACA,uBAAA;CpD0iLL;AoDviLC;EAEI,6BAAA;CpDwiLL;AmDzzKD;EChQE,sBAAA;CpD4jLD;AoD1jLC;EACE,eAAA;EACA,0BAAA;EACA,sBAAA;CpD4jLH;AoD/jLC;EAMI,0BAAA;CpD4jLL;AoDlkLC;EASI,eAAA;EACA,0BAAA;CpD4jLL;AoDzjLC;EAEI,6BAAA;CpD0jLL;AmDx0KD;ECnQE,sBAAA;CpD8kLD;AoD5kLC;EACE,eAAA;EACA,0BAAA;EACA,sBAAA;CpD8kLH;AoDjlLC;EAMI,0BAAA;CpD8kLL;AoDplLC;EASI,eAAA;EACA,0BAAA;CpD8kLL;AoD3kLC;EAEI,6BAAA;CpD4kLL;AmDv1KD;ECtQE,sBAAA;CpDgmLD;AoD9lLC;EACE,eAAA;EACA,0BAAA;EACA,sBAAA;CpDgmLH;AoDnmLC;EAMI,0BAAA;CpDgmLL;AoDtmLC;EASI,eAAA;EACA,0BAAA;CpDgmLL;AoD7lLC;EAEI,6BAAA;CpD8lLL;AmDt2KD;ECzQE,sBAAA;CpDknLD;AoDhnLC;EACE,eAAA;EACA,0BAAA;EACA,sBAAA;CpDknLH;AoDrnLC;EAMI,0BAAA;CpDknLL;AoDxnLC;EASI,eAAA;EACA,0BAAA;CpDknLL;AoD/mLC;EAEI,6BAAA;CpDgnLL;AqDhoLD;EACE,mBAAA;EACA,eAAA;EACA,UAAA;EACA,WAAA;EACA,iBAAA;CrDkoLD;AqDvoLD;;;;;EAYI,mBAAA;EACA,OAAA;EACA,QAAA;EACA,UAAA;EACA,aAAA;EACA,YAAA;EACA,UAAA;CrDkoLH;AqD7nLD;EACE,uBAAA;CrD+nLD;AqD3nLD;EACE,oBAAA;CrD6nLD;AsDxpLD;EACE,iBAAA;EACA,cAAA;EACA,oBAAA;EACA,0BAAA;EACA,0BAAA;EACA,mBAAA;EjDwDA,wDAAA;EACQ,gDAAA;CLmmLT;AsDlqLD;EASI,mBAAA;EACA,kCAAA;CtD4pLH;AsDvpLD;EACE,cAAA;EACA,mBAAA;CtDypLD;AsDvpLD;EACE,aAAA;EACA,mBAAA;CtDypLD;AuD/qLD;EACE,aAAA;EACA,gBAAA;EACA,kBAAA;EACA,eAAA;EACA,YAAA;EACA,0BAAA;EjCRA,aAAA;EAGA,0BAAA;CtBwrLD;AuDhrLC;;EAEE,YAAA;EACA,sBAAA;EACA,gBAAA;EjCfF,aAAA;EAGA,0BAAA;CtBgsLD;AuD5qLC;EACE,WAAA;EACA,gBAAA;EACA,wBAAA;EACA,UAAA;EACA,yBAAA;CvD8qLH;AwDnsLD;EACE,iBAAA;CxDqsLD;AwDjsLD;EACE,cAAA;EACA,iBAAA;EACA,gBAAA;EACA,OAAA;EACA,SAAA;EACA,UAAA;EACA,QAAA;EACA,cAAA;EACA,kCAAA;EAIA,WAAA;CxDgsLD;AwD7rLC;EnD+GA,sCAAA;EACI,kCAAA;EACC,iCAAA;EACG,8BAAA;EAkER,oDAAA;EAEK,0CAAA;EACG,oCAAA;CLghLT;AwDnsLC;EnD2GA,mCAAA;EACI,+BAAA;EACC,8BAAA;EACG,2BAAA;CL2lLT;AwDvsLD;EACE,mBAAA;EACA,iBAAA;CxDysLD;AwDrsLD;EACE,mBAAA;EACA,YAAA;EACA,aAAA;CxDusLD;AwDnsLD;EACE,mBAAA;EACA,uBAAA;EACA,uBAAA;EACA,qCAAA;EACA,mBAAA;EnDaA,iDAAA;EACQ,yCAAA;EmDZR,qCAAA;UAAA,6BAAA;EAEA,WAAA;CxDqsLD;AwDjsLD;EACE,gBAAA;EACA,OAAA;EACA,SAAA;EACA,UAAA;EACA,QAAA;EACA,cAAA;EACA,uBAAA;CxDmsLD;AwDjsLC;ElCrEA,WAAA;EAGA,yBAAA;CtBuwLD;AwDpsLC;ElCtEA,aAAA;EAGA,0BAAA;CtB2wLD;AwDnsLD;EACE,cAAA;EACA,iCAAA;CxDqsLD;AwDjsLD;EACE,iBAAA;CxDmsLD;AwD/rLD;EACE,UAAA;EACA,wBAAA;CxDisLD;AwD5rLD;EACE,mBAAA;EACA,cAAA;CxD8rLD;AwD1rLD;EACE,cAAA;EACA,kBAAA;EACA,8BAAA;CxD4rLD;AwD/rLD;EAQI,iBAAA;EACA,iBAAA;CxD0rLH;AwDnsLD;EAaI,kBAAA;CxDyrLH;AwDtsLD;EAiBI,eAAA;CxDwrLH;AwDnrLD;EACE,mBAAA;EACA,aAAA;EACA,YAAA;EACA,aAAA;EACA,iBAAA;CxDqrLD;AwDnqLD;EAZE;IACE,aAAA;IACA,kBAAA;GxDkrLD;EwDhrLD;InDvEA,kDAAA;IACQ,0CAAA;GL0vLP;EwD/qLD;IAAY,aAAA;GxDkrLX;CACF;AwD7qLD;EAFE;IAAY,aAAA;GxDmrLX;CACF;AyDl0LD;EACE,mBAAA;EACA,cAAA;EACA,eAAA;ECRA,4DAAA;EAEA,mBAAA;EACA,oBAAA;EACA,uBAAA;EACA,iBAAA;EACA,wBAAA;EACA,iBAAA;EACA,kBAAA;EACA,sBAAA;EACA,kBAAA;EACA,qBAAA;EACA,oBAAA;EACA,mBAAA;EACA,qBAAA;EACA,kBAAA;EDHA,gBAAA;EnCVA,WAAA;EAGA,yBAAA;CtBy1LD;AyD90LC;EnCdA,aAAA;EAGA,0BAAA;CtB61LD;AyDj1LC;EAAW,iBAAA;EAAmB,eAAA;CzDq1L/B;AyDp1LC;EAAW,iBAAA;EAAmB,eAAA;CzDw1L/B;AyDv1LC;EAAW,gBAAA;EAAmB,eAAA;CzD21L/B;AyD11LC;EAAW,kBAAA;EAAmB,eAAA;CzD81L/B;AyD11LD;EACE,iBAAA;EACA,iBAAA;EACA,YAAA;EACA,mBAAA;EACA,uBAAA;EACA,mBAAA;CzD41LD;AyDx1LD;EACE,mBAAA;EACA,SAAA;EACA,UAAA;EACA,0BAAA;EACA,oBAAA;CzD01LD;AyDt1LC;EACE,UAAA;EACA,UAAA;EACA,kBAAA;EACA,wBAAA;EACA,uBAAA;CzDw1LH;AyDt1LC;EACE,UAAA;EACA,WAAA;EACA,oBAAA;EACA,wBAAA;EACA,uBAAA;CzDw1LH;AyDt1LC;EACE,UAAA;EACA,UAAA;EACA,oBAAA;EACA,wBAAA;EACA,uBAAA;CzDw1LH;AyDt1LC;EACE,SAAA;EACA,QAAA;EACA,iBAAA;EACA,4BAAA;EACA,yBAAA;CzDw1LH;AyDt1LC;EACE,SAAA;EACA,SAAA;EACA,iBAAA;EACA,4BAAA;EACA,wBAAA;CzDw1LH;AyDt1LC;EACE,OAAA;EACA,UAAA;EACA,kBAAA;EACA,wBAAA;EACA,0BAAA;CzDw1LH;AyDt1LC;EACE,OAAA;EACA,WAAA;EACA,iBAAA;EACA,wBAAA;EACA,0BAAA;CzDw1LH;AyDt1LC;EACE,OAAA;EACA,UAAA;EACA,iBAAA;EACA,wBAAA;EACA,0BAAA;CzDw1LH;A2Dr7LD;EACE,mBAAA;EACA,OAAA;EACA,QAAA;EACA,cAAA;EACA,cAAA;EACA,iBAAA;EACA,aAAA;EDXA,4DAAA;EAEA,mBAAA;EACA,oBAAA;EACA,uBAAA;EACA,iBAAA;EACA,wBAAA;EACA,iBAAA;EACA,kBAAA;EACA,sBAAA;EACA,kBAAA;EACA,qBAAA;EACA,oBAAA;EACA,mBAAA;EACA,qBAAA;EACA,kBAAA;ECAA,gBAAA;EAEA,uBAAA;EACA,qCAAA;UAAA,6BAAA;EACA,uBAAA;EACA,qCAAA;EACA,mBAAA;EtD8CA,kDAAA;EACQ,0CAAA;CLq5LT;A2Dh8LC;EAAY,kBAAA;C3Dm8Lb;A2Dl8LC;EAAY,kBAAA;C3Dq8Lb;A2Dp8LC;EAAY,iBAAA;C3Du8Lb;A2Dt8LC;EAAY,mBAAA;C3Dy8Lb;A2Dt8LD;EACE,UAAA;EACA,kBAAA;EACA,gBAAA;EACA,0BAAA;EACA,iCAAA;EACA,2BAAA;C3Dw8LD;A2Dr8LD;EACE,kBAAA;C3Du8LD;A2D/7LC;;EAEE,mBAAA;EACA,eAAA;EACA,SAAA;EACA,UAAA;EACA,0BAAA;EACA,oBAAA;C3Di8LH;A2D97LD;EACE,mBAAA;C3Dg8LD;A2D97LD;EACE,mBAAA;EACA,YAAA;C3Dg8LD;A2D57LC;EACE,UAAA;EACA,mBAAA;EACA,uBAAA;EACA,0BAAA;EACA,sCAAA;EACA,cAAA;C3D87LH;A2D77LG;EACE,aAAA;EACA,YAAA;EACA,mBAAA;EACA,uBAAA;EACA,uBAAA;C3D+7LL;A2D57LC;EACE,SAAA;EACA,YAAA;EACA,kBAAA;EACA,qBAAA;EACA,4BAAA;EACA,wCAAA;C3D87LH;A2D77LG;EACE,aAAA;EACA,UAAA;EACA,cAAA;EACA,qBAAA;EACA,yBAAA;C3D+7LL;A2D57LC;EACE,UAAA;EACA,mBAAA;EACA,oBAAA;EACA,6BAAA;EACA,yCAAA;EACA,WAAA;C3D87LH;A2D77LG;EACE,aAAA;EACA,SAAA;EACA,mBAAA;EACA,oBAAA;EACA,0BAAA;C3D+7LL;A2D37LC;EACE,SAAA;EACA,aAAA;EACA,kBAAA;EACA,sBAAA;EACA,2BAAA;EACA,uCAAA;C3D67LH;A2D57LG;EACE,aAAA;EACA,WAAA;EACA,sBAAA;EACA,wBAAA;EACA,cAAA;C3D87LL;A4DvjMD;EACE,mBAAA;C5DyjMD;A4DtjMD;EACE,mBAAA;EACA,iBAAA;EACA,YAAA;C5DwjMD;A4D3jMD;EAMI,cAAA;EACA,mBAAA;EvD6KF,0CAAA;EACK,qCAAA;EACG,kCAAA;CL44LT;A4DlkMD;;EAcM,eAAA;C5DwjML;A4D9hMC;EA4NF;IvD3DE,uDAAA;IAEK,6CAAA;IACG,uCAAA;IA7JR,oCAAA;IAEQ,4BAAA;IA+GR,4BAAA;IAEQ,oBAAA;GLi7LP;E4D5jMG;;IvDmHJ,2CAAA;IACQ,mCAAA;IuDjHF,QAAA;G5D+jML;E4D7jMG;;IvD8GJ,4CAAA;IACQ,oCAAA;IuD5GF,QAAA;G5DgkML;E4D9jMG;;;IvDyGJ,wCAAA;IACQ,gCAAA;IuDtGF,QAAA;G5DikML;CACF;A4DvmMD;;;EA6CI,eAAA;C5D+jMH;A4D5mMD;EAiDI,QAAA;C5D8jMH;A4D/mMD;;EAsDI,mBAAA;EACA,OAAA;EACA,YAAA;C5D6jMH;A4DrnMD;EA4DI,WAAA;C5D4jMH;A4DxnMD;EA+DI,YAAA;C5D4jMH;A4D3nMD;;EAmEI,QAAA;C5D4jMH;A4D/nMD;EAuEI,YAAA;C5D2jMH;A4DloMD;EA0EI,WAAA;C5D2jMH;A4DnjMD;EACE,mBAAA;EACA,OAAA;EACA,QAAA;EACA,UAAA;EACA,WAAA;EtC9FA,aAAA;EAGA,0BAAA;EsC6FA,gBAAA;EACA,YAAA;EACA,mBAAA;EACA,0CAAA;EACA,mCAAA;C5DsjMD;A4DjjMC;EdnGE,mGAAA;EACA,8FAAA;EACA,qHAAA;EAAA,+FAAA;EACA,4BAAA;EACA,uHAAA;C9CupMH;A4DrjMC;EACE,WAAA;EACA,SAAA;EdxGA,mGAAA;EACA,8FAAA;EACA,qHAAA;EAAA,+FAAA;EACA,4BAAA;EACA,uHAAA;C9CgqMH;A4DvjMC;;EAEE,WAAA;EACA,YAAA;EACA,sBAAA;EtCvHF,aAAA;EAGA,0BAAA;CtB+qMD;A4DzlMD;;;;EAuCI,mBAAA;EACA,SAAA;EACA,kBAAA;EACA,WAAA;EACA,sBAAA;C5DwjMH;A4DnmMD;;EA+CI,UAAA;EACA,mBAAA;C5DwjMH;A4DxmMD;;EAoDI,WAAA;EACA,oBAAA;C5DwjMH;A4D7mMD;;EAyDI,YAAA;EACA,aAAA;EACA,eAAA;EACA,mBAAA;C5DwjMH;A4DnjMG;EACE,iBAAA;C5DqjML;A4DjjMG;EACE,iBAAA;C5DmjML;A4DziMD;EACE,mBAAA;EACA,aAAA;EACA,UAAA;EACA,YAAA;EACA,WAAA;EACA,kBAAA;EACA,gBAAA;EACA,iBAAA;EACA,mBAAA;C5D2iMD;A4DpjMD;EAYI,sBAAA;EACA,YAAA;EACA,aAAA;EACA,YAAA;EACA,oBAAA;EACA,uBAAA;EACA,oBAAA;EACA,gBAAA;EAWA,0BAAA;EACA,mCAAA;C5DiiMH;A4DhkMD;EAkCI,UAAA;EACA,YAAA;EACA,aAAA;EACA,uBAAA;C5DiiMH;A4D1hMD;EACE,mBAAA;EACA,UAAA;EACA,WAAA;EACA,aAAA;EACA,YAAA;EACA,kBAAA;EACA,qBAAA;EACA,YAAA;EACA,mBAAA;EACA,0CAAA;C5D4hMD;A4D3hMC;EACE,kBAAA;C5D6hMH;A4Dp/LD;EAhCE;;;;IAKI,YAAA;IACA,aAAA;IACA,kBAAA;IACA,gBAAA;G5DshMH;E4D9hMD;;IAYI,mBAAA;G5DshMH;E4DliMD;;IAgBI,oBAAA;G5DshMH;E4DjhMD;IACE,UAAA;IACA,WAAA;IACA,qBAAA;G5DmhMD;E4D/gMD;IACE,aAAA;G5DihMD;CACF;A6DhxMC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAEE,aAAA;EACA,eAAA;C7DgzMH;A6D9yMC;;;;;;;;;;;;;;;;EACE,YAAA;C7D+zMH;AiCv0MD;E6BRE,eAAA;EACA,kBAAA;EACA,mBAAA;C9Dk1MD;AiCz0MD;EACE,wBAAA;CjC20MD;AiCz0MD;EACE,uBAAA;CjC20MD;AiCn0MD;EACE,yBAAA;CjCq0MD;AiCn0MD;EACE,0BAAA;CjCq0MD;AiCn0MD;EACE,mBAAA;CjCq0MD;AiCn0MD;E8BzBE,YAAA;EACA,mBAAA;EACA,kBAAA;EACA,8BAAA;EACA,UAAA;C/D+1MD;AiCj0MD;EACE,yBAAA;CjCm0MD;AiC5zMD;EACE,gBAAA;CjC8zMD;AgE/1MD;EACE,oBAAA;ChEi2MD;AgE31MD;;;;ECdE,yBAAA;CjE+2MD;AgE11MD;;;;;;;;;;;;EAYE,yBAAA;ChE41MD;AgEr1MD;EA6IA;IC7LE,0BAAA;GjEy4MC;EiEx4MD;IAAU,0BAAA;GjE24MT;EiE14MD;IAAU,8BAAA;GjE64MT;EiE54MD;;IACU,+BAAA;GjE+4MT;CACF;AgE/1MD;EAwIA;IA1II,0BAAA;GhEq2MD;CACF;AgE/1MD;EAmIA;IArII,2BAAA;GhEq2MD;CACF;AgE/1MD;EA8HA;IAhII,iCAAA;GhEq2MD;CACF;AgE91MD;EAwHA;IC7LE,0BAAA;GjEu6MC;EiEt6MD;IAAU,0BAAA;GjEy6MT;EiEx6MD;IAAU,8BAAA;GjE26MT;EiE16MD;;IACU,+BAAA;GjE66MT;CACF;AgEx2MD;EAmHA;IArHI,0BAAA;GhE82MD;CACF;AgEx2MD;EA8GA;IAhHI,2BAAA;GhE82MD;CACF;AgEx2MD;EAyGA;IA3GI,iCAAA;GhE82MD;CACF;AgEv2MD;EAmGA;IC7LE,0BAAA;GjEq8MC;EiEp8MD;IAAU,0BAAA;GjEu8MT;EiEt8MD;IAAU,8BAAA;GjEy8MT;EiEx8MD;;IACU,+BAAA;GjE28MT;CACF;AgEj3MD;EA8FA;IAhGI,0BAAA;GhEu3MD;CACF;AgEj3MD;EAyFA;IA3FI,2BAAA;GhEu3MD;CACF;AgEj3MD;EAoFA;IAtFI,iCAAA;GhEu3MD;CACF;AgEh3MD;EA8EA;IC7LE,0BAAA;GjEm+MC;EiEl+MD;IAAU,0BAAA;GjEq+MT;EiEp+MD;IAAU,8BAAA;GjEu+MT;EiEt+MD;;IACU,+BAAA;GjEy+MT;CACF;AgE13MD;EAyEA;IA3EI,0BAAA;GhEg4MD;CACF;AgE13MD;EAoEA;IAtEI,2BAAA;GhEg4MD;CACF;AgE13MD;EA+DA;IAjEI,iCAAA;GhEg4MD;CACF;AgEz3MD;EAyDA;ICrLE,yBAAA;GjEy/MC;CACF;AgEz3MD;EAoDA;ICrLE,yBAAA;GjE8/MC;CACF;AgEz3MD;EA+CA;ICrLE,yBAAA;GjEmgNC;CACF;AgEz3MD;EA0CA;ICrLE,yBAAA;GjEwgNC;CACF;AgEt3MD;ECnJE,yBAAA;CjE4gND;AgEn3MD;EA4BA;IC7LE,0BAAA;GjEwhNC;EiEvhND;IAAU,0BAAA;GjE0hNT;EiEzhND;IAAU,8BAAA;GjE4hNT;EiE3hND;;IACU,+BAAA;GjE8hNT;CACF;AgEj4MD;EACE,yBAAA;ChEm4MD;AgE93MD;EAqBA;IAvBI,0BAAA;GhEo4MD;CACF;AgEl4MD;EACE,yBAAA;ChEo4MD;AgE/3MD;EAcA;IAhBI,2BAAA;GhEq4MD;CACF;AgEn4MD;EACE,yBAAA;ChEq4MD;AgEh4MD;EAOA;IATI,iCAAA;GhEs4MD;CACF;AgE/3MD;EACA;ICrLE,yBAAA;GjEujNC;CACF","file":"bootstrap.css","sourcesContent":["/*!\n * Bootstrap v3.3.6 (http://getbootstrap.com)\n * Copyright 2011-2015 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */\nhtml {\n font-family: sans-serif;\n -ms-text-size-adjust: 100%;\n -webkit-text-size-adjust: 100%;\n}\nbody {\n margin: 0;\n}\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nmenu,\nnav,\nsection,\nsummary {\n display: block;\n}\naudio,\ncanvas,\nprogress,\nvideo {\n display: inline-block;\n vertical-align: baseline;\n}\naudio:not([controls]) {\n display: none;\n height: 0;\n}\n[hidden],\ntemplate {\n display: none;\n}\na {\n background-color: transparent;\n}\na:active,\na:hover {\n outline: 0;\n}\nabbr[title] {\n border-bottom: 1px dotted;\n}\nb,\nstrong {\n font-weight: bold;\n}\ndfn {\n font-style: italic;\n}\nh1 {\n font-size: 2em;\n margin: 0.67em 0;\n}\nmark {\n background: #ff0;\n color: #000;\n}\nsmall {\n font-size: 80%;\n}\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\nsup {\n top: -0.5em;\n}\nsub {\n bottom: -0.25em;\n}\nimg {\n border: 0;\n}\nsvg:not(:root) {\n overflow: hidden;\n}\nfigure {\n margin: 1em 40px;\n}\nhr {\n box-sizing: content-box;\n height: 0;\n}\npre {\n overflow: auto;\n}\ncode,\nkbd,\npre,\nsamp {\n font-family: monospace, monospace;\n font-size: 1em;\n}\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n color: inherit;\n font: inherit;\n margin: 0;\n}\nbutton {\n overflow: visible;\n}\nbutton,\nselect {\n text-transform: none;\n}\nbutton,\nhtml input[type=\"button\"],\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n -webkit-appearance: button;\n cursor: pointer;\n}\nbutton[disabled],\nhtml input[disabled] {\n cursor: default;\n}\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n border: 0;\n padding: 0;\n}\ninput {\n line-height: normal;\n}\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n box-sizing: border-box;\n padding: 0;\n}\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\ninput[type=\"search\"] {\n -webkit-appearance: textfield;\n box-sizing: content-box;\n}\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\nfieldset {\n border: 1px solid #c0c0c0;\n margin: 0 2px;\n padding: 0.35em 0.625em 0.75em;\n}\nlegend {\n border: 0;\n padding: 0;\n}\ntextarea {\n overflow: auto;\n}\noptgroup {\n font-weight: bold;\n}\ntable {\n border-collapse: collapse;\n border-spacing: 0;\n}\ntd,\nth {\n padding: 0;\n}\n/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */\n@media print {\n *,\n *:before,\n *:after {\n background: transparent !important;\n color: #000 !important;\n box-shadow: none !important;\n text-shadow: none !important;\n }\n a,\n a:visited {\n text-decoration: underline;\n }\n a[href]:after {\n content: \" (\" attr(href) \")\";\n }\n abbr[title]:after {\n content: \" (\" attr(title) \")\";\n }\n a[href^=\"#\"]:after,\n a[href^=\"javascript:\"]:after {\n content: \"\";\n }\n pre,\n blockquote {\n border: 1px solid #999;\n page-break-inside: avoid;\n }\n thead {\n display: table-header-group;\n }\n tr,\n img {\n page-break-inside: avoid;\n }\n img {\n max-width: 100% !important;\n }\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n h2,\n h3 {\n page-break-after: avoid;\n }\n .navbar {\n display: none;\n }\n .btn > .caret,\n .dropup > .btn > .caret {\n border-top-color: #000 !important;\n }\n .label {\n border: 1px solid #000;\n }\n .table {\n border-collapse: collapse !important;\n }\n .table td,\n .table th {\n background-color: #fff !important;\n }\n .table-bordered th,\n .table-bordered td {\n border: 1px solid #ddd !important;\n }\n}\n@font-face {\n font-family: 'Glyphicons Halflings';\n src: url('../fonts/glyphicons-halflings-regular.eot');\n src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');\n}\n.glyphicon {\n position: relative;\n top: 1px;\n display: inline-block;\n font-family: 'Glyphicons Halflings';\n font-style: normal;\n font-weight: normal;\n line-height: 1;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n.glyphicon-asterisk:before {\n content: \"\\002a\";\n}\n.glyphicon-plus:before {\n content: \"\\002b\";\n}\n.glyphicon-euro:before,\n.glyphicon-eur:before {\n content: \"\\20ac\";\n}\n.glyphicon-minus:before {\n content: \"\\2212\";\n}\n.glyphicon-cloud:before {\n content: \"\\2601\";\n}\n.glyphicon-envelope:before {\n content: \"\\2709\";\n}\n.glyphicon-pencil:before {\n content: \"\\270f\";\n}\n.glyphicon-glass:before {\n content: \"\\e001\";\n}\n.glyphicon-music:before {\n content: \"\\e002\";\n}\n.glyphicon-search:before {\n content: \"\\e003\";\n}\n.glyphicon-heart:before {\n content: \"\\e005\";\n}\n.glyphicon-star:before {\n content: \"\\e006\";\n}\n.glyphicon-star-empty:before {\n content: \"\\e007\";\n}\n.glyphicon-user:before {\n content: \"\\e008\";\n}\n.glyphicon-film:before {\n content: \"\\e009\";\n}\n.glyphicon-th-large:before {\n content: \"\\e010\";\n}\n.glyphicon-th:before {\n content: \"\\e011\";\n}\n.glyphicon-th-list:before {\n content: \"\\e012\";\n}\n.glyphicon-ok:before {\n content: \"\\e013\";\n}\n.glyphicon-remove:before {\n content: \"\\e014\";\n}\n.glyphicon-zoom-in:before {\n content: \"\\e015\";\n}\n.glyphicon-zoom-out:before {\n content: \"\\e016\";\n}\n.glyphicon-off:before {\n content: \"\\e017\";\n}\n.glyphicon-signal:before {\n content: \"\\e018\";\n}\n.glyphicon-cog:before {\n content: \"\\e019\";\n}\n.glyphicon-trash:before {\n content: \"\\e020\";\n}\n.glyphicon-home:before {\n content: \"\\e021\";\n}\n.glyphicon-file:before {\n content: \"\\e022\";\n}\n.glyphicon-time:before {\n content: \"\\e023\";\n}\n.glyphicon-road:before {\n content: \"\\e024\";\n}\n.glyphicon-download-alt:before {\n content: \"\\e025\";\n}\n.glyphicon-download:before {\n content: \"\\e026\";\n}\n.glyphicon-upload:before {\n content: \"\\e027\";\n}\n.glyphicon-inbox:before {\n content: \"\\e028\";\n}\n.glyphicon-play-circle:before {\n content: \"\\e029\";\n}\n.glyphicon-repeat:before {\n content: \"\\e030\";\n}\n.glyphicon-refresh:before {\n content: \"\\e031\";\n}\n.glyphicon-list-alt:before {\n content: \"\\e032\";\n}\n.glyphicon-lock:before {\n content: \"\\e033\";\n}\n.glyphicon-flag:before {\n content: \"\\e034\";\n}\n.glyphicon-headphones:before {\n content: \"\\e035\";\n}\n.glyphicon-volume-off:before {\n content: \"\\e036\";\n}\n.glyphicon-volume-down:before {\n content: \"\\e037\";\n}\n.glyphicon-volume-up:before {\n content: \"\\e038\";\n}\n.glyphicon-qrcode:before {\n content: \"\\e039\";\n}\n.glyphicon-barcode:before {\n content: \"\\e040\";\n}\n.glyphicon-tag:before {\n content: \"\\e041\";\n}\n.glyphicon-tags:before {\n content: \"\\e042\";\n}\n.glyphicon-book:before {\n content: \"\\e043\";\n}\n.glyphicon-bookmark:before {\n content: \"\\e044\";\n}\n.glyphicon-print:before {\n content: \"\\e045\";\n}\n.glyphicon-camera:before {\n content: \"\\e046\";\n}\n.glyphicon-font:before {\n content: \"\\e047\";\n}\n.glyphicon-bold:before {\n content: \"\\e048\";\n}\n.glyphicon-italic:before {\n content: \"\\e049\";\n}\n.glyphicon-text-height:before {\n content: \"\\e050\";\n}\n.glyphicon-text-width:before {\n content: \"\\e051\";\n}\n.glyphicon-align-left:before {\n content: \"\\e052\";\n}\n.glyphicon-align-center:before {\n content: \"\\e053\";\n}\n.glyphicon-align-right:before {\n content: \"\\e054\";\n}\n.glyphicon-align-justify:before {\n content: \"\\e055\";\n}\n.glyphicon-list:before {\n content: \"\\e056\";\n}\n.glyphicon-indent-left:before {\n content: \"\\e057\";\n}\n.glyphicon-indent-right:before {\n content: \"\\e058\";\n}\n.glyphicon-facetime-video:before {\n content: \"\\e059\";\n}\n.glyphicon-picture:before {\n content: \"\\e060\";\n}\n.glyphicon-map-marker:before {\n content: \"\\e062\";\n}\n.glyphicon-adjust:before {\n content: \"\\e063\";\n}\n.glyphicon-tint:before {\n content: \"\\e064\";\n}\n.glyphicon-edit:before {\n content: \"\\e065\";\n}\n.glyphicon-share:before {\n content: \"\\e066\";\n}\n.glyphicon-check:before {\n content: \"\\e067\";\n}\n.glyphicon-move:before {\n content: \"\\e068\";\n}\n.glyphicon-step-backward:before {\n content: \"\\e069\";\n}\n.glyphicon-fast-backward:before {\n content: \"\\e070\";\n}\n.glyphicon-backward:before {\n content: \"\\e071\";\n}\n.glyphicon-play:before {\n content: \"\\e072\";\n}\n.glyphicon-pause:before {\n content: \"\\e073\";\n}\n.glyphicon-stop:before {\n content: \"\\e074\";\n}\n.glyphicon-forward:before {\n content: \"\\e075\";\n}\n.glyphicon-fast-forward:before {\n content: \"\\e076\";\n}\n.glyphicon-step-forward:before {\n content: \"\\e077\";\n}\n.glyphicon-eject:before {\n content: \"\\e078\";\n}\n.glyphicon-chevron-left:before {\n content: \"\\e079\";\n}\n.glyphicon-chevron-right:before {\n content: \"\\e080\";\n}\n.glyphicon-plus-sign:before {\n content: \"\\e081\";\n}\n.glyphicon-minus-sign:before {\n content: \"\\e082\";\n}\n.glyphicon-remove-sign:before {\n content: \"\\e083\";\n}\n.glyphicon-ok-sign:before {\n content: \"\\e084\";\n}\n.glyphicon-question-sign:before {\n content: \"\\e085\";\n}\n.glyphicon-info-sign:before {\n content: \"\\e086\";\n}\n.glyphicon-screenshot:before {\n content: \"\\e087\";\n}\n.glyphicon-remove-circle:before {\n content: \"\\e088\";\n}\n.glyphicon-ok-circle:before {\n content: \"\\e089\";\n}\n.glyphicon-ban-circle:before {\n content: \"\\e090\";\n}\n.glyphicon-arrow-left:before {\n content: \"\\e091\";\n}\n.glyphicon-arrow-right:before {\n content: \"\\e092\";\n}\n.glyphicon-arrow-up:before {\n content: \"\\e093\";\n}\n.glyphicon-arrow-down:before {\n content: \"\\e094\";\n}\n.glyphicon-share-alt:before {\n content: \"\\e095\";\n}\n.glyphicon-resize-full:before {\n content: \"\\e096\";\n}\n.glyphicon-resize-small:before {\n content: \"\\e097\";\n}\n.glyphicon-exclamation-sign:before {\n content: \"\\e101\";\n}\n.glyphicon-gift:before {\n content: \"\\e102\";\n}\n.glyphicon-leaf:before {\n content: \"\\e103\";\n}\n.glyphicon-fire:before {\n content: \"\\e104\";\n}\n.glyphicon-eye-open:before {\n content: \"\\e105\";\n}\n.glyphicon-eye-close:before {\n content: \"\\e106\";\n}\n.glyphicon-warning-sign:before {\n content: \"\\e107\";\n}\n.glyphicon-plane:before {\n content: \"\\e108\";\n}\n.glyphicon-calendar:before {\n content: \"\\e109\";\n}\n.glyphicon-random:before {\n content: \"\\e110\";\n}\n.glyphicon-comment:before {\n content: \"\\e111\";\n}\n.glyphicon-magnet:before {\n content: \"\\e112\";\n}\n.glyphicon-chevron-up:before {\n content: \"\\e113\";\n}\n.glyphicon-chevron-down:before {\n content: \"\\e114\";\n}\n.glyphicon-retweet:before {\n content: \"\\e115\";\n}\n.glyphicon-shopping-cart:before {\n content: \"\\e116\";\n}\n.glyphicon-folder-close:before {\n content: \"\\e117\";\n}\n.glyphicon-folder-open:before {\n content: \"\\e118\";\n}\n.glyphicon-resize-vertical:before {\n content: \"\\e119\";\n}\n.glyphicon-resize-horizontal:before {\n content: \"\\e120\";\n}\n.glyphicon-hdd:before {\n content: \"\\e121\";\n}\n.glyphicon-bullhorn:before {\n content: \"\\e122\";\n}\n.glyphicon-bell:before {\n content: \"\\e123\";\n}\n.glyphicon-certificate:before {\n content: \"\\e124\";\n}\n.glyphicon-thumbs-up:before {\n content: \"\\e125\";\n}\n.glyphicon-thumbs-down:before {\n content: \"\\e126\";\n}\n.glyphicon-hand-right:before {\n content: \"\\e127\";\n}\n.glyphicon-hand-left:before {\n content: \"\\e128\";\n}\n.glyphicon-hand-up:before {\n content: \"\\e129\";\n}\n.glyphicon-hand-down:before {\n content: \"\\e130\";\n}\n.glyphicon-circle-arrow-right:before {\n content: \"\\e131\";\n}\n.glyphicon-circle-arrow-left:before {\n content: \"\\e132\";\n}\n.glyphicon-circle-arrow-up:before {\n content: \"\\e133\";\n}\n.glyphicon-circle-arrow-down:before {\n content: \"\\e134\";\n}\n.glyphicon-globe:before {\n content: \"\\e135\";\n}\n.glyphicon-wrench:before {\n content: \"\\e136\";\n}\n.glyphicon-tasks:before {\n content: \"\\e137\";\n}\n.glyphicon-filter:before {\n content: \"\\e138\";\n}\n.glyphicon-briefcase:before {\n content: \"\\e139\";\n}\n.glyphicon-fullscreen:before {\n content: \"\\e140\";\n}\n.glyphicon-dashboard:before {\n content: \"\\e141\";\n}\n.glyphicon-paperclip:before {\n content: \"\\e142\";\n}\n.glyphicon-heart-empty:before {\n content: \"\\e143\";\n}\n.glyphicon-link:before {\n content: \"\\e144\";\n}\n.glyphicon-phone:before {\n content: \"\\e145\";\n}\n.glyphicon-pushpin:before {\n content: \"\\e146\";\n}\n.glyphicon-usd:before {\n content: \"\\e148\";\n}\n.glyphicon-gbp:before {\n content: \"\\e149\";\n}\n.glyphicon-sort:before {\n content: \"\\e150\";\n}\n.glyphicon-sort-by-alphabet:before {\n content: \"\\e151\";\n}\n.glyphicon-sort-by-alphabet-alt:before {\n content: \"\\e152\";\n}\n.glyphicon-sort-by-order:before {\n content: \"\\e153\";\n}\n.glyphicon-sort-by-order-alt:before {\n content: \"\\e154\";\n}\n.glyphicon-sort-by-attributes:before {\n content: \"\\e155\";\n}\n.glyphicon-sort-by-attributes-alt:before {\n content: \"\\e156\";\n}\n.glyphicon-unchecked:before {\n content: \"\\e157\";\n}\n.glyphicon-expand:before {\n content: \"\\e158\";\n}\n.glyphicon-collapse-down:before {\n content: \"\\e159\";\n}\n.glyphicon-collapse-up:before {\n content: \"\\e160\";\n}\n.glyphicon-log-in:before {\n content: \"\\e161\";\n}\n.glyphicon-flash:before {\n content: \"\\e162\";\n}\n.glyphicon-log-out:before {\n content: \"\\e163\";\n}\n.glyphicon-new-window:before {\n content: \"\\e164\";\n}\n.glyphicon-record:before {\n content: \"\\e165\";\n}\n.glyphicon-save:before {\n content: \"\\e166\";\n}\n.glyphicon-open:before {\n content: \"\\e167\";\n}\n.glyphicon-saved:before {\n content: \"\\e168\";\n}\n.glyphicon-import:before {\n content: \"\\e169\";\n}\n.glyphicon-export:before {\n content: \"\\e170\";\n}\n.glyphicon-send:before {\n content: \"\\e171\";\n}\n.glyphicon-floppy-disk:before {\n content: \"\\e172\";\n}\n.glyphicon-floppy-saved:before {\n content: \"\\e173\";\n}\n.glyphicon-floppy-remove:before {\n content: \"\\e174\";\n}\n.glyphicon-floppy-save:before {\n content: \"\\e175\";\n}\n.glyphicon-floppy-open:before {\n content: \"\\e176\";\n}\n.glyphicon-credit-card:before {\n content: \"\\e177\";\n}\n.glyphicon-transfer:before {\n content: \"\\e178\";\n}\n.glyphicon-cutlery:before {\n content: \"\\e179\";\n}\n.glyphicon-header:before {\n content: \"\\e180\";\n}\n.glyphicon-compressed:before {\n content: \"\\e181\";\n}\n.glyphicon-earphone:before {\n content: \"\\e182\";\n}\n.glyphicon-phone-alt:before {\n content: \"\\e183\";\n}\n.glyphicon-tower:before {\n content: \"\\e184\";\n}\n.glyphicon-stats:before {\n content: \"\\e185\";\n}\n.glyphicon-sd-video:before {\n content: \"\\e186\";\n}\n.glyphicon-hd-video:before {\n content: \"\\e187\";\n}\n.glyphicon-subtitles:before {\n content: \"\\e188\";\n}\n.glyphicon-sound-stereo:before {\n content: \"\\e189\";\n}\n.glyphicon-sound-dolby:before {\n content: \"\\e190\";\n}\n.glyphicon-sound-5-1:before {\n content: \"\\e191\";\n}\n.glyphicon-sound-6-1:before {\n content: \"\\e192\";\n}\n.glyphicon-sound-7-1:before {\n content: \"\\e193\";\n}\n.glyphicon-copyright-mark:before {\n content: \"\\e194\";\n}\n.glyphicon-registration-mark:before {\n content: \"\\e195\";\n}\n.glyphicon-cloud-download:before {\n content: \"\\e197\";\n}\n.glyphicon-cloud-upload:before {\n content: \"\\e198\";\n}\n.glyphicon-tree-conifer:before {\n content: \"\\e199\";\n}\n.glyphicon-tree-deciduous:before {\n content: \"\\e200\";\n}\n.glyphicon-cd:before {\n content: \"\\e201\";\n}\n.glyphicon-save-file:before {\n content: \"\\e202\";\n}\n.glyphicon-open-file:before {\n content: \"\\e203\";\n}\n.glyphicon-level-up:before {\n content: \"\\e204\";\n}\n.glyphicon-copy:before {\n content: \"\\e205\";\n}\n.glyphicon-paste:before {\n content: \"\\e206\";\n}\n.glyphicon-alert:before {\n content: \"\\e209\";\n}\n.glyphicon-equalizer:before {\n content: \"\\e210\";\n}\n.glyphicon-king:before {\n content: \"\\e211\";\n}\n.glyphicon-queen:before {\n content: \"\\e212\";\n}\n.glyphicon-pawn:before {\n content: \"\\e213\";\n}\n.glyphicon-bishop:before {\n content: \"\\e214\";\n}\n.glyphicon-knight:before {\n content: \"\\e215\";\n}\n.glyphicon-baby-formula:before {\n content: \"\\e216\";\n}\n.glyphicon-tent:before {\n content: \"\\26fa\";\n}\n.glyphicon-blackboard:before {\n content: \"\\e218\";\n}\n.glyphicon-bed:before {\n content: \"\\e219\";\n}\n.glyphicon-apple:before {\n content: \"\\f8ff\";\n}\n.glyphicon-erase:before {\n content: \"\\e221\";\n}\n.glyphicon-hourglass:before {\n content: \"\\231b\";\n}\n.glyphicon-lamp:before {\n content: \"\\e223\";\n}\n.glyphicon-duplicate:before {\n content: \"\\e224\";\n}\n.glyphicon-piggy-bank:before {\n content: \"\\e225\";\n}\n.glyphicon-scissors:before {\n content: \"\\e226\";\n}\n.glyphicon-bitcoin:before {\n content: \"\\e227\";\n}\n.glyphicon-btc:before {\n content: \"\\e227\";\n}\n.glyphicon-xbt:before {\n content: \"\\e227\";\n}\n.glyphicon-yen:before {\n content: \"\\00a5\";\n}\n.glyphicon-jpy:before {\n content: \"\\00a5\";\n}\n.glyphicon-ruble:before {\n content: \"\\20bd\";\n}\n.glyphicon-rub:before {\n content: \"\\20bd\";\n}\n.glyphicon-scale:before {\n content: \"\\e230\";\n}\n.glyphicon-ice-lolly:before {\n content: \"\\e231\";\n}\n.glyphicon-ice-lolly-tasted:before {\n content: \"\\e232\";\n}\n.glyphicon-education:before {\n content: \"\\e233\";\n}\n.glyphicon-option-horizontal:before {\n content: \"\\e234\";\n}\n.glyphicon-option-vertical:before {\n content: \"\\e235\";\n}\n.glyphicon-menu-hamburger:before {\n content: \"\\e236\";\n}\n.glyphicon-modal-window:before {\n content: \"\\e237\";\n}\n.glyphicon-oil:before {\n content: \"\\e238\";\n}\n.glyphicon-grain:before {\n content: \"\\e239\";\n}\n.glyphicon-sunglasses:before {\n content: \"\\e240\";\n}\n.glyphicon-text-size:before {\n content: \"\\e241\";\n}\n.glyphicon-text-color:before {\n content: \"\\e242\";\n}\n.glyphicon-text-background:before {\n content: \"\\e243\";\n}\n.glyphicon-object-align-top:before {\n content: \"\\e244\";\n}\n.glyphicon-object-align-bottom:before {\n content: \"\\e245\";\n}\n.glyphicon-object-align-horizontal:before {\n content: \"\\e246\";\n}\n.glyphicon-object-align-left:before {\n content: \"\\e247\";\n}\n.glyphicon-object-align-vertical:before {\n content: \"\\e248\";\n}\n.glyphicon-object-align-right:before {\n content: \"\\e249\";\n}\n.glyphicon-triangle-right:before {\n content: \"\\e250\";\n}\n.glyphicon-triangle-left:before {\n content: \"\\e251\";\n}\n.glyphicon-triangle-bottom:before {\n content: \"\\e252\";\n}\n.glyphicon-triangle-top:before {\n content: \"\\e253\";\n}\n.glyphicon-console:before {\n content: \"\\e254\";\n}\n.glyphicon-superscript:before {\n content: \"\\e255\";\n}\n.glyphicon-subscript:before {\n content: \"\\e256\";\n}\n.glyphicon-menu-left:before {\n content: \"\\e257\";\n}\n.glyphicon-menu-right:before {\n content: \"\\e258\";\n}\n.glyphicon-menu-down:before {\n content: \"\\e259\";\n}\n.glyphicon-menu-up:before {\n content: \"\\e260\";\n}\n* {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\n*:before,\n*:after {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\nhtml {\n font-size: 10px;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\nbody {\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-size: 14px;\n line-height: 1.42857143;\n color: #333333;\n background-color: #fff;\n}\ninput,\nbutton,\nselect,\ntextarea {\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\na {\n color: #337ab7;\n text-decoration: none;\n}\na:hover,\na:focus {\n color: #23527c;\n text-decoration: underline;\n}\na:focus {\n outline: thin dotted;\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\nfigure {\n margin: 0;\n}\nimg {\n vertical-align: middle;\n}\n.img-responsive,\n.thumbnail > img,\n.thumbnail a > img,\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n display: block;\n max-width: 100%;\n height: auto;\n}\n.img-rounded {\n border-radius: 6px;\n}\n.img-thumbnail {\n padding: 4px;\n line-height: 1.42857143;\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 4px;\n -webkit-transition: all 0.2s ease-in-out;\n -o-transition: all 0.2s ease-in-out;\n transition: all 0.2s ease-in-out;\n display: inline-block;\n max-width: 100%;\n height: auto;\n}\n.img-circle {\n border-radius: 50%;\n}\nhr {\n margin-top: 20px;\n margin-bottom: 20px;\n border: 0;\n border-top: 1px solid #eeeeee;\n}\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n border: 0;\n}\n.sr-only-focusable:active,\n.sr-only-focusable:focus {\n position: static;\n width: auto;\n height: auto;\n margin: 0;\n overflow: visible;\n clip: auto;\n}\n[role=\"button\"] {\n cursor: pointer;\n}\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\n.h1,\n.h2,\n.h3,\n.h4,\n.h5,\n.h6 {\n font-family: inherit;\n font-weight: 500;\n line-height: 1.1;\n color: inherit;\n}\nh1 small,\nh2 small,\nh3 small,\nh4 small,\nh5 small,\nh6 small,\n.h1 small,\n.h2 small,\n.h3 small,\n.h4 small,\n.h5 small,\n.h6 small,\nh1 .small,\nh2 .small,\nh3 .small,\nh4 .small,\nh5 .small,\nh6 .small,\n.h1 .small,\n.h2 .small,\n.h3 .small,\n.h4 .small,\n.h5 .small,\n.h6 .small {\n font-weight: normal;\n line-height: 1;\n color: #777777;\n}\nh1,\n.h1,\nh2,\n.h2,\nh3,\n.h3 {\n margin-top: 20px;\n margin-bottom: 10px;\n}\nh1 small,\n.h1 small,\nh2 small,\n.h2 small,\nh3 small,\n.h3 small,\nh1 .small,\n.h1 .small,\nh2 .small,\n.h2 .small,\nh3 .small,\n.h3 .small {\n font-size: 65%;\n}\nh4,\n.h4,\nh5,\n.h5,\nh6,\n.h6 {\n margin-top: 10px;\n margin-bottom: 10px;\n}\nh4 small,\n.h4 small,\nh5 small,\n.h5 small,\nh6 small,\n.h6 small,\nh4 .small,\n.h4 .small,\nh5 .small,\n.h5 .small,\nh6 .small,\n.h6 .small {\n font-size: 75%;\n}\nh1,\n.h1 {\n font-size: 36px;\n}\nh2,\n.h2 {\n font-size: 30px;\n}\nh3,\n.h3 {\n font-size: 24px;\n}\nh4,\n.h4 {\n font-size: 18px;\n}\nh5,\n.h5 {\n font-size: 14px;\n}\nh6,\n.h6 {\n font-size: 12px;\n}\np {\n margin: 0 0 10px;\n}\n.lead {\n margin-bottom: 20px;\n font-size: 16px;\n font-weight: 300;\n line-height: 1.4;\n}\n@media (min-width: 768px) {\n .lead {\n font-size: 21px;\n }\n}\nsmall,\n.small {\n font-size: 85%;\n}\nmark,\n.mark {\n background-color: #fcf8e3;\n padding: .2em;\n}\n.text-left {\n text-align: left;\n}\n.text-right {\n text-align: right;\n}\n.text-center {\n text-align: center;\n}\n.text-justify {\n text-align: justify;\n}\n.text-nowrap {\n white-space: nowrap;\n}\n.text-lowercase {\n text-transform: lowercase;\n}\n.text-uppercase {\n text-transform: uppercase;\n}\n.text-capitalize {\n text-transform: capitalize;\n}\n.text-muted {\n color: #777777;\n}\n.text-primary {\n color: #337ab7;\n}\na.text-primary:hover,\na.text-primary:focus {\n color: #286090;\n}\n.text-success {\n color: #3c763d;\n}\na.text-success:hover,\na.text-success:focus {\n color: #2b542c;\n}\n.text-info {\n color: #31708f;\n}\na.text-info:hover,\na.text-info:focus {\n color: #245269;\n}\n.text-warning {\n color: #8a6d3b;\n}\na.text-warning:hover,\na.text-warning:focus {\n color: #66512c;\n}\n.text-danger {\n color: #a94442;\n}\na.text-danger:hover,\na.text-danger:focus {\n color: #843534;\n}\n.bg-primary {\n color: #fff;\n background-color: #337ab7;\n}\na.bg-primary:hover,\na.bg-primary:focus {\n background-color: #286090;\n}\n.bg-success {\n background-color: #dff0d8;\n}\na.bg-success:hover,\na.bg-success:focus {\n background-color: #c1e2b3;\n}\n.bg-info {\n background-color: #d9edf7;\n}\na.bg-info:hover,\na.bg-info:focus {\n background-color: #afd9ee;\n}\n.bg-warning {\n background-color: #fcf8e3;\n}\na.bg-warning:hover,\na.bg-warning:focus {\n background-color: #f7ecb5;\n}\n.bg-danger {\n background-color: #f2dede;\n}\na.bg-danger:hover,\na.bg-danger:focus {\n background-color: #e4b9b9;\n}\n.page-header {\n padding-bottom: 9px;\n margin: 40px 0 20px;\n border-bottom: 1px solid #eeeeee;\n}\nul,\nol {\n margin-top: 0;\n margin-bottom: 10px;\n}\nul ul,\nol ul,\nul ol,\nol ol {\n margin-bottom: 0;\n}\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n.list-inline {\n padding-left: 0;\n list-style: none;\n margin-left: -5px;\n}\n.list-inline > li {\n display: inline-block;\n padding-left: 5px;\n padding-right: 5px;\n}\ndl {\n margin-top: 0;\n margin-bottom: 20px;\n}\ndt,\ndd {\n line-height: 1.42857143;\n}\ndt {\n font-weight: bold;\n}\ndd {\n margin-left: 0;\n}\n@media (min-width: 768px) {\n .dl-horizontal dt {\n float: left;\n width: 160px;\n clear: left;\n text-align: right;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n .dl-horizontal dd {\n margin-left: 180px;\n }\n}\nabbr[title],\nabbr[data-original-title] {\n cursor: help;\n border-bottom: 1px dotted #777777;\n}\n.initialism {\n font-size: 90%;\n text-transform: uppercase;\n}\nblockquote {\n padding: 10px 20px;\n margin: 0 0 20px;\n font-size: 17.5px;\n border-left: 5px solid #eeeeee;\n}\nblockquote p:last-child,\nblockquote ul:last-child,\nblockquote ol:last-child {\n margin-bottom: 0;\n}\nblockquote footer,\nblockquote small,\nblockquote .small {\n display: block;\n font-size: 80%;\n line-height: 1.42857143;\n color: #777777;\n}\nblockquote footer:before,\nblockquote small:before,\nblockquote .small:before {\n content: '\\2014 \\00A0';\n}\n.blockquote-reverse,\nblockquote.pull-right {\n padding-right: 15px;\n padding-left: 0;\n border-right: 5px solid #eeeeee;\n border-left: 0;\n text-align: right;\n}\n.blockquote-reverse footer:before,\nblockquote.pull-right footer:before,\n.blockquote-reverse small:before,\nblockquote.pull-right small:before,\n.blockquote-reverse .small:before,\nblockquote.pull-right .small:before {\n content: '';\n}\n.blockquote-reverse footer:after,\nblockquote.pull-right footer:after,\n.blockquote-reverse small:after,\nblockquote.pull-right small:after,\n.blockquote-reverse .small:after,\nblockquote.pull-right .small:after {\n content: '\\00A0 \\2014';\n}\naddress {\n margin-bottom: 20px;\n font-style: normal;\n line-height: 1.42857143;\n}\ncode,\nkbd,\npre,\nsamp {\n font-family: Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\ncode {\n padding: 2px 4px;\n font-size: 90%;\n color: #c7254e;\n background-color: #f9f2f4;\n border-radius: 4px;\n}\nkbd {\n padding: 2px 4px;\n font-size: 90%;\n color: #fff;\n background-color: #333;\n border-radius: 3px;\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25);\n}\nkbd kbd {\n padding: 0;\n font-size: 100%;\n font-weight: bold;\n box-shadow: none;\n}\npre {\n display: block;\n padding: 9.5px;\n margin: 0 0 10px;\n font-size: 13px;\n line-height: 1.42857143;\n word-break: break-all;\n word-wrap: break-word;\n color: #333333;\n background-color: #f5f5f5;\n border: 1px solid #ccc;\n border-radius: 4px;\n}\npre code {\n padding: 0;\n font-size: inherit;\n color: inherit;\n white-space: pre-wrap;\n background-color: transparent;\n border-radius: 0;\n}\n.pre-scrollable {\n max-height: 340px;\n overflow-y: scroll;\n}\n.container {\n margin-right: auto;\n margin-left: auto;\n padding-left: 15px;\n padding-right: 15px;\n}\n@media (min-width: 768px) {\n .container {\n width: 750px;\n }\n}\n@media (min-width: 992px) {\n .container {\n width: 970px;\n }\n}\n@media (min-width: 1200px) {\n .container {\n width: 1170px;\n }\n}\n.container-fluid {\n margin-right: auto;\n margin-left: auto;\n padding-left: 15px;\n padding-right: 15px;\n}\n.row {\n margin-left: -15px;\n margin-right: -15px;\n}\n.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 {\n position: relative;\n min-height: 1px;\n padding-left: 15px;\n padding-right: 15px;\n}\n.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 {\n float: left;\n}\n.col-xs-12 {\n width: 100%;\n}\n.col-xs-11 {\n width: 91.66666667%;\n}\n.col-xs-10 {\n width: 83.33333333%;\n}\n.col-xs-9 {\n width: 75%;\n}\n.col-xs-8 {\n width: 66.66666667%;\n}\n.col-xs-7 {\n width: 58.33333333%;\n}\n.col-xs-6 {\n width: 50%;\n}\n.col-xs-5 {\n width: 41.66666667%;\n}\n.col-xs-4 {\n width: 33.33333333%;\n}\n.col-xs-3 {\n width: 25%;\n}\n.col-xs-2 {\n width: 16.66666667%;\n}\n.col-xs-1 {\n width: 8.33333333%;\n}\n.col-xs-pull-12 {\n right: 100%;\n}\n.col-xs-pull-11 {\n right: 91.66666667%;\n}\n.col-xs-pull-10 {\n right: 83.33333333%;\n}\n.col-xs-pull-9 {\n right: 75%;\n}\n.col-xs-pull-8 {\n right: 66.66666667%;\n}\n.col-xs-pull-7 {\n right: 58.33333333%;\n}\n.col-xs-pull-6 {\n right: 50%;\n}\n.col-xs-pull-5 {\n right: 41.66666667%;\n}\n.col-xs-pull-4 {\n right: 33.33333333%;\n}\n.col-xs-pull-3 {\n right: 25%;\n}\n.col-xs-pull-2 {\n right: 16.66666667%;\n}\n.col-xs-pull-1 {\n right: 8.33333333%;\n}\n.col-xs-pull-0 {\n right: auto;\n}\n.col-xs-push-12 {\n left: 100%;\n}\n.col-xs-push-11 {\n left: 91.66666667%;\n}\n.col-xs-push-10 {\n left: 83.33333333%;\n}\n.col-xs-push-9 {\n left: 75%;\n}\n.col-xs-push-8 {\n left: 66.66666667%;\n}\n.col-xs-push-7 {\n left: 58.33333333%;\n}\n.col-xs-push-6 {\n left: 50%;\n}\n.col-xs-push-5 {\n left: 41.66666667%;\n}\n.col-xs-push-4 {\n left: 33.33333333%;\n}\n.col-xs-push-3 {\n left: 25%;\n}\n.col-xs-push-2 {\n left: 16.66666667%;\n}\n.col-xs-push-1 {\n left: 8.33333333%;\n}\n.col-xs-push-0 {\n left: auto;\n}\n.col-xs-offset-12 {\n margin-left: 100%;\n}\n.col-xs-offset-11 {\n margin-left: 91.66666667%;\n}\n.col-xs-offset-10 {\n margin-left: 83.33333333%;\n}\n.col-xs-offset-9 {\n margin-left: 75%;\n}\n.col-xs-offset-8 {\n margin-left: 66.66666667%;\n}\n.col-xs-offset-7 {\n margin-left: 58.33333333%;\n}\n.col-xs-offset-6 {\n margin-left: 50%;\n}\n.col-xs-offset-5 {\n margin-left: 41.66666667%;\n}\n.col-xs-offset-4 {\n margin-left: 33.33333333%;\n}\n.col-xs-offset-3 {\n margin-left: 25%;\n}\n.col-xs-offset-2 {\n margin-left: 16.66666667%;\n}\n.col-xs-offset-1 {\n margin-left: 8.33333333%;\n}\n.col-xs-offset-0 {\n margin-left: 0%;\n}\n@media (min-width: 768px) {\n .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 {\n float: left;\n }\n .col-sm-12 {\n width: 100%;\n }\n .col-sm-11 {\n width: 91.66666667%;\n }\n .col-sm-10 {\n width: 83.33333333%;\n }\n .col-sm-9 {\n width: 75%;\n }\n .col-sm-8 {\n width: 66.66666667%;\n }\n .col-sm-7 {\n width: 58.33333333%;\n }\n .col-sm-6 {\n width: 50%;\n }\n .col-sm-5 {\n width: 41.66666667%;\n }\n .col-sm-4 {\n width: 33.33333333%;\n }\n .col-sm-3 {\n width: 25%;\n }\n .col-sm-2 {\n width: 16.66666667%;\n }\n .col-sm-1 {\n width: 8.33333333%;\n }\n .col-sm-pull-12 {\n right: 100%;\n }\n .col-sm-pull-11 {\n right: 91.66666667%;\n }\n .col-sm-pull-10 {\n right: 83.33333333%;\n }\n .col-sm-pull-9 {\n right: 75%;\n }\n .col-sm-pull-8 {\n right: 66.66666667%;\n }\n .col-sm-pull-7 {\n right: 58.33333333%;\n }\n .col-sm-pull-6 {\n right: 50%;\n }\n .col-sm-pull-5 {\n right: 41.66666667%;\n }\n .col-sm-pull-4 {\n right: 33.33333333%;\n }\n .col-sm-pull-3 {\n right: 25%;\n }\n .col-sm-pull-2 {\n right: 16.66666667%;\n }\n .col-sm-pull-1 {\n right: 8.33333333%;\n }\n .col-sm-pull-0 {\n right: auto;\n }\n .col-sm-push-12 {\n left: 100%;\n }\n .col-sm-push-11 {\n left: 91.66666667%;\n }\n .col-sm-push-10 {\n left: 83.33333333%;\n }\n .col-sm-push-9 {\n left: 75%;\n }\n .col-sm-push-8 {\n left: 66.66666667%;\n }\n .col-sm-push-7 {\n left: 58.33333333%;\n }\n .col-sm-push-6 {\n left: 50%;\n }\n .col-sm-push-5 {\n left: 41.66666667%;\n }\n .col-sm-push-4 {\n left: 33.33333333%;\n }\n .col-sm-push-3 {\n left: 25%;\n }\n .col-sm-push-2 {\n left: 16.66666667%;\n }\n .col-sm-push-1 {\n left: 8.33333333%;\n }\n .col-sm-push-0 {\n left: auto;\n }\n .col-sm-offset-12 {\n margin-left: 100%;\n }\n .col-sm-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-sm-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-sm-offset-9 {\n margin-left: 75%;\n }\n .col-sm-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-sm-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-sm-offset-6 {\n margin-left: 50%;\n }\n .col-sm-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-sm-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-sm-offset-3 {\n margin-left: 25%;\n }\n .col-sm-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-sm-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-sm-offset-0 {\n margin-left: 0%;\n }\n}\n@media (min-width: 992px) {\n .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 {\n float: left;\n }\n .col-md-12 {\n width: 100%;\n }\n .col-md-11 {\n width: 91.66666667%;\n }\n .col-md-10 {\n width: 83.33333333%;\n }\n .col-md-9 {\n width: 75%;\n }\n .col-md-8 {\n width: 66.66666667%;\n }\n .col-md-7 {\n width: 58.33333333%;\n }\n .col-md-6 {\n width: 50%;\n }\n .col-md-5 {\n width: 41.66666667%;\n }\n .col-md-4 {\n width: 33.33333333%;\n }\n .col-md-3 {\n width: 25%;\n }\n .col-md-2 {\n width: 16.66666667%;\n }\n .col-md-1 {\n width: 8.33333333%;\n }\n .col-md-pull-12 {\n right: 100%;\n }\n .col-md-pull-11 {\n right: 91.66666667%;\n }\n .col-md-pull-10 {\n right: 83.33333333%;\n }\n .col-md-pull-9 {\n right: 75%;\n }\n .col-md-pull-8 {\n right: 66.66666667%;\n }\n .col-md-pull-7 {\n right: 58.33333333%;\n }\n .col-md-pull-6 {\n right: 50%;\n }\n .col-md-pull-5 {\n right: 41.66666667%;\n }\n .col-md-pull-4 {\n right: 33.33333333%;\n }\n .col-md-pull-3 {\n right: 25%;\n }\n .col-md-pull-2 {\n right: 16.66666667%;\n }\n .col-md-pull-1 {\n right: 8.33333333%;\n }\n .col-md-pull-0 {\n right: auto;\n }\n .col-md-push-12 {\n left: 100%;\n }\n .col-md-push-11 {\n left: 91.66666667%;\n }\n .col-md-push-10 {\n left: 83.33333333%;\n }\n .col-md-push-9 {\n left: 75%;\n }\n .col-md-push-8 {\n left: 66.66666667%;\n }\n .col-md-push-7 {\n left: 58.33333333%;\n }\n .col-md-push-6 {\n left: 50%;\n }\n .col-md-push-5 {\n left: 41.66666667%;\n }\n .col-md-push-4 {\n left: 33.33333333%;\n }\n .col-md-push-3 {\n left: 25%;\n }\n .col-md-push-2 {\n left: 16.66666667%;\n }\n .col-md-push-1 {\n left: 8.33333333%;\n }\n .col-md-push-0 {\n left: auto;\n }\n .col-md-offset-12 {\n margin-left: 100%;\n }\n .col-md-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-md-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-md-offset-9 {\n margin-left: 75%;\n }\n .col-md-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-md-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-md-offset-6 {\n margin-left: 50%;\n }\n .col-md-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-md-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-md-offset-3 {\n margin-left: 25%;\n }\n .col-md-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-md-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-md-offset-0 {\n margin-left: 0%;\n }\n}\n@media (min-width: 1200px) {\n .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 {\n float: left;\n }\n .col-lg-12 {\n width: 100%;\n }\n .col-lg-11 {\n width: 91.66666667%;\n }\n .col-lg-10 {\n width: 83.33333333%;\n }\n .col-lg-9 {\n width: 75%;\n }\n .col-lg-8 {\n width: 66.66666667%;\n }\n .col-lg-7 {\n width: 58.33333333%;\n }\n .col-lg-6 {\n width: 50%;\n }\n .col-lg-5 {\n width: 41.66666667%;\n }\n .col-lg-4 {\n width: 33.33333333%;\n }\n .col-lg-3 {\n width: 25%;\n }\n .col-lg-2 {\n width: 16.66666667%;\n }\n .col-lg-1 {\n width: 8.33333333%;\n }\n .col-lg-pull-12 {\n right: 100%;\n }\n .col-lg-pull-11 {\n right: 91.66666667%;\n }\n .col-lg-pull-10 {\n right: 83.33333333%;\n }\n .col-lg-pull-9 {\n right: 75%;\n }\n .col-lg-pull-8 {\n right: 66.66666667%;\n }\n .col-lg-pull-7 {\n right: 58.33333333%;\n }\n .col-lg-pull-6 {\n right: 50%;\n }\n .col-lg-pull-5 {\n right: 41.66666667%;\n }\n .col-lg-pull-4 {\n right: 33.33333333%;\n }\n .col-lg-pull-3 {\n right: 25%;\n }\n .col-lg-pull-2 {\n right: 16.66666667%;\n }\n .col-lg-pull-1 {\n right: 8.33333333%;\n }\n .col-lg-pull-0 {\n right: auto;\n }\n .col-lg-push-12 {\n left: 100%;\n }\n .col-lg-push-11 {\n left: 91.66666667%;\n }\n .col-lg-push-10 {\n left: 83.33333333%;\n }\n .col-lg-push-9 {\n left: 75%;\n }\n .col-lg-push-8 {\n left: 66.66666667%;\n }\n .col-lg-push-7 {\n left: 58.33333333%;\n }\n .col-lg-push-6 {\n left: 50%;\n }\n .col-lg-push-5 {\n left: 41.66666667%;\n }\n .col-lg-push-4 {\n left: 33.33333333%;\n }\n .col-lg-push-3 {\n left: 25%;\n }\n .col-lg-push-2 {\n left: 16.66666667%;\n }\n .col-lg-push-1 {\n left: 8.33333333%;\n }\n .col-lg-push-0 {\n left: auto;\n }\n .col-lg-offset-12 {\n margin-left: 100%;\n }\n .col-lg-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-lg-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-lg-offset-9 {\n margin-left: 75%;\n }\n .col-lg-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-lg-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-lg-offset-6 {\n margin-left: 50%;\n }\n .col-lg-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-lg-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-lg-offset-3 {\n margin-left: 25%;\n }\n .col-lg-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-lg-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-lg-offset-0 {\n margin-left: 0%;\n }\n}\ntable {\n background-color: transparent;\n}\ncaption {\n padding-top: 8px;\n padding-bottom: 8px;\n color: #777777;\n text-align: left;\n}\nth {\n text-align: left;\n}\n.table {\n width: 100%;\n max-width: 100%;\n margin-bottom: 20px;\n}\n.table > thead > tr > th,\n.table > tbody > tr > th,\n.table > tfoot > tr > th,\n.table > thead > tr > td,\n.table > tbody > tr > td,\n.table > tfoot > tr > td {\n padding: 8px;\n line-height: 1.42857143;\n vertical-align: top;\n border-top: 1px solid #ddd;\n}\n.table > thead > tr > th {\n vertical-align: bottom;\n border-bottom: 2px solid #ddd;\n}\n.table > caption + thead > tr:first-child > th,\n.table > colgroup + thead > tr:first-child > th,\n.table > thead:first-child > tr:first-child > th,\n.table > caption + thead > tr:first-child > td,\n.table > colgroup + thead > tr:first-child > td,\n.table > thead:first-child > tr:first-child > td {\n border-top: 0;\n}\n.table > tbody + tbody {\n border-top: 2px solid #ddd;\n}\n.table .table {\n background-color: #fff;\n}\n.table-condensed > thead > tr > th,\n.table-condensed > tbody > tr > th,\n.table-condensed > tfoot > tr > th,\n.table-condensed > thead > tr > td,\n.table-condensed > tbody > tr > td,\n.table-condensed > tfoot > tr > td {\n padding: 5px;\n}\n.table-bordered {\n border: 1px solid #ddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > tbody > tr > th,\n.table-bordered > tfoot > tr > th,\n.table-bordered > thead > tr > td,\n.table-bordered > tbody > tr > td,\n.table-bordered > tfoot > tr > td {\n border: 1px solid #ddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > thead > tr > td {\n border-bottom-width: 2px;\n}\n.table-striped > tbody > tr:nth-of-type(odd) {\n background-color: #f9f9f9;\n}\n.table-hover > tbody > tr:hover {\n background-color: #f5f5f5;\n}\ntable col[class*=\"col-\"] {\n position: static;\n float: none;\n display: table-column;\n}\ntable td[class*=\"col-\"],\ntable th[class*=\"col-\"] {\n position: static;\n float: none;\n display: table-cell;\n}\n.table > thead > tr > td.active,\n.table > tbody > tr > td.active,\n.table > tfoot > tr > td.active,\n.table > thead > tr > th.active,\n.table > tbody > tr > th.active,\n.table > tfoot > tr > th.active,\n.table > thead > tr.active > td,\n.table > tbody > tr.active > td,\n.table > tfoot > tr.active > td,\n.table > thead > tr.active > th,\n.table > tbody > tr.active > th,\n.table > tfoot > tr.active > th {\n background-color: #f5f5f5;\n}\n.table-hover > tbody > tr > td.active:hover,\n.table-hover > tbody > tr > th.active:hover,\n.table-hover > tbody > tr.active:hover > td,\n.table-hover > tbody > tr:hover > .active,\n.table-hover > tbody > tr.active:hover > th {\n background-color: #e8e8e8;\n}\n.table > thead > tr > td.success,\n.table > tbody > tr > td.success,\n.table > tfoot > tr > td.success,\n.table > thead > tr > th.success,\n.table > tbody > tr > th.success,\n.table > tfoot > tr > th.success,\n.table > thead > tr.success > td,\n.table > tbody > tr.success > td,\n.table > tfoot > tr.success > td,\n.table > thead > tr.success > th,\n.table > tbody > tr.success > th,\n.table > tfoot > tr.success > th {\n background-color: #dff0d8;\n}\n.table-hover > tbody > tr > td.success:hover,\n.table-hover > tbody > tr > th.success:hover,\n.table-hover > tbody > tr.success:hover > td,\n.table-hover > tbody > tr:hover > .success,\n.table-hover > tbody > tr.success:hover > th {\n background-color: #d0e9c6;\n}\n.table > thead > tr > td.info,\n.table > tbody > tr > td.info,\n.table > tfoot > tr > td.info,\n.table > thead > tr > th.info,\n.table > tbody > tr > th.info,\n.table > tfoot > tr > th.info,\n.table > thead > tr.info > td,\n.table > tbody > tr.info > td,\n.table > tfoot > tr.info > td,\n.table > thead > tr.info > th,\n.table > tbody > tr.info > th,\n.table > tfoot > tr.info > th {\n background-color: #d9edf7;\n}\n.table-hover > tbody > tr > td.info:hover,\n.table-hover > tbody > tr > th.info:hover,\n.table-hover > tbody > tr.info:hover > td,\n.table-hover > tbody > tr:hover > .info,\n.table-hover > tbody > tr.info:hover > th {\n background-color: #c4e3f3;\n}\n.table > thead > tr > td.warning,\n.table > tbody > tr > td.warning,\n.table > tfoot > tr > td.warning,\n.table > thead > tr > th.warning,\n.table > tbody > tr > th.warning,\n.table > tfoot > tr > th.warning,\n.table > thead > tr.warning > td,\n.table > tbody > tr.warning > td,\n.table > tfoot > tr.warning > td,\n.table > thead > tr.warning > th,\n.table > tbody > tr.warning > th,\n.table > tfoot > tr.warning > th {\n background-color: #fcf8e3;\n}\n.table-hover > tbody > tr > td.warning:hover,\n.table-hover > tbody > tr > th.warning:hover,\n.table-hover > tbody > tr.warning:hover > td,\n.table-hover > tbody > tr:hover > .warning,\n.table-hover > tbody > tr.warning:hover > th {\n background-color: #faf2cc;\n}\n.table > thead > tr > td.danger,\n.table > tbody > tr > td.danger,\n.table > tfoot > tr > td.danger,\n.table > thead > tr > th.danger,\n.table > tbody > tr > th.danger,\n.table > tfoot > tr > th.danger,\n.table > thead > tr.danger > td,\n.table > tbody > tr.danger > td,\n.table > tfoot > tr.danger > td,\n.table > thead > tr.danger > th,\n.table > tbody > tr.danger > th,\n.table > tfoot > tr.danger > th {\n background-color: #f2dede;\n}\n.table-hover > tbody > tr > td.danger:hover,\n.table-hover > tbody > tr > th.danger:hover,\n.table-hover > tbody > tr.danger:hover > td,\n.table-hover > tbody > tr:hover > .danger,\n.table-hover > tbody > tr.danger:hover > th {\n background-color: #ebcccc;\n}\n.table-responsive {\n overflow-x: auto;\n min-height: 0.01%;\n}\n@media screen and (max-width: 767px) {\n .table-responsive {\n width: 100%;\n margin-bottom: 15px;\n overflow-y: hidden;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n border: 1px solid #ddd;\n }\n .table-responsive > .table {\n margin-bottom: 0;\n }\n .table-responsive > .table > thead > tr > th,\n .table-responsive > .table > tbody > tr > th,\n .table-responsive > .table > tfoot > tr > th,\n .table-responsive > .table > thead > tr > td,\n .table-responsive > .table > tbody > tr > td,\n .table-responsive > .table > tfoot > tr > td {\n white-space: nowrap;\n }\n .table-responsive > .table-bordered {\n border: 0;\n }\n .table-responsive > .table-bordered > thead > tr > th:first-child,\n .table-responsive > .table-bordered > tbody > tr > th:first-child,\n .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n .table-responsive > .table-bordered > thead > tr > td:first-child,\n .table-responsive > .table-bordered > tbody > tr > td:first-child,\n .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n border-left: 0;\n }\n .table-responsive > .table-bordered > thead > tr > th:last-child,\n .table-responsive > .table-bordered > tbody > tr > th:last-child,\n .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n .table-responsive > .table-bordered > thead > tr > td:last-child,\n .table-responsive > .table-bordered > tbody > tr > td:last-child,\n .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n border-right: 0;\n }\n .table-responsive > .table-bordered > tbody > tr:last-child > th,\n .table-responsive > .table-bordered > tfoot > tr:last-child > th,\n .table-responsive > .table-bordered > tbody > tr:last-child > td,\n .table-responsive > .table-bordered > tfoot > tr:last-child > td {\n border-bottom: 0;\n }\n}\nfieldset {\n padding: 0;\n margin: 0;\n border: 0;\n min-width: 0;\n}\nlegend {\n display: block;\n width: 100%;\n padding: 0;\n margin-bottom: 20px;\n font-size: 21px;\n line-height: inherit;\n color: #333333;\n border: 0;\n border-bottom: 1px solid #e5e5e5;\n}\nlabel {\n display: inline-block;\n max-width: 100%;\n margin-bottom: 5px;\n font-weight: bold;\n}\ninput[type=\"search\"] {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n margin: 4px 0 0;\n margin-top: 1px \\9;\n line-height: normal;\n}\ninput[type=\"file\"] {\n display: block;\n}\ninput[type=\"range\"] {\n display: block;\n width: 100%;\n}\nselect[multiple],\nselect[size] {\n height: auto;\n}\ninput[type=\"file\"]:focus,\ninput[type=\"radio\"]:focus,\ninput[type=\"checkbox\"]:focus {\n outline: thin dotted;\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\noutput {\n display: block;\n padding-top: 7px;\n font-size: 14px;\n line-height: 1.42857143;\n color: #555555;\n}\n.form-control {\n display: block;\n width: 100%;\n height: 34px;\n padding: 6px 12px;\n font-size: 14px;\n line-height: 1.42857143;\n color: #555555;\n background-color: #fff;\n background-image: none;\n border: 1px solid #ccc;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n}\n.form-control:focus {\n border-color: #66afe9;\n outline: 0;\n -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);\n box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);\n}\n.form-control::-moz-placeholder {\n color: #999;\n opacity: 1;\n}\n.form-control:-ms-input-placeholder {\n color: #999;\n}\n.form-control::-webkit-input-placeholder {\n color: #999;\n}\n.form-control::-ms-expand {\n border: 0;\n background-color: transparent;\n}\n.form-control[disabled],\n.form-control[readonly],\nfieldset[disabled] .form-control {\n background-color: #eeeeee;\n opacity: 1;\n}\n.form-control[disabled],\nfieldset[disabled] .form-control {\n cursor: not-allowed;\n}\ntextarea.form-control {\n height: auto;\n}\ninput[type=\"search\"] {\n -webkit-appearance: none;\n}\n@media screen and (-webkit-min-device-pixel-ratio: 0) {\n input[type=\"date\"].form-control,\n input[type=\"time\"].form-control,\n input[type=\"datetime-local\"].form-control,\n input[type=\"month\"].form-control {\n line-height: 34px;\n }\n input[type=\"date\"].input-sm,\n input[type=\"time\"].input-sm,\n input[type=\"datetime-local\"].input-sm,\n input[type=\"month\"].input-sm,\n .input-group-sm input[type=\"date\"],\n .input-group-sm input[type=\"time\"],\n .input-group-sm input[type=\"datetime-local\"],\n .input-group-sm input[type=\"month\"] {\n line-height: 30px;\n }\n input[type=\"date\"].input-lg,\n input[type=\"time\"].input-lg,\n input[type=\"datetime-local\"].input-lg,\n input[type=\"month\"].input-lg,\n .input-group-lg input[type=\"date\"],\n .input-group-lg input[type=\"time\"],\n .input-group-lg input[type=\"datetime-local\"],\n .input-group-lg input[type=\"month\"] {\n line-height: 46px;\n }\n}\n.form-group {\n margin-bottom: 15px;\n}\n.radio,\n.checkbox {\n position: relative;\n display: block;\n margin-top: 10px;\n margin-bottom: 10px;\n}\n.radio label,\n.checkbox label {\n min-height: 20px;\n padding-left: 20px;\n margin-bottom: 0;\n font-weight: normal;\n cursor: pointer;\n}\n.radio input[type=\"radio\"],\n.radio-inline input[type=\"radio\"],\n.checkbox input[type=\"checkbox\"],\n.checkbox-inline input[type=\"checkbox\"] {\n position: absolute;\n margin-left: -20px;\n margin-top: 4px \\9;\n}\n.radio + .radio,\n.checkbox + .checkbox {\n margin-top: -5px;\n}\n.radio-inline,\n.checkbox-inline {\n position: relative;\n display: inline-block;\n padding-left: 20px;\n margin-bottom: 0;\n vertical-align: middle;\n font-weight: normal;\n cursor: pointer;\n}\n.radio-inline + .radio-inline,\n.checkbox-inline + .checkbox-inline {\n margin-top: 0;\n margin-left: 10px;\n}\ninput[type=\"radio\"][disabled],\ninput[type=\"checkbox\"][disabled],\ninput[type=\"radio\"].disabled,\ninput[type=\"checkbox\"].disabled,\nfieldset[disabled] input[type=\"radio\"],\nfieldset[disabled] input[type=\"checkbox\"] {\n cursor: not-allowed;\n}\n.radio-inline.disabled,\n.checkbox-inline.disabled,\nfieldset[disabled] .radio-inline,\nfieldset[disabled] .checkbox-inline {\n cursor: not-allowed;\n}\n.radio.disabled label,\n.checkbox.disabled label,\nfieldset[disabled] .radio label,\nfieldset[disabled] .checkbox label {\n cursor: not-allowed;\n}\n.form-control-static {\n padding-top: 7px;\n padding-bottom: 7px;\n margin-bottom: 0;\n min-height: 34px;\n}\n.form-control-static.input-lg,\n.form-control-static.input-sm {\n padding-left: 0;\n padding-right: 0;\n}\n.input-sm {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\nselect.input-sm {\n height: 30px;\n line-height: 30px;\n}\ntextarea.input-sm,\nselect[multiple].input-sm {\n height: auto;\n}\n.form-group-sm .form-control {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.form-group-sm select.form-control {\n height: 30px;\n line-height: 30px;\n}\n.form-group-sm textarea.form-control,\n.form-group-sm select[multiple].form-control {\n height: auto;\n}\n.form-group-sm .form-control-static {\n height: 30px;\n min-height: 32px;\n padding: 6px 10px;\n font-size: 12px;\n line-height: 1.5;\n}\n.input-lg {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\nselect.input-lg {\n height: 46px;\n line-height: 46px;\n}\ntextarea.input-lg,\nselect[multiple].input-lg {\n height: auto;\n}\n.form-group-lg .form-control {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\n.form-group-lg select.form-control {\n height: 46px;\n line-height: 46px;\n}\n.form-group-lg textarea.form-control,\n.form-group-lg select[multiple].form-control {\n height: auto;\n}\n.form-group-lg .form-control-static {\n height: 46px;\n min-height: 38px;\n padding: 11px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n}\n.has-feedback {\n position: relative;\n}\n.has-feedback .form-control {\n padding-right: 42.5px;\n}\n.form-control-feedback {\n position: absolute;\n top: 0;\n right: 0;\n z-index: 2;\n display: block;\n width: 34px;\n height: 34px;\n line-height: 34px;\n text-align: center;\n pointer-events: none;\n}\n.input-lg + .form-control-feedback,\n.input-group-lg + .form-control-feedback,\n.form-group-lg .form-control + .form-control-feedback {\n width: 46px;\n height: 46px;\n line-height: 46px;\n}\n.input-sm + .form-control-feedback,\n.input-group-sm + .form-control-feedback,\n.form-group-sm .form-control + .form-control-feedback {\n width: 30px;\n height: 30px;\n line-height: 30px;\n}\n.has-success .help-block,\n.has-success .control-label,\n.has-success .radio,\n.has-success .checkbox,\n.has-success .radio-inline,\n.has-success .checkbox-inline,\n.has-success.radio label,\n.has-success.checkbox label,\n.has-success.radio-inline label,\n.has-success.checkbox-inline label {\n color: #3c763d;\n}\n.has-success .form-control {\n border-color: #3c763d;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-success .form-control:focus {\n border-color: #2b542c;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;\n}\n.has-success .input-group-addon {\n color: #3c763d;\n border-color: #3c763d;\n background-color: #dff0d8;\n}\n.has-success .form-control-feedback {\n color: #3c763d;\n}\n.has-warning .help-block,\n.has-warning .control-label,\n.has-warning .radio,\n.has-warning .checkbox,\n.has-warning .radio-inline,\n.has-warning .checkbox-inline,\n.has-warning.radio label,\n.has-warning.checkbox label,\n.has-warning.radio-inline label,\n.has-warning.checkbox-inline label {\n color: #8a6d3b;\n}\n.has-warning .form-control {\n border-color: #8a6d3b;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-warning .form-control:focus {\n border-color: #66512c;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;\n}\n.has-warning .input-group-addon {\n color: #8a6d3b;\n border-color: #8a6d3b;\n background-color: #fcf8e3;\n}\n.has-warning .form-control-feedback {\n color: #8a6d3b;\n}\n.has-error .help-block,\n.has-error .control-label,\n.has-error .radio,\n.has-error .checkbox,\n.has-error .radio-inline,\n.has-error .checkbox-inline,\n.has-error.radio label,\n.has-error.checkbox label,\n.has-error.radio-inline label,\n.has-error.checkbox-inline label {\n color: #a94442;\n}\n.has-error .form-control {\n border-color: #a94442;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-error .form-control:focus {\n border-color: #843534;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;\n}\n.has-error .input-group-addon {\n color: #a94442;\n border-color: #a94442;\n background-color: #f2dede;\n}\n.has-error .form-control-feedback {\n color: #a94442;\n}\n.has-feedback label ~ .form-control-feedback {\n top: 25px;\n}\n.has-feedback label.sr-only ~ .form-control-feedback {\n top: 0;\n}\n.help-block {\n display: block;\n margin-top: 5px;\n margin-bottom: 10px;\n color: #737373;\n}\n@media (min-width: 768px) {\n .form-inline .form-group {\n display: inline-block;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .form-inline .form-control-static {\n display: inline-block;\n }\n .form-inline .input-group {\n display: inline-table;\n vertical-align: middle;\n }\n .form-inline .input-group .input-group-addon,\n .form-inline .input-group .input-group-btn,\n .form-inline .input-group .form-control {\n width: auto;\n }\n .form-inline .input-group > .form-control {\n width: 100%;\n }\n .form-inline .control-label {\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .radio,\n .form-inline .checkbox {\n display: inline-block;\n margin-top: 0;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .radio label,\n .form-inline .checkbox label {\n padding-left: 0;\n }\n .form-inline .radio input[type=\"radio\"],\n .form-inline .checkbox input[type=\"checkbox\"] {\n position: relative;\n margin-left: 0;\n }\n .form-inline .has-feedback .form-control-feedback {\n top: 0;\n }\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox,\n.form-horizontal .radio-inline,\n.form-horizontal .checkbox-inline {\n margin-top: 0;\n margin-bottom: 0;\n padding-top: 7px;\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox {\n min-height: 27px;\n}\n.form-horizontal .form-group {\n margin-left: -15px;\n margin-right: -15px;\n}\n@media (min-width: 768px) {\n .form-horizontal .control-label {\n text-align: right;\n margin-bottom: 0;\n padding-top: 7px;\n }\n}\n.form-horizontal .has-feedback .form-control-feedback {\n right: 15px;\n}\n@media (min-width: 768px) {\n .form-horizontal .form-group-lg .control-label {\n padding-top: 11px;\n font-size: 18px;\n }\n}\n@media (min-width: 768px) {\n .form-horizontal .form-group-sm .control-label {\n padding-top: 6px;\n font-size: 12px;\n }\n}\n.btn {\n display: inline-block;\n margin-bottom: 0;\n font-weight: normal;\n text-align: center;\n vertical-align: middle;\n touch-action: manipulation;\n cursor: pointer;\n background-image: none;\n border: 1px solid transparent;\n white-space: nowrap;\n padding: 6px 12px;\n font-size: 14px;\n line-height: 1.42857143;\n border-radius: 4px;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n.btn:focus,\n.btn:active:focus,\n.btn.active:focus,\n.btn.focus,\n.btn:active.focus,\n.btn.active.focus {\n outline: thin dotted;\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\n.btn:hover,\n.btn:focus,\n.btn.focus {\n color: #333;\n text-decoration: none;\n}\n.btn:active,\n.btn.active {\n outline: 0;\n background-image: none;\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn.disabled,\n.btn[disabled],\nfieldset[disabled] .btn {\n cursor: not-allowed;\n opacity: 0.65;\n filter: alpha(opacity=65);\n -webkit-box-shadow: none;\n box-shadow: none;\n}\na.btn.disabled,\nfieldset[disabled] a.btn {\n pointer-events: none;\n}\n.btn-default {\n color: #333;\n background-color: #fff;\n border-color: #ccc;\n}\n.btn-default:focus,\n.btn-default.focus {\n color: #333;\n background-color: #e6e6e6;\n border-color: #8c8c8c;\n}\n.btn-default:hover {\n color: #333;\n background-color: #e6e6e6;\n border-color: #adadad;\n}\n.btn-default:active,\n.btn-default.active,\n.open > .dropdown-toggle.btn-default {\n color: #333;\n background-color: #e6e6e6;\n border-color: #adadad;\n}\n.btn-default:active:hover,\n.btn-default.active:hover,\n.open > .dropdown-toggle.btn-default:hover,\n.btn-default:active:focus,\n.btn-default.active:focus,\n.open > .dropdown-toggle.btn-default:focus,\n.btn-default:active.focus,\n.btn-default.active.focus,\n.open > .dropdown-toggle.btn-default.focus {\n color: #333;\n background-color: #d4d4d4;\n border-color: #8c8c8c;\n}\n.btn-default:active,\n.btn-default.active,\n.open > .dropdown-toggle.btn-default {\n background-image: none;\n}\n.btn-default.disabled:hover,\n.btn-default[disabled]:hover,\nfieldset[disabled] .btn-default:hover,\n.btn-default.disabled:focus,\n.btn-default[disabled]:focus,\nfieldset[disabled] .btn-default:focus,\n.btn-default.disabled.focus,\n.btn-default[disabled].focus,\nfieldset[disabled] .btn-default.focus {\n background-color: #fff;\n border-color: #ccc;\n}\n.btn-default .badge {\n color: #fff;\n background-color: #333;\n}\n.btn-primary {\n color: #fff;\n background-color: #337ab7;\n border-color: #2e6da4;\n}\n.btn-primary:focus,\n.btn-primary.focus {\n color: #fff;\n background-color: #286090;\n border-color: #122b40;\n}\n.btn-primary:hover {\n color: #fff;\n background-color: #286090;\n border-color: #204d74;\n}\n.btn-primary:active,\n.btn-primary.active,\n.open > .dropdown-toggle.btn-primary {\n color: #fff;\n background-color: #286090;\n border-color: #204d74;\n}\n.btn-primary:active:hover,\n.btn-primary.active:hover,\n.open > .dropdown-toggle.btn-primary:hover,\n.btn-primary:active:focus,\n.btn-primary.active:focus,\n.open > .dropdown-toggle.btn-primary:focus,\n.btn-primary:active.focus,\n.btn-primary.active.focus,\n.open > .dropdown-toggle.btn-primary.focus {\n color: #fff;\n background-color: #204d74;\n border-color: #122b40;\n}\n.btn-primary:active,\n.btn-primary.active,\n.open > .dropdown-toggle.btn-primary {\n background-image: none;\n}\n.btn-primary.disabled:hover,\n.btn-primary[disabled]:hover,\nfieldset[disabled] .btn-primary:hover,\n.btn-primary.disabled:focus,\n.btn-primary[disabled]:focus,\nfieldset[disabled] .btn-primary:focus,\n.btn-primary.disabled.focus,\n.btn-primary[disabled].focus,\nfieldset[disabled] .btn-primary.focus {\n background-color: #337ab7;\n border-color: #2e6da4;\n}\n.btn-primary .badge {\n color: #337ab7;\n background-color: #fff;\n}\n.btn-success {\n color: #fff;\n background-color: #5cb85c;\n border-color: #4cae4c;\n}\n.btn-success:focus,\n.btn-success.focus {\n color: #fff;\n background-color: #449d44;\n border-color: #255625;\n}\n.btn-success:hover {\n color: #fff;\n background-color: #449d44;\n border-color: #398439;\n}\n.btn-success:active,\n.btn-success.active,\n.open > .dropdown-toggle.btn-success {\n color: #fff;\n background-color: #449d44;\n border-color: #398439;\n}\n.btn-success:active:hover,\n.btn-success.active:hover,\n.open > .dropdown-toggle.btn-success:hover,\n.btn-success:active:focus,\n.btn-success.active:focus,\n.open > .dropdown-toggle.btn-success:focus,\n.btn-success:active.focus,\n.btn-success.active.focus,\n.open > .dropdown-toggle.btn-success.focus {\n color: #fff;\n background-color: #398439;\n border-color: #255625;\n}\n.btn-success:active,\n.btn-success.active,\n.open > .dropdown-toggle.btn-success {\n background-image: none;\n}\n.btn-success.disabled:hover,\n.btn-success[disabled]:hover,\nfieldset[disabled] .btn-success:hover,\n.btn-success.disabled:focus,\n.btn-success[disabled]:focus,\nfieldset[disabled] .btn-success:focus,\n.btn-success.disabled.focus,\n.btn-success[disabled].focus,\nfieldset[disabled] .btn-success.focus {\n background-color: #5cb85c;\n border-color: #4cae4c;\n}\n.btn-success .badge {\n color: #5cb85c;\n background-color: #fff;\n}\n.btn-info {\n color: #fff;\n background-color: #5bc0de;\n border-color: #46b8da;\n}\n.btn-info:focus,\n.btn-info.focus {\n color: #fff;\n background-color: #31b0d5;\n border-color: #1b6d85;\n}\n.btn-info:hover {\n color: #fff;\n background-color: #31b0d5;\n border-color: #269abc;\n}\n.btn-info:active,\n.btn-info.active,\n.open > .dropdown-toggle.btn-info {\n color: #fff;\n background-color: #31b0d5;\n border-color: #269abc;\n}\n.btn-info:active:hover,\n.btn-info.active:hover,\n.open > .dropdown-toggle.btn-info:hover,\n.btn-info:active:focus,\n.btn-info.active:focus,\n.open > .dropdown-toggle.btn-info:focus,\n.btn-info:active.focus,\n.btn-info.active.focus,\n.open > .dropdown-toggle.btn-info.focus {\n color: #fff;\n background-color: #269abc;\n border-color: #1b6d85;\n}\n.btn-info:active,\n.btn-info.active,\n.open > .dropdown-toggle.btn-info {\n background-image: none;\n}\n.btn-info.disabled:hover,\n.btn-info[disabled]:hover,\nfieldset[disabled] .btn-info:hover,\n.btn-info.disabled:focus,\n.btn-info[disabled]:focus,\nfieldset[disabled] .btn-info:focus,\n.btn-info.disabled.focus,\n.btn-info[disabled].focus,\nfieldset[disabled] .btn-info.focus {\n background-color: #5bc0de;\n border-color: #46b8da;\n}\n.btn-info .badge {\n color: #5bc0de;\n background-color: #fff;\n}\n.btn-warning {\n color: #fff;\n background-color: #f0ad4e;\n border-color: #eea236;\n}\n.btn-warning:focus,\n.btn-warning.focus {\n color: #fff;\n background-color: #ec971f;\n border-color: #985f0d;\n}\n.btn-warning:hover {\n color: #fff;\n background-color: #ec971f;\n border-color: #d58512;\n}\n.btn-warning:active,\n.btn-warning.active,\n.open > .dropdown-toggle.btn-warning {\n color: #fff;\n background-color: #ec971f;\n border-color: #d58512;\n}\n.btn-warning:active:hover,\n.btn-warning.active:hover,\n.open > .dropdown-toggle.btn-warning:hover,\n.btn-warning:active:focus,\n.btn-warning.active:focus,\n.open > .dropdown-toggle.btn-warning:focus,\n.btn-warning:active.focus,\n.btn-warning.active.focus,\n.open > .dropdown-toggle.btn-warning.focus {\n color: #fff;\n background-color: #d58512;\n border-color: #985f0d;\n}\n.btn-warning:active,\n.btn-warning.active,\n.open > .dropdown-toggle.btn-warning {\n background-image: none;\n}\n.btn-warning.disabled:hover,\n.btn-warning[disabled]:hover,\nfieldset[disabled] .btn-warning:hover,\n.btn-warning.disabled:focus,\n.btn-warning[disabled]:focus,\nfieldset[disabled] .btn-warning:focus,\n.btn-warning.disabled.focus,\n.btn-warning[disabled].focus,\nfieldset[disabled] .btn-warning.focus {\n background-color: #f0ad4e;\n border-color: #eea236;\n}\n.btn-warning .badge {\n color: #f0ad4e;\n background-color: #fff;\n}\n.btn-danger {\n color: #fff;\n background-color: #d9534f;\n border-color: #d43f3a;\n}\n.btn-danger:focus,\n.btn-danger.focus {\n color: #fff;\n background-color: #c9302c;\n border-color: #761c19;\n}\n.btn-danger:hover {\n color: #fff;\n background-color: #c9302c;\n border-color: #ac2925;\n}\n.btn-danger:active,\n.btn-danger.active,\n.open > .dropdown-toggle.btn-danger {\n color: #fff;\n background-color: #c9302c;\n border-color: #ac2925;\n}\n.btn-danger:active:hover,\n.btn-danger.active:hover,\n.open > .dropdown-toggle.btn-danger:hover,\n.btn-danger:active:focus,\n.btn-danger.active:focus,\n.open > .dropdown-toggle.btn-danger:focus,\n.btn-danger:active.focus,\n.btn-danger.active.focus,\n.open > .dropdown-toggle.btn-danger.focus {\n color: #fff;\n background-color: #ac2925;\n border-color: #761c19;\n}\n.btn-danger:active,\n.btn-danger.active,\n.open > .dropdown-toggle.btn-danger {\n background-image: none;\n}\n.btn-danger.disabled:hover,\n.btn-danger[disabled]:hover,\nfieldset[disabled] .btn-danger:hover,\n.btn-danger.disabled:focus,\n.btn-danger[disabled]:focus,\nfieldset[disabled] .btn-danger:focus,\n.btn-danger.disabled.focus,\n.btn-danger[disabled].focus,\nfieldset[disabled] .btn-danger.focus {\n background-color: #d9534f;\n border-color: #d43f3a;\n}\n.btn-danger .badge {\n color: #d9534f;\n background-color: #fff;\n}\n.btn-link {\n color: #337ab7;\n font-weight: normal;\n border-radius: 0;\n}\n.btn-link,\n.btn-link:active,\n.btn-link.active,\n.btn-link[disabled],\nfieldset[disabled] .btn-link {\n background-color: transparent;\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn-link,\n.btn-link:hover,\n.btn-link:focus,\n.btn-link:active {\n border-color: transparent;\n}\n.btn-link:hover,\n.btn-link:focus {\n color: #23527c;\n text-decoration: underline;\n background-color: transparent;\n}\n.btn-link[disabled]:hover,\nfieldset[disabled] .btn-link:hover,\n.btn-link[disabled]:focus,\nfieldset[disabled] .btn-link:focus {\n color: #777777;\n text-decoration: none;\n}\n.btn-lg,\n.btn-group-lg > .btn {\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\n.btn-sm,\n.btn-group-sm > .btn {\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.btn-xs,\n.btn-group-xs > .btn {\n padding: 1px 5px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.btn-block {\n display: block;\n width: 100%;\n}\n.btn-block + .btn-block {\n margin-top: 5px;\n}\ninput[type=\"submit\"].btn-block,\ninput[type=\"reset\"].btn-block,\ninput[type=\"button\"].btn-block {\n width: 100%;\n}\n.fade {\n opacity: 0;\n -webkit-transition: opacity 0.15s linear;\n -o-transition: opacity 0.15s linear;\n transition: opacity 0.15s linear;\n}\n.fade.in {\n opacity: 1;\n}\n.collapse {\n display: none;\n}\n.collapse.in {\n display: block;\n}\ntr.collapse.in {\n display: table-row;\n}\ntbody.collapse.in {\n display: table-row-group;\n}\n.collapsing {\n position: relative;\n height: 0;\n overflow: hidden;\n -webkit-transition-property: height, visibility;\n transition-property: height, visibility;\n -webkit-transition-duration: 0.35s;\n transition-duration: 0.35s;\n -webkit-transition-timing-function: ease;\n transition-timing-function: ease;\n}\n.caret {\n display: inline-block;\n width: 0;\n height: 0;\n margin-left: 2px;\n vertical-align: middle;\n border-top: 4px dashed;\n border-top: 4px solid \\9;\n border-right: 4px solid transparent;\n border-left: 4px solid transparent;\n}\n.dropup,\n.dropdown {\n position: relative;\n}\n.dropdown-toggle:focus {\n outline: 0;\n}\n.dropdown-menu {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: 1000;\n display: none;\n float: left;\n min-width: 160px;\n padding: 5px 0;\n margin: 2px 0 0;\n list-style: none;\n font-size: 14px;\n text-align: left;\n background-color: #fff;\n border: 1px solid #ccc;\n border: 1px solid rgba(0, 0, 0, 0.15);\n border-radius: 4px;\n -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n background-clip: padding-box;\n}\n.dropdown-menu.pull-right {\n right: 0;\n left: auto;\n}\n.dropdown-menu .divider {\n height: 1px;\n margin: 9px 0;\n overflow: hidden;\n background-color: #e5e5e5;\n}\n.dropdown-menu > li > a {\n display: block;\n padding: 3px 20px;\n clear: both;\n font-weight: normal;\n line-height: 1.42857143;\n color: #333333;\n white-space: nowrap;\n}\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n text-decoration: none;\n color: #262626;\n background-color: #f5f5f5;\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n color: #fff;\n text-decoration: none;\n outline: 0;\n background-color: #337ab7;\n}\n.dropdown-menu > .disabled > a,\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n color: #777777;\n}\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n text-decoration: none;\n background-color: transparent;\n background-image: none;\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n cursor: not-allowed;\n}\n.open > .dropdown-menu {\n display: block;\n}\n.open > a {\n outline: 0;\n}\n.dropdown-menu-right {\n left: auto;\n right: 0;\n}\n.dropdown-menu-left {\n left: 0;\n right: auto;\n}\n.dropdown-header {\n display: block;\n padding: 3px 20px;\n font-size: 12px;\n line-height: 1.42857143;\n color: #777777;\n white-space: nowrap;\n}\n.dropdown-backdrop {\n position: fixed;\n left: 0;\n right: 0;\n bottom: 0;\n top: 0;\n z-index: 990;\n}\n.pull-right > .dropdown-menu {\n right: 0;\n left: auto;\n}\n.dropup .caret,\n.navbar-fixed-bottom .dropdown .caret {\n border-top: 0;\n border-bottom: 4px dashed;\n border-bottom: 4px solid \\9;\n content: \"\";\n}\n.dropup .dropdown-menu,\n.navbar-fixed-bottom .dropdown .dropdown-menu {\n top: auto;\n bottom: 100%;\n margin-bottom: 2px;\n}\n@media (min-width: 768px) {\n .navbar-right .dropdown-menu {\n left: auto;\n right: 0;\n }\n .navbar-right .dropdown-menu-left {\n left: 0;\n right: auto;\n }\n}\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: inline-block;\n vertical-align: middle;\n}\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n position: relative;\n float: left;\n}\n.btn-group > .btn:hover,\n.btn-group-vertical > .btn:hover,\n.btn-group > .btn:focus,\n.btn-group-vertical > .btn:focus,\n.btn-group > .btn:active,\n.btn-group-vertical > .btn:active,\n.btn-group > .btn.active,\n.btn-group-vertical > .btn.active {\n z-index: 2;\n}\n.btn-group .btn + .btn,\n.btn-group .btn + .btn-group,\n.btn-group .btn-group + .btn,\n.btn-group .btn-group + .btn-group {\n margin-left: -1px;\n}\n.btn-toolbar {\n margin-left: -5px;\n}\n.btn-toolbar .btn,\n.btn-toolbar .btn-group,\n.btn-toolbar .input-group {\n float: left;\n}\n.btn-toolbar > .btn,\n.btn-toolbar > .btn-group,\n.btn-toolbar > .input-group {\n margin-left: 5px;\n}\n.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {\n border-radius: 0;\n}\n.btn-group > .btn:first-child {\n margin-left: 0;\n}\n.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) {\n border-bottom-right-radius: 0;\n border-top-right-radius: 0;\n}\n.btn-group > .btn:last-child:not(:first-child),\n.btn-group > .dropdown-toggle:not(:first-child) {\n border-bottom-left-radius: 0;\n border-top-left-radius: 0;\n}\n.btn-group > .btn-group {\n float: left;\n}\n.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n border-bottom-right-radius: 0;\n border-top-right-radius: 0;\n}\n.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child {\n border-bottom-left-radius: 0;\n border-top-left-radius: 0;\n}\n.btn-group .dropdown-toggle:active,\n.btn-group.open .dropdown-toggle {\n outline: 0;\n}\n.btn-group > .btn + .dropdown-toggle {\n padding-left: 8px;\n padding-right: 8px;\n}\n.btn-group > .btn-lg + .dropdown-toggle {\n padding-left: 12px;\n padding-right: 12px;\n}\n.btn-group.open .dropdown-toggle {\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn-group.open .dropdown-toggle.btn-link {\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn .caret {\n margin-left: 0;\n}\n.btn-lg .caret {\n border-width: 5px 5px 0;\n border-bottom-width: 0;\n}\n.dropup .btn-lg .caret {\n border-width: 0 5px 5px;\n}\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group,\n.btn-group-vertical > .btn-group > .btn {\n display: block;\n float: none;\n width: 100%;\n max-width: 100%;\n}\n.btn-group-vertical > .btn-group > .btn {\n float: none;\n}\n.btn-group-vertical > .btn + .btn,\n.btn-group-vertical > .btn + .btn-group,\n.btn-group-vertical > .btn-group + .btn,\n.btn-group-vertical > .btn-group + .btn-group {\n margin-top: -1px;\n margin-left: 0;\n}\n.btn-group-vertical > .btn:not(:first-child):not(:last-child) {\n border-radius: 0;\n}\n.btn-group-vertical > .btn:first-child:not(:last-child) {\n border-top-right-radius: 4px;\n border-top-left-radius: 4px;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn:last-child:not(:first-child) {\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n border-bottom-right-radius: 4px;\n border-bottom-left-radius: 4px;\n}\n.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n}\n.btn-group-justified {\n display: table;\n width: 100%;\n table-layout: fixed;\n border-collapse: separate;\n}\n.btn-group-justified > .btn,\n.btn-group-justified > .btn-group {\n float: none;\n display: table-cell;\n width: 1%;\n}\n.btn-group-justified > .btn-group .btn {\n width: 100%;\n}\n.btn-group-justified > .btn-group .dropdown-menu {\n left: auto;\n}\n[data-toggle=\"buttons\"] > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn input[type=\"checkbox\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"checkbox\"] {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n}\n.input-group {\n position: relative;\n display: table;\n border-collapse: separate;\n}\n.input-group[class*=\"col-\"] {\n float: none;\n padding-left: 0;\n padding-right: 0;\n}\n.input-group .form-control {\n position: relative;\n z-index: 2;\n float: left;\n width: 100%;\n margin-bottom: 0;\n}\n.input-group .form-control:focus {\n z-index: 3;\n}\n.input-group-lg > .form-control,\n.input-group-lg > .input-group-addon,\n.input-group-lg > .input-group-btn > .btn {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\nselect.input-group-lg > .form-control,\nselect.input-group-lg > .input-group-addon,\nselect.input-group-lg > .input-group-btn > .btn {\n height: 46px;\n line-height: 46px;\n}\ntextarea.input-group-lg > .form-control,\ntextarea.input-group-lg > .input-group-addon,\ntextarea.input-group-lg > .input-group-btn > .btn,\nselect[multiple].input-group-lg > .form-control,\nselect[multiple].input-group-lg > .input-group-addon,\nselect[multiple].input-group-lg > .input-group-btn > .btn {\n height: auto;\n}\n.input-group-sm > .form-control,\n.input-group-sm > .input-group-addon,\n.input-group-sm > .input-group-btn > .btn {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\nselect.input-group-sm > .form-control,\nselect.input-group-sm > .input-group-addon,\nselect.input-group-sm > .input-group-btn > .btn {\n height: 30px;\n line-height: 30px;\n}\ntextarea.input-group-sm > .form-control,\ntextarea.input-group-sm > .input-group-addon,\ntextarea.input-group-sm > .input-group-btn > .btn,\nselect[multiple].input-group-sm > .form-control,\nselect[multiple].input-group-sm > .input-group-addon,\nselect[multiple].input-group-sm > .input-group-btn > .btn {\n height: auto;\n}\n.input-group-addon,\n.input-group-btn,\n.input-group .form-control {\n display: table-cell;\n}\n.input-group-addon:not(:first-child):not(:last-child),\n.input-group-btn:not(:first-child):not(:last-child),\n.input-group .form-control:not(:first-child):not(:last-child) {\n border-radius: 0;\n}\n.input-group-addon,\n.input-group-btn {\n width: 1%;\n white-space: nowrap;\n vertical-align: middle;\n}\n.input-group-addon {\n padding: 6px 12px;\n font-size: 14px;\n font-weight: normal;\n line-height: 1;\n color: #555555;\n text-align: center;\n background-color: #eeeeee;\n border: 1px solid #ccc;\n border-radius: 4px;\n}\n.input-group-addon.input-sm {\n padding: 5px 10px;\n font-size: 12px;\n border-radius: 3px;\n}\n.input-group-addon.input-lg {\n padding: 10px 16px;\n font-size: 18px;\n border-radius: 6px;\n}\n.input-group-addon input[type=\"radio\"],\n.input-group-addon input[type=\"checkbox\"] {\n margin-top: 0;\n}\n.input-group .form-control:first-child,\n.input-group-addon:first-child,\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group > .btn,\n.input-group-btn:first-child > .dropdown-toggle,\n.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group-btn:last-child > .btn-group:not(:last-child) > .btn {\n border-bottom-right-radius: 0;\n border-top-right-radius: 0;\n}\n.input-group-addon:first-child {\n border-right: 0;\n}\n.input-group .form-control:last-child,\n.input-group-addon:last-child,\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group > .btn,\n.input-group-btn:last-child > .dropdown-toggle,\n.input-group-btn:first-child > .btn:not(:first-child),\n.input-group-btn:first-child > .btn-group:not(:first-child) > .btn {\n border-bottom-left-radius: 0;\n border-top-left-radius: 0;\n}\n.input-group-addon:last-child {\n border-left: 0;\n}\n.input-group-btn {\n position: relative;\n font-size: 0;\n white-space: nowrap;\n}\n.input-group-btn > .btn {\n position: relative;\n}\n.input-group-btn > .btn + .btn {\n margin-left: -1px;\n}\n.input-group-btn > .btn:hover,\n.input-group-btn > .btn:focus,\n.input-group-btn > .btn:active {\n z-index: 2;\n}\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group {\n margin-right: -1px;\n}\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group {\n z-index: 2;\n margin-left: -1px;\n}\n.nav {\n margin-bottom: 0;\n padding-left: 0;\n list-style: none;\n}\n.nav > li {\n position: relative;\n display: block;\n}\n.nav > li > a {\n position: relative;\n display: block;\n padding: 10px 15px;\n}\n.nav > li > a:hover,\n.nav > li > a:focus {\n text-decoration: none;\n background-color: #eeeeee;\n}\n.nav > li.disabled > a {\n color: #777777;\n}\n.nav > li.disabled > a:hover,\n.nav > li.disabled > a:focus {\n color: #777777;\n text-decoration: none;\n background-color: transparent;\n cursor: not-allowed;\n}\n.nav .open > a,\n.nav .open > a:hover,\n.nav .open > a:focus {\n background-color: #eeeeee;\n border-color: #337ab7;\n}\n.nav .nav-divider {\n height: 1px;\n margin: 9px 0;\n overflow: hidden;\n background-color: #e5e5e5;\n}\n.nav > li > a > img {\n max-width: none;\n}\n.nav-tabs {\n border-bottom: 1px solid #ddd;\n}\n.nav-tabs > li {\n float: left;\n margin-bottom: -1px;\n}\n.nav-tabs > li > a {\n margin-right: 2px;\n line-height: 1.42857143;\n border: 1px solid transparent;\n border-radius: 4px 4px 0 0;\n}\n.nav-tabs > li > a:hover {\n border-color: #eeeeee #eeeeee #ddd;\n}\n.nav-tabs > li.active > a,\n.nav-tabs > li.active > a:hover,\n.nav-tabs > li.active > a:focus {\n color: #555555;\n background-color: #fff;\n border: 1px solid #ddd;\n border-bottom-color: transparent;\n cursor: default;\n}\n.nav-tabs.nav-justified {\n width: 100%;\n border-bottom: 0;\n}\n.nav-tabs.nav-justified > li {\n float: none;\n}\n.nav-tabs.nav-justified > li > a {\n text-align: center;\n margin-bottom: 5px;\n}\n.nav-tabs.nav-justified > .dropdown .dropdown-menu {\n top: auto;\n left: auto;\n}\n@media (min-width: 768px) {\n .nav-tabs.nav-justified > li {\n display: table-cell;\n width: 1%;\n }\n .nav-tabs.nav-justified > li > a {\n margin-bottom: 0;\n }\n}\n.nav-tabs.nav-justified > li > a {\n margin-right: 0;\n border-radius: 4px;\n}\n.nav-tabs.nav-justified > .active > a,\n.nav-tabs.nav-justified > .active > a:hover,\n.nav-tabs.nav-justified > .active > a:focus {\n border: 1px solid #ddd;\n}\n@media (min-width: 768px) {\n .nav-tabs.nav-justified > li > a {\n border-bottom: 1px solid #ddd;\n border-radius: 4px 4px 0 0;\n }\n .nav-tabs.nav-justified > .active > a,\n .nav-tabs.nav-justified > .active > a:hover,\n .nav-tabs.nav-justified > .active > a:focus {\n border-bottom-color: #fff;\n }\n}\n.nav-pills > li {\n float: left;\n}\n.nav-pills > li > a {\n border-radius: 4px;\n}\n.nav-pills > li + li {\n margin-left: 2px;\n}\n.nav-pills > li.active > a,\n.nav-pills > li.active > a:hover,\n.nav-pills > li.active > a:focus {\n color: #fff;\n background-color: #337ab7;\n}\n.nav-stacked > li {\n float: none;\n}\n.nav-stacked > li + li {\n margin-top: 2px;\n margin-left: 0;\n}\n.nav-justified {\n width: 100%;\n}\n.nav-justified > li {\n float: none;\n}\n.nav-justified > li > a {\n text-align: center;\n margin-bottom: 5px;\n}\n.nav-justified > .dropdown .dropdown-menu {\n top: auto;\n left: auto;\n}\n@media (min-width: 768px) {\n .nav-justified > li {\n display: table-cell;\n width: 1%;\n }\n .nav-justified > li > a {\n margin-bottom: 0;\n }\n}\n.nav-tabs-justified {\n border-bottom: 0;\n}\n.nav-tabs-justified > li > a {\n margin-right: 0;\n border-radius: 4px;\n}\n.nav-tabs-justified > .active > a,\n.nav-tabs-justified > .active > a:hover,\n.nav-tabs-justified > .active > a:focus {\n border: 1px solid #ddd;\n}\n@media (min-width: 768px) {\n .nav-tabs-justified > li > a {\n border-bottom: 1px solid #ddd;\n border-radius: 4px 4px 0 0;\n }\n .nav-tabs-justified > .active > a,\n .nav-tabs-justified > .active > a:hover,\n .nav-tabs-justified > .active > a:focus {\n border-bottom-color: #fff;\n }\n}\n.tab-content > .tab-pane {\n display: none;\n}\n.tab-content > .active {\n display: block;\n}\n.nav-tabs .dropdown-menu {\n margin-top: -1px;\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n}\n.navbar {\n position: relative;\n min-height: 50px;\n margin-bottom: 20px;\n border: 1px solid transparent;\n}\n@media (min-width: 768px) {\n .navbar {\n border-radius: 4px;\n }\n}\n@media (min-width: 768px) {\n .navbar-header {\n float: left;\n }\n}\n.navbar-collapse {\n overflow-x: visible;\n padding-right: 15px;\n padding-left: 15px;\n border-top: 1px solid transparent;\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);\n -webkit-overflow-scrolling: touch;\n}\n.navbar-collapse.in {\n overflow-y: auto;\n}\n@media (min-width: 768px) {\n .navbar-collapse {\n width: auto;\n border-top: 0;\n box-shadow: none;\n }\n .navbar-collapse.collapse {\n display: block !important;\n height: auto !important;\n padding-bottom: 0;\n overflow: visible !important;\n }\n .navbar-collapse.in {\n overflow-y: visible;\n }\n .navbar-fixed-top .navbar-collapse,\n .navbar-static-top .navbar-collapse,\n .navbar-fixed-bottom .navbar-collapse {\n padding-left: 0;\n padding-right: 0;\n }\n}\n.navbar-fixed-top .navbar-collapse,\n.navbar-fixed-bottom .navbar-collapse {\n max-height: 340px;\n}\n@media (max-device-width: 480px) and (orientation: landscape) {\n .navbar-fixed-top .navbar-collapse,\n .navbar-fixed-bottom .navbar-collapse {\n max-height: 200px;\n }\n}\n.container > .navbar-header,\n.container-fluid > .navbar-header,\n.container > .navbar-collapse,\n.container-fluid > .navbar-collapse {\n margin-right: -15px;\n margin-left: -15px;\n}\n@media (min-width: 768px) {\n .container > .navbar-header,\n .container-fluid > .navbar-header,\n .container > .navbar-collapse,\n .container-fluid > .navbar-collapse {\n margin-right: 0;\n margin-left: 0;\n }\n}\n.navbar-static-top {\n z-index: 1000;\n border-width: 0 0 1px;\n}\n@media (min-width: 768px) {\n .navbar-static-top {\n border-radius: 0;\n }\n}\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n position: fixed;\n right: 0;\n left: 0;\n z-index: 1030;\n}\n@media (min-width: 768px) {\n .navbar-fixed-top,\n .navbar-fixed-bottom {\n border-radius: 0;\n }\n}\n.navbar-fixed-top {\n top: 0;\n border-width: 0 0 1px;\n}\n.navbar-fixed-bottom {\n bottom: 0;\n margin-bottom: 0;\n border-width: 1px 0 0;\n}\n.navbar-brand {\n float: left;\n padding: 15px 15px;\n font-size: 18px;\n line-height: 20px;\n height: 50px;\n}\n.navbar-brand:hover,\n.navbar-brand:focus {\n text-decoration: none;\n}\n.navbar-brand > img {\n display: block;\n}\n@media (min-width: 768px) {\n .navbar > .container .navbar-brand,\n .navbar > .container-fluid .navbar-brand {\n margin-left: -15px;\n }\n}\n.navbar-toggle {\n position: relative;\n float: right;\n margin-right: 15px;\n padding: 9px 10px;\n margin-top: 8px;\n margin-bottom: 8px;\n background-color: transparent;\n background-image: none;\n border: 1px solid transparent;\n border-radius: 4px;\n}\n.navbar-toggle:focus {\n outline: 0;\n}\n.navbar-toggle .icon-bar {\n display: block;\n width: 22px;\n height: 2px;\n border-radius: 1px;\n}\n.navbar-toggle .icon-bar + .icon-bar {\n margin-top: 4px;\n}\n@media (min-width: 768px) {\n .navbar-toggle {\n display: none;\n }\n}\n.navbar-nav {\n margin: 7.5px -15px;\n}\n.navbar-nav > li > a {\n padding-top: 10px;\n padding-bottom: 10px;\n line-height: 20px;\n}\n@media (max-width: 767px) {\n .navbar-nav .open .dropdown-menu {\n position: static;\n float: none;\n width: auto;\n margin-top: 0;\n background-color: transparent;\n border: 0;\n box-shadow: none;\n }\n .navbar-nav .open .dropdown-menu > li > a,\n .navbar-nav .open .dropdown-menu .dropdown-header {\n padding: 5px 15px 5px 25px;\n }\n .navbar-nav .open .dropdown-menu > li > a {\n line-height: 20px;\n }\n .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-nav .open .dropdown-menu > li > a:focus {\n background-image: none;\n }\n}\n@media (min-width: 768px) {\n .navbar-nav {\n float: left;\n margin: 0;\n }\n .navbar-nav > li {\n float: left;\n }\n .navbar-nav > li > a {\n padding-top: 15px;\n padding-bottom: 15px;\n }\n}\n.navbar-form {\n margin-left: -15px;\n margin-right: -15px;\n padding: 10px 15px;\n border-top: 1px solid transparent;\n border-bottom: 1px solid transparent;\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);\n margin-top: 8px;\n margin-bottom: 8px;\n}\n@media (min-width: 768px) {\n .navbar-form .form-group {\n display: inline-block;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .navbar-form .form-control-static {\n display: inline-block;\n }\n .navbar-form .input-group {\n display: inline-table;\n vertical-align: middle;\n }\n .navbar-form .input-group .input-group-addon,\n .navbar-form .input-group .input-group-btn,\n .navbar-form .input-group .form-control {\n width: auto;\n }\n .navbar-form .input-group > .form-control {\n width: 100%;\n }\n .navbar-form .control-label {\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .radio,\n .navbar-form .checkbox {\n display: inline-block;\n margin-top: 0;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .radio label,\n .navbar-form .checkbox label {\n padding-left: 0;\n }\n .navbar-form .radio input[type=\"radio\"],\n .navbar-form .checkbox input[type=\"checkbox\"] {\n position: relative;\n margin-left: 0;\n }\n .navbar-form .has-feedback .form-control-feedback {\n top: 0;\n }\n}\n@media (max-width: 767px) {\n .navbar-form .form-group {\n margin-bottom: 5px;\n }\n .navbar-form .form-group:last-child {\n margin-bottom: 0;\n }\n}\n@media (min-width: 768px) {\n .navbar-form {\n width: auto;\n border: 0;\n margin-left: 0;\n margin-right: 0;\n padding-top: 0;\n padding-bottom: 0;\n -webkit-box-shadow: none;\n box-shadow: none;\n }\n}\n.navbar-nav > li > .dropdown-menu {\n margin-top: 0;\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n}\n.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {\n margin-bottom: 0;\n border-top-right-radius: 4px;\n border-top-left-radius: 4px;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.navbar-btn {\n margin-top: 8px;\n margin-bottom: 8px;\n}\n.navbar-btn.btn-sm {\n margin-top: 10px;\n margin-bottom: 10px;\n}\n.navbar-btn.btn-xs {\n margin-top: 14px;\n margin-bottom: 14px;\n}\n.navbar-text {\n margin-top: 15px;\n margin-bottom: 15px;\n}\n@media (min-width: 768px) {\n .navbar-text {\n float: left;\n margin-left: 15px;\n margin-right: 15px;\n }\n}\n@media (min-width: 768px) {\n .navbar-left {\n float: left !important;\n }\n .navbar-right {\n float: right !important;\n margin-right: -15px;\n }\n .navbar-right ~ .navbar-right {\n margin-right: 0;\n }\n}\n.navbar-default {\n background-color: #f8f8f8;\n border-color: #e7e7e7;\n}\n.navbar-default .navbar-brand {\n color: #777;\n}\n.navbar-default .navbar-brand:hover,\n.navbar-default .navbar-brand:focus {\n color: #5e5e5e;\n background-color: transparent;\n}\n.navbar-default .navbar-text {\n color: #777;\n}\n.navbar-default .navbar-nav > li > a {\n color: #777;\n}\n.navbar-default .navbar-nav > li > a:hover,\n.navbar-default .navbar-nav > li > a:focus {\n color: #333;\n background-color: transparent;\n}\n.navbar-default .navbar-nav > .active > a,\n.navbar-default .navbar-nav > .active > a:hover,\n.navbar-default .navbar-nav > .active > a:focus {\n color: #555;\n background-color: #e7e7e7;\n}\n.navbar-default .navbar-nav > .disabled > a,\n.navbar-default .navbar-nav > .disabled > a:hover,\n.navbar-default .navbar-nav > .disabled > a:focus {\n color: #ccc;\n background-color: transparent;\n}\n.navbar-default .navbar-toggle {\n border-color: #ddd;\n}\n.navbar-default .navbar-toggle:hover,\n.navbar-default .navbar-toggle:focus {\n background-color: #ddd;\n}\n.navbar-default .navbar-toggle .icon-bar {\n background-color: #888;\n}\n.navbar-default .navbar-collapse,\n.navbar-default .navbar-form {\n border-color: #e7e7e7;\n}\n.navbar-default .navbar-nav > .open > a,\n.navbar-default .navbar-nav > .open > a:hover,\n.navbar-default .navbar-nav > .open > a:focus {\n background-color: #e7e7e7;\n color: #555;\n}\n@media (max-width: 767px) {\n .navbar-default .navbar-nav .open .dropdown-menu > li > a {\n color: #777;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus {\n color: #333;\n background-color: transparent;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a,\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #555;\n background-color: #e7e7e7;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a,\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n color: #ccc;\n background-color: transparent;\n }\n}\n.navbar-default .navbar-link {\n color: #777;\n}\n.navbar-default .navbar-link:hover {\n color: #333;\n}\n.navbar-default .btn-link {\n color: #777;\n}\n.navbar-default .btn-link:hover,\n.navbar-default .btn-link:focus {\n color: #333;\n}\n.navbar-default .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-default .btn-link:hover,\n.navbar-default .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-default .btn-link:focus {\n color: #ccc;\n}\n.navbar-inverse {\n background-color: #222;\n border-color: #080808;\n}\n.navbar-inverse .navbar-brand {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-brand:hover,\n.navbar-inverse .navbar-brand:focus {\n color: #fff;\n background-color: transparent;\n}\n.navbar-inverse .navbar-text {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-nav > li > a {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-nav > li > a:hover,\n.navbar-inverse .navbar-nav > li > a:focus {\n color: #fff;\n background-color: transparent;\n}\n.navbar-inverse .navbar-nav > .active > a,\n.navbar-inverse .navbar-nav > .active > a:hover,\n.navbar-inverse .navbar-nav > .active > a:focus {\n color: #fff;\n background-color: #080808;\n}\n.navbar-inverse .navbar-nav > .disabled > a,\n.navbar-inverse .navbar-nav > .disabled > a:hover,\n.navbar-inverse .navbar-nav > .disabled > a:focus {\n color: #444;\n background-color: transparent;\n}\n.navbar-inverse .navbar-toggle {\n border-color: #333;\n}\n.navbar-inverse .navbar-toggle:hover,\n.navbar-inverse .navbar-toggle:focus {\n background-color: #333;\n}\n.navbar-inverse .navbar-toggle .icon-bar {\n background-color: #fff;\n}\n.navbar-inverse .navbar-collapse,\n.navbar-inverse .navbar-form {\n border-color: #101010;\n}\n.navbar-inverse .navbar-nav > .open > a,\n.navbar-inverse .navbar-nav > .open > a:hover,\n.navbar-inverse .navbar-nav > .open > a:focus {\n background-color: #080808;\n color: #fff;\n}\n@media (max-width: 767px) {\n .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header {\n border-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu .divider {\n background-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a {\n color: #9d9d9d;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus {\n color: #fff;\n background-color: transparent;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #fff;\n background-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n color: #444;\n background-color: transparent;\n }\n}\n.navbar-inverse .navbar-link {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-link:hover {\n color: #fff;\n}\n.navbar-inverse .btn-link {\n color: #9d9d9d;\n}\n.navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link:focus {\n color: #fff;\n}\n.navbar-inverse .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-inverse .btn-link:focus {\n color: #444;\n}\n.breadcrumb {\n padding: 8px 15px;\n margin-bottom: 20px;\n list-style: none;\n background-color: #f5f5f5;\n border-radius: 4px;\n}\n.breadcrumb > li {\n display: inline-block;\n}\n.breadcrumb > li + li:before {\n content: \"/\\00a0\";\n padding: 0 5px;\n color: #ccc;\n}\n.breadcrumb > .active {\n color: #777777;\n}\n.pagination {\n display: inline-block;\n padding-left: 0;\n margin: 20px 0;\n border-radius: 4px;\n}\n.pagination > li {\n display: inline;\n}\n.pagination > li > a,\n.pagination > li > span {\n position: relative;\n float: left;\n padding: 6px 12px;\n line-height: 1.42857143;\n text-decoration: none;\n color: #337ab7;\n background-color: #fff;\n border: 1px solid #ddd;\n margin-left: -1px;\n}\n.pagination > li:first-child > a,\n.pagination > li:first-child > span {\n margin-left: 0;\n border-bottom-left-radius: 4px;\n border-top-left-radius: 4px;\n}\n.pagination > li:last-child > a,\n.pagination > li:last-child > span {\n border-bottom-right-radius: 4px;\n border-top-right-radius: 4px;\n}\n.pagination > li > a:hover,\n.pagination > li > span:hover,\n.pagination > li > a:focus,\n.pagination > li > span:focus {\n z-index: 2;\n color: #23527c;\n background-color: #eeeeee;\n border-color: #ddd;\n}\n.pagination > .active > a,\n.pagination > .active > span,\n.pagination > .active > a:hover,\n.pagination > .active > span:hover,\n.pagination > .active > a:focus,\n.pagination > .active > span:focus {\n z-index: 3;\n color: #fff;\n background-color: #337ab7;\n border-color: #337ab7;\n cursor: default;\n}\n.pagination > .disabled > span,\n.pagination > .disabled > span:hover,\n.pagination > .disabled > span:focus,\n.pagination > .disabled > a,\n.pagination > .disabled > a:hover,\n.pagination > .disabled > a:focus {\n color: #777777;\n background-color: #fff;\n border-color: #ddd;\n cursor: not-allowed;\n}\n.pagination-lg > li > a,\n.pagination-lg > li > span {\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n}\n.pagination-lg > li:first-child > a,\n.pagination-lg > li:first-child > span {\n border-bottom-left-radius: 6px;\n border-top-left-radius: 6px;\n}\n.pagination-lg > li:last-child > a,\n.pagination-lg > li:last-child > span {\n border-bottom-right-radius: 6px;\n border-top-right-radius: 6px;\n}\n.pagination-sm > li > a,\n.pagination-sm > li > span {\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n}\n.pagination-sm > li:first-child > a,\n.pagination-sm > li:first-child > span {\n border-bottom-left-radius: 3px;\n border-top-left-radius: 3px;\n}\n.pagination-sm > li:last-child > a,\n.pagination-sm > li:last-child > span {\n border-bottom-right-radius: 3px;\n border-top-right-radius: 3px;\n}\n.pager {\n padding-left: 0;\n margin: 20px 0;\n list-style: none;\n text-align: center;\n}\n.pager li {\n display: inline;\n}\n.pager li > a,\n.pager li > span {\n display: inline-block;\n padding: 5px 14px;\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 15px;\n}\n.pager li > a:hover,\n.pager li > a:focus {\n text-decoration: none;\n background-color: #eeeeee;\n}\n.pager .next > a,\n.pager .next > span {\n float: right;\n}\n.pager .previous > a,\n.pager .previous > span {\n float: left;\n}\n.pager .disabled > a,\n.pager .disabled > a:hover,\n.pager .disabled > a:focus,\n.pager .disabled > span {\n color: #777777;\n background-color: #fff;\n cursor: not-allowed;\n}\n.label {\n display: inline;\n padding: .2em .6em .3em;\n font-size: 75%;\n font-weight: bold;\n line-height: 1;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: .25em;\n}\na.label:hover,\na.label:focus {\n color: #fff;\n text-decoration: none;\n cursor: pointer;\n}\n.label:empty {\n display: none;\n}\n.btn .label {\n position: relative;\n top: -1px;\n}\n.label-default {\n background-color: #777777;\n}\n.label-default[href]:hover,\n.label-default[href]:focus {\n background-color: #5e5e5e;\n}\n.label-primary {\n background-color: #337ab7;\n}\n.label-primary[href]:hover,\n.label-primary[href]:focus {\n background-color: #286090;\n}\n.label-success {\n background-color: #5cb85c;\n}\n.label-success[href]:hover,\n.label-success[href]:focus {\n background-color: #449d44;\n}\n.label-info {\n background-color: #5bc0de;\n}\n.label-info[href]:hover,\n.label-info[href]:focus {\n background-color: #31b0d5;\n}\n.label-warning {\n background-color: #f0ad4e;\n}\n.label-warning[href]:hover,\n.label-warning[href]:focus {\n background-color: #ec971f;\n}\n.label-danger {\n background-color: #d9534f;\n}\n.label-danger[href]:hover,\n.label-danger[href]:focus {\n background-color: #c9302c;\n}\n.badge {\n display: inline-block;\n min-width: 10px;\n padding: 3px 7px;\n font-size: 12px;\n font-weight: bold;\n color: #fff;\n line-height: 1;\n vertical-align: middle;\n white-space: nowrap;\n text-align: center;\n background-color: #777777;\n border-radius: 10px;\n}\n.badge:empty {\n display: none;\n}\n.btn .badge {\n position: relative;\n top: -1px;\n}\n.btn-xs .badge,\n.btn-group-xs > .btn .badge {\n top: 0;\n padding: 1px 5px;\n}\na.badge:hover,\na.badge:focus {\n color: #fff;\n text-decoration: none;\n cursor: pointer;\n}\n.list-group-item.active > .badge,\n.nav-pills > .active > a > .badge {\n color: #337ab7;\n background-color: #fff;\n}\n.list-group-item > .badge {\n float: right;\n}\n.list-group-item > .badge + .badge {\n margin-right: 5px;\n}\n.nav-pills > li > a > .badge {\n margin-left: 3px;\n}\n.jumbotron {\n padding-top: 30px;\n padding-bottom: 30px;\n margin-bottom: 30px;\n color: inherit;\n background-color: #eeeeee;\n}\n.jumbotron h1,\n.jumbotron .h1 {\n color: inherit;\n}\n.jumbotron p {\n margin-bottom: 15px;\n font-size: 21px;\n font-weight: 200;\n}\n.jumbotron > hr {\n border-top-color: #d5d5d5;\n}\n.container .jumbotron,\n.container-fluid .jumbotron {\n border-radius: 6px;\n padding-left: 15px;\n padding-right: 15px;\n}\n.jumbotron .container {\n max-width: 100%;\n}\n@media screen and (min-width: 768px) {\n .jumbotron {\n padding-top: 48px;\n padding-bottom: 48px;\n }\n .container .jumbotron,\n .container-fluid .jumbotron {\n padding-left: 60px;\n padding-right: 60px;\n }\n .jumbotron h1,\n .jumbotron .h1 {\n font-size: 63px;\n }\n}\n.thumbnail {\n display: block;\n padding: 4px;\n margin-bottom: 20px;\n line-height: 1.42857143;\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 4px;\n -webkit-transition: border 0.2s ease-in-out;\n -o-transition: border 0.2s ease-in-out;\n transition: border 0.2s ease-in-out;\n}\n.thumbnail > img,\n.thumbnail a > img {\n margin-left: auto;\n margin-right: auto;\n}\na.thumbnail:hover,\na.thumbnail:focus,\na.thumbnail.active {\n border-color: #337ab7;\n}\n.thumbnail .caption {\n padding: 9px;\n color: #333333;\n}\n.alert {\n padding: 15px;\n margin-bottom: 20px;\n border: 1px solid transparent;\n border-radius: 4px;\n}\n.alert h4 {\n margin-top: 0;\n color: inherit;\n}\n.alert .alert-link {\n font-weight: bold;\n}\n.alert > p,\n.alert > ul {\n margin-bottom: 0;\n}\n.alert > p + p {\n margin-top: 5px;\n}\n.alert-dismissable,\n.alert-dismissible {\n padding-right: 35px;\n}\n.alert-dismissable .close,\n.alert-dismissible .close {\n position: relative;\n top: -2px;\n right: -21px;\n color: inherit;\n}\n.alert-success {\n background-color: #dff0d8;\n border-color: #d6e9c6;\n color: #3c763d;\n}\n.alert-success hr {\n border-top-color: #c9e2b3;\n}\n.alert-success .alert-link {\n color: #2b542c;\n}\n.alert-info {\n background-color: #d9edf7;\n border-color: #bce8f1;\n color: #31708f;\n}\n.alert-info hr {\n border-top-color: #a6e1ec;\n}\n.alert-info .alert-link {\n color: #245269;\n}\n.alert-warning {\n background-color: #fcf8e3;\n border-color: #faebcc;\n color: #8a6d3b;\n}\n.alert-warning hr {\n border-top-color: #f7e1b5;\n}\n.alert-warning .alert-link {\n color: #66512c;\n}\n.alert-danger {\n background-color: #f2dede;\n border-color: #ebccd1;\n color: #a94442;\n}\n.alert-danger hr {\n border-top-color: #e4b9c0;\n}\n.alert-danger .alert-link {\n color: #843534;\n}\n@-webkit-keyframes progress-bar-stripes {\n from {\n background-position: 40px 0;\n }\n to {\n background-position: 0 0;\n }\n}\n@keyframes progress-bar-stripes {\n from {\n background-position: 40px 0;\n }\n to {\n background-position: 0 0;\n }\n}\n.progress {\n overflow: hidden;\n height: 20px;\n margin-bottom: 20px;\n background-color: #f5f5f5;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\n box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\n}\n.progress-bar {\n float: left;\n width: 0%;\n height: 100%;\n font-size: 12px;\n line-height: 20px;\n color: #fff;\n text-align: center;\n background-color: #337ab7;\n -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\n -webkit-transition: width 0.6s ease;\n -o-transition: width 0.6s ease;\n transition: width 0.6s ease;\n}\n.progress-striped .progress-bar,\n.progress-bar-striped {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-size: 40px 40px;\n}\n.progress.active .progress-bar,\n.progress-bar.active {\n -webkit-animation: progress-bar-stripes 2s linear infinite;\n -o-animation: progress-bar-stripes 2s linear infinite;\n animation: progress-bar-stripes 2s linear infinite;\n}\n.progress-bar-success {\n background-color: #5cb85c;\n}\n.progress-striped .progress-bar-success {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-info {\n background-color: #5bc0de;\n}\n.progress-striped .progress-bar-info {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-warning {\n background-color: #f0ad4e;\n}\n.progress-striped .progress-bar-warning {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-danger {\n background-color: #d9534f;\n}\n.progress-striped .progress-bar-danger {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.media {\n margin-top: 15px;\n}\n.media:first-child {\n margin-top: 0;\n}\n.media,\n.media-body {\n zoom: 1;\n overflow: hidden;\n}\n.media-body {\n width: 10000px;\n}\n.media-object {\n display: block;\n}\n.media-object.img-thumbnail {\n max-width: none;\n}\n.media-right,\n.media > .pull-right {\n padding-left: 10px;\n}\n.media-left,\n.media > .pull-left {\n padding-right: 10px;\n}\n.media-left,\n.media-right,\n.media-body {\n display: table-cell;\n vertical-align: top;\n}\n.media-middle {\n vertical-align: middle;\n}\n.media-bottom {\n vertical-align: bottom;\n}\n.media-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n.media-list {\n padding-left: 0;\n list-style: none;\n}\n.list-group {\n margin-bottom: 20px;\n padding-left: 0;\n}\n.list-group-item {\n position: relative;\n display: block;\n padding: 10px 15px;\n margin-bottom: -1px;\n background-color: #fff;\n border: 1px solid #ddd;\n}\n.list-group-item:first-child {\n border-top-right-radius: 4px;\n border-top-left-radius: 4px;\n}\n.list-group-item:last-child {\n margin-bottom: 0;\n border-bottom-right-radius: 4px;\n border-bottom-left-radius: 4px;\n}\na.list-group-item,\nbutton.list-group-item {\n color: #555;\n}\na.list-group-item .list-group-item-heading,\nbutton.list-group-item .list-group-item-heading {\n color: #333;\n}\na.list-group-item:hover,\nbutton.list-group-item:hover,\na.list-group-item:focus,\nbutton.list-group-item:focus {\n text-decoration: none;\n color: #555;\n background-color: #f5f5f5;\n}\nbutton.list-group-item {\n width: 100%;\n text-align: left;\n}\n.list-group-item.disabled,\n.list-group-item.disabled:hover,\n.list-group-item.disabled:focus {\n background-color: #eeeeee;\n color: #777777;\n cursor: not-allowed;\n}\n.list-group-item.disabled .list-group-item-heading,\n.list-group-item.disabled:hover .list-group-item-heading,\n.list-group-item.disabled:focus .list-group-item-heading {\n color: inherit;\n}\n.list-group-item.disabled .list-group-item-text,\n.list-group-item.disabled:hover .list-group-item-text,\n.list-group-item.disabled:focus .list-group-item-text {\n color: #777777;\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n z-index: 2;\n color: #fff;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.list-group-item.active .list-group-item-heading,\n.list-group-item.active:hover .list-group-item-heading,\n.list-group-item.active:focus .list-group-item-heading,\n.list-group-item.active .list-group-item-heading > small,\n.list-group-item.active:hover .list-group-item-heading > small,\n.list-group-item.active:focus .list-group-item-heading > small,\n.list-group-item.active .list-group-item-heading > .small,\n.list-group-item.active:hover .list-group-item-heading > .small,\n.list-group-item.active:focus .list-group-item-heading > .small {\n color: inherit;\n}\n.list-group-item.active .list-group-item-text,\n.list-group-item.active:hover .list-group-item-text,\n.list-group-item.active:focus .list-group-item-text {\n color: #c7ddef;\n}\n.list-group-item-success {\n color: #3c763d;\n background-color: #dff0d8;\n}\na.list-group-item-success,\nbutton.list-group-item-success {\n color: #3c763d;\n}\na.list-group-item-success .list-group-item-heading,\nbutton.list-group-item-success .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-success:hover,\nbutton.list-group-item-success:hover,\na.list-group-item-success:focus,\nbutton.list-group-item-success:focus {\n color: #3c763d;\n background-color: #d0e9c6;\n}\na.list-group-item-success.active,\nbutton.list-group-item-success.active,\na.list-group-item-success.active:hover,\nbutton.list-group-item-success.active:hover,\na.list-group-item-success.active:focus,\nbutton.list-group-item-success.active:focus {\n color: #fff;\n background-color: #3c763d;\n border-color: #3c763d;\n}\n.list-group-item-info {\n color: #31708f;\n background-color: #d9edf7;\n}\na.list-group-item-info,\nbutton.list-group-item-info {\n color: #31708f;\n}\na.list-group-item-info .list-group-item-heading,\nbutton.list-group-item-info .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-info:hover,\nbutton.list-group-item-info:hover,\na.list-group-item-info:focus,\nbutton.list-group-item-info:focus {\n color: #31708f;\n background-color: #c4e3f3;\n}\na.list-group-item-info.active,\nbutton.list-group-item-info.active,\na.list-group-item-info.active:hover,\nbutton.list-group-item-info.active:hover,\na.list-group-item-info.active:focus,\nbutton.list-group-item-info.active:focus {\n color: #fff;\n background-color: #31708f;\n border-color: #31708f;\n}\n.list-group-item-warning {\n color: #8a6d3b;\n background-color: #fcf8e3;\n}\na.list-group-item-warning,\nbutton.list-group-item-warning {\n color: #8a6d3b;\n}\na.list-group-item-warning .list-group-item-heading,\nbutton.list-group-item-warning .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-warning:hover,\nbutton.list-group-item-warning:hover,\na.list-group-item-warning:focus,\nbutton.list-group-item-warning:focus {\n color: #8a6d3b;\n background-color: #faf2cc;\n}\na.list-group-item-warning.active,\nbutton.list-group-item-warning.active,\na.list-group-item-warning.active:hover,\nbutton.list-group-item-warning.active:hover,\na.list-group-item-warning.active:focus,\nbutton.list-group-item-warning.active:focus {\n color: #fff;\n background-color: #8a6d3b;\n border-color: #8a6d3b;\n}\n.list-group-item-danger {\n color: #a94442;\n background-color: #f2dede;\n}\na.list-group-item-danger,\nbutton.list-group-item-danger {\n color: #a94442;\n}\na.list-group-item-danger .list-group-item-heading,\nbutton.list-group-item-danger .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-danger:hover,\nbutton.list-group-item-danger:hover,\na.list-group-item-danger:focus,\nbutton.list-group-item-danger:focus {\n color: #a94442;\n background-color: #ebcccc;\n}\na.list-group-item-danger.active,\nbutton.list-group-item-danger.active,\na.list-group-item-danger.active:hover,\nbutton.list-group-item-danger.active:hover,\na.list-group-item-danger.active:focus,\nbutton.list-group-item-danger.active:focus {\n color: #fff;\n background-color: #a94442;\n border-color: #a94442;\n}\n.list-group-item-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n.list-group-item-text {\n margin-bottom: 0;\n line-height: 1.3;\n}\n.panel {\n margin-bottom: 20px;\n background-color: #fff;\n border: 1px solid transparent;\n border-radius: 4px;\n -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\n box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\n}\n.panel-body {\n padding: 15px;\n}\n.panel-heading {\n padding: 10px 15px;\n border-bottom: 1px solid transparent;\n border-top-right-radius: 3px;\n border-top-left-radius: 3px;\n}\n.panel-heading > .dropdown .dropdown-toggle {\n color: inherit;\n}\n.panel-title {\n margin-top: 0;\n margin-bottom: 0;\n font-size: 16px;\n color: inherit;\n}\n.panel-title > a,\n.panel-title > small,\n.panel-title > .small,\n.panel-title > small > a,\n.panel-title > .small > a {\n color: inherit;\n}\n.panel-footer {\n padding: 10px 15px;\n background-color: #f5f5f5;\n border-top: 1px solid #ddd;\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .list-group,\n.panel > .panel-collapse > .list-group {\n margin-bottom: 0;\n}\n.panel > .list-group .list-group-item,\n.panel > .panel-collapse > .list-group .list-group-item {\n border-width: 1px 0;\n border-radius: 0;\n}\n.panel > .list-group:first-child .list-group-item:first-child,\n.panel > .panel-collapse > .list-group:first-child .list-group-item:first-child {\n border-top: 0;\n border-top-right-radius: 3px;\n border-top-left-radius: 3px;\n}\n.panel > .list-group:last-child .list-group-item:last-child,\n.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child {\n border-bottom: 0;\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .panel-heading + .panel-collapse > .list-group .list-group-item:first-child {\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n}\n.panel-heading + .list-group .list-group-item:first-child {\n border-top-width: 0;\n}\n.list-group + .panel-footer {\n border-top-width: 0;\n}\n.panel > .table,\n.panel > .table-responsive > .table,\n.panel > .panel-collapse > .table {\n margin-bottom: 0;\n}\n.panel > .table caption,\n.panel > .table-responsive > .table caption,\n.panel > .panel-collapse > .table caption {\n padding-left: 15px;\n padding-right: 15px;\n}\n.panel > .table:first-child,\n.panel > .table-responsive:first-child > .table:first-child {\n border-top-right-radius: 3px;\n border-top-left-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child {\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child {\n border-top-left-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child {\n border-top-right-radius: 3px;\n}\n.panel > .table:last-child,\n.panel > .table-responsive:last-child > .table:last-child {\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child {\n border-bottom-left-radius: 3px;\n border-bottom-right-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child {\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child {\n border-bottom-right-radius: 3px;\n}\n.panel > .panel-body + .table,\n.panel > .panel-body + .table-responsive,\n.panel > .table + .panel-body,\n.panel > .table-responsive + .panel-body {\n border-top: 1px solid #ddd;\n}\n.panel > .table > tbody:first-child > tr:first-child th,\n.panel > .table > tbody:first-child > tr:first-child td {\n border-top: 0;\n}\n.panel > .table-bordered,\n.panel > .table-responsive > .table-bordered {\n border: 0;\n}\n.panel > .table-bordered > thead > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:first-child,\n.panel > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-bordered > thead > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:first-child,\n.panel > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-bordered > tfoot > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n border-left: 0;\n}\n.panel > .table-bordered > thead > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:last-child,\n.panel > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-bordered > thead > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:last-child,\n.panel > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-bordered > tfoot > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n border-right: 0;\n}\n.panel > .table-bordered > thead > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > td,\n.panel > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-bordered > thead > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > th,\n.panel > .table-bordered > tbody > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th {\n border-bottom: 0;\n}\n.panel > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-bordered > tfoot > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th {\n border-bottom: 0;\n}\n.panel > .table-responsive {\n border: 0;\n margin-bottom: 0;\n}\n.panel-group {\n margin-bottom: 20px;\n}\n.panel-group .panel {\n margin-bottom: 0;\n border-radius: 4px;\n}\n.panel-group .panel + .panel {\n margin-top: 5px;\n}\n.panel-group .panel-heading {\n border-bottom: 0;\n}\n.panel-group .panel-heading + .panel-collapse > .panel-body,\n.panel-group .panel-heading + .panel-collapse > .list-group {\n border-top: 1px solid #ddd;\n}\n.panel-group .panel-footer {\n border-top: 0;\n}\n.panel-group .panel-footer + .panel-collapse .panel-body {\n border-bottom: 1px solid #ddd;\n}\n.panel-default {\n border-color: #ddd;\n}\n.panel-default > .panel-heading {\n color: #333333;\n background-color: #f5f5f5;\n border-color: #ddd;\n}\n.panel-default > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #ddd;\n}\n.panel-default > .panel-heading .badge {\n color: #f5f5f5;\n background-color: #333333;\n}\n.panel-default > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #ddd;\n}\n.panel-primary {\n border-color: #337ab7;\n}\n.panel-primary > .panel-heading {\n color: #fff;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.panel-primary > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #337ab7;\n}\n.panel-primary > .panel-heading .badge {\n color: #337ab7;\n background-color: #fff;\n}\n.panel-primary > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #337ab7;\n}\n.panel-success {\n border-color: #d6e9c6;\n}\n.panel-success > .panel-heading {\n color: #3c763d;\n background-color: #dff0d8;\n border-color: #d6e9c6;\n}\n.panel-success > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #d6e9c6;\n}\n.panel-success > .panel-heading .badge {\n color: #dff0d8;\n background-color: #3c763d;\n}\n.panel-success > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #d6e9c6;\n}\n.panel-info {\n border-color: #bce8f1;\n}\n.panel-info > .panel-heading {\n color: #31708f;\n background-color: #d9edf7;\n border-color: #bce8f1;\n}\n.panel-info > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #bce8f1;\n}\n.panel-info > .panel-heading .badge {\n color: #d9edf7;\n background-color: #31708f;\n}\n.panel-info > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #bce8f1;\n}\n.panel-warning {\n border-color: #faebcc;\n}\n.panel-warning > .panel-heading {\n color: #8a6d3b;\n background-color: #fcf8e3;\n border-color: #faebcc;\n}\n.panel-warning > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #faebcc;\n}\n.panel-warning > .panel-heading .badge {\n color: #fcf8e3;\n background-color: #8a6d3b;\n}\n.panel-warning > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #faebcc;\n}\n.panel-danger {\n border-color: #ebccd1;\n}\n.panel-danger > .panel-heading {\n color: #a94442;\n background-color: #f2dede;\n border-color: #ebccd1;\n}\n.panel-danger > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #ebccd1;\n}\n.panel-danger > .panel-heading .badge {\n color: #f2dede;\n background-color: #a94442;\n}\n.panel-danger > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #ebccd1;\n}\n.embed-responsive {\n position: relative;\n display: block;\n height: 0;\n padding: 0;\n overflow: hidden;\n}\n.embed-responsive .embed-responsive-item,\n.embed-responsive iframe,\n.embed-responsive embed,\n.embed-responsive object,\n.embed-responsive video {\n position: absolute;\n top: 0;\n left: 0;\n bottom: 0;\n height: 100%;\n width: 100%;\n border: 0;\n}\n.embed-responsive-16by9 {\n padding-bottom: 56.25%;\n}\n.embed-responsive-4by3 {\n padding-bottom: 75%;\n}\n.well {\n min-height: 20px;\n padding: 19px;\n margin-bottom: 20px;\n background-color: #f5f5f5;\n border: 1px solid #e3e3e3;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);\n}\n.well blockquote {\n border-color: #ddd;\n border-color: rgba(0, 0, 0, 0.15);\n}\n.well-lg {\n padding: 24px;\n border-radius: 6px;\n}\n.well-sm {\n padding: 9px;\n border-radius: 3px;\n}\n.close {\n float: right;\n font-size: 21px;\n font-weight: bold;\n line-height: 1;\n color: #000;\n text-shadow: 0 1px 0 #fff;\n opacity: 0.2;\n filter: alpha(opacity=20);\n}\n.close:hover,\n.close:focus {\n color: #000;\n text-decoration: none;\n cursor: pointer;\n opacity: 0.5;\n filter: alpha(opacity=50);\n}\nbutton.close {\n padding: 0;\n cursor: pointer;\n background: transparent;\n border: 0;\n -webkit-appearance: none;\n}\n.modal-open {\n overflow: hidden;\n}\n.modal {\n display: none;\n overflow: hidden;\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1050;\n -webkit-overflow-scrolling: touch;\n outline: 0;\n}\n.modal.fade .modal-dialog {\n -webkit-transform: translate(0, -25%);\n -ms-transform: translate(0, -25%);\n -o-transform: translate(0, -25%);\n transform: translate(0, -25%);\n -webkit-transition: -webkit-transform 0.3s ease-out;\n -moz-transition: -moz-transform 0.3s ease-out;\n -o-transition: -o-transform 0.3s ease-out;\n transition: transform 0.3s ease-out;\n}\n.modal.in .modal-dialog {\n -webkit-transform: translate(0, 0);\n -ms-transform: translate(0, 0);\n -o-transform: translate(0, 0);\n transform: translate(0, 0);\n}\n.modal-open .modal {\n overflow-x: hidden;\n overflow-y: auto;\n}\n.modal-dialog {\n position: relative;\n width: auto;\n margin: 10px;\n}\n.modal-content {\n position: relative;\n background-color: #fff;\n border: 1px solid #999;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 6px;\n -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n background-clip: padding-box;\n outline: 0;\n}\n.modal-backdrop {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1040;\n background-color: #000;\n}\n.modal-backdrop.fade {\n opacity: 0;\n filter: alpha(opacity=0);\n}\n.modal-backdrop.in {\n opacity: 0.5;\n filter: alpha(opacity=50);\n}\n.modal-header {\n padding: 15px;\n border-bottom: 1px solid #e5e5e5;\n}\n.modal-header .close {\n margin-top: -2px;\n}\n.modal-title {\n margin: 0;\n line-height: 1.42857143;\n}\n.modal-body {\n position: relative;\n padding: 15px;\n}\n.modal-footer {\n padding: 15px;\n text-align: right;\n border-top: 1px solid #e5e5e5;\n}\n.modal-footer .btn + .btn {\n margin-left: 5px;\n margin-bottom: 0;\n}\n.modal-footer .btn-group .btn + .btn {\n margin-left: -1px;\n}\n.modal-footer .btn-block + .btn-block {\n margin-left: 0;\n}\n.modal-scrollbar-measure {\n position: absolute;\n top: -9999px;\n width: 50px;\n height: 50px;\n overflow: scroll;\n}\n@media (min-width: 768px) {\n .modal-dialog {\n width: 600px;\n margin: 30px auto;\n }\n .modal-content {\n -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n }\n .modal-sm {\n width: 300px;\n }\n}\n@media (min-width: 992px) {\n .modal-lg {\n width: 900px;\n }\n}\n.tooltip {\n position: absolute;\n z-index: 1070;\n display: block;\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-style: normal;\n font-weight: normal;\n letter-spacing: normal;\n line-break: auto;\n line-height: 1.42857143;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n white-space: normal;\n word-break: normal;\n word-spacing: normal;\n word-wrap: normal;\n font-size: 12px;\n opacity: 0;\n filter: alpha(opacity=0);\n}\n.tooltip.in {\n opacity: 0.9;\n filter: alpha(opacity=90);\n}\n.tooltip.top {\n margin-top: -3px;\n padding: 5px 0;\n}\n.tooltip.right {\n margin-left: 3px;\n padding: 0 5px;\n}\n.tooltip.bottom {\n margin-top: 3px;\n padding: 5px 0;\n}\n.tooltip.left {\n margin-left: -3px;\n padding: 0 5px;\n}\n.tooltip-inner {\n max-width: 200px;\n padding: 3px 8px;\n color: #fff;\n text-align: center;\n background-color: #000;\n border-radius: 4px;\n}\n.tooltip-arrow {\n position: absolute;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n}\n.tooltip.top .tooltip-arrow {\n bottom: 0;\n left: 50%;\n margin-left: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000;\n}\n.tooltip.top-left .tooltip-arrow {\n bottom: 0;\n right: 5px;\n margin-bottom: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000;\n}\n.tooltip.top-right .tooltip-arrow {\n bottom: 0;\n left: 5px;\n margin-bottom: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000;\n}\n.tooltip.right .tooltip-arrow {\n top: 50%;\n left: 0;\n margin-top: -5px;\n border-width: 5px 5px 5px 0;\n border-right-color: #000;\n}\n.tooltip.left .tooltip-arrow {\n top: 50%;\n right: 0;\n margin-top: -5px;\n border-width: 5px 0 5px 5px;\n border-left-color: #000;\n}\n.tooltip.bottom .tooltip-arrow {\n top: 0;\n left: 50%;\n margin-left: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000;\n}\n.tooltip.bottom-left .tooltip-arrow {\n top: 0;\n right: 5px;\n margin-top: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000;\n}\n.tooltip.bottom-right .tooltip-arrow {\n top: 0;\n left: 5px;\n margin-top: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000;\n}\n.popover {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 1060;\n display: none;\n max-width: 276px;\n padding: 1px;\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-style: normal;\n font-weight: normal;\n letter-spacing: normal;\n line-break: auto;\n line-height: 1.42857143;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n white-space: normal;\n word-break: normal;\n word-spacing: normal;\n word-wrap: normal;\n font-size: 14px;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #ccc;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 6px;\n -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\n box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\n}\n.popover.top {\n margin-top: -10px;\n}\n.popover.right {\n margin-left: 10px;\n}\n.popover.bottom {\n margin-top: 10px;\n}\n.popover.left {\n margin-left: -10px;\n}\n.popover-title {\n margin: 0;\n padding: 8px 14px;\n font-size: 14px;\n background-color: #f7f7f7;\n border-bottom: 1px solid #ebebeb;\n border-radius: 5px 5px 0 0;\n}\n.popover-content {\n padding: 9px 14px;\n}\n.popover > .arrow,\n.popover > .arrow:after {\n position: absolute;\n display: block;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n}\n.popover > .arrow {\n border-width: 11px;\n}\n.popover > .arrow:after {\n border-width: 10px;\n content: \"\";\n}\n.popover.top > .arrow {\n left: 50%;\n margin-left: -11px;\n border-bottom-width: 0;\n border-top-color: #999999;\n border-top-color: rgba(0, 0, 0, 0.25);\n bottom: -11px;\n}\n.popover.top > .arrow:after {\n content: \" \";\n bottom: 1px;\n margin-left: -10px;\n border-bottom-width: 0;\n border-top-color: #fff;\n}\n.popover.right > .arrow {\n top: 50%;\n left: -11px;\n margin-top: -11px;\n border-left-width: 0;\n border-right-color: #999999;\n border-right-color: rgba(0, 0, 0, 0.25);\n}\n.popover.right > .arrow:after {\n content: \" \";\n left: 1px;\n bottom: -10px;\n border-left-width: 0;\n border-right-color: #fff;\n}\n.popover.bottom > .arrow {\n left: 50%;\n margin-left: -11px;\n border-top-width: 0;\n border-bottom-color: #999999;\n border-bottom-color: rgba(0, 0, 0, 0.25);\n top: -11px;\n}\n.popover.bottom > .arrow:after {\n content: \" \";\n top: 1px;\n margin-left: -10px;\n border-top-width: 0;\n border-bottom-color: #fff;\n}\n.popover.left > .arrow {\n top: 50%;\n right: -11px;\n margin-top: -11px;\n border-right-width: 0;\n border-left-color: #999999;\n border-left-color: rgba(0, 0, 0, 0.25);\n}\n.popover.left > .arrow:after {\n content: \" \";\n right: 1px;\n border-right-width: 0;\n border-left-color: #fff;\n bottom: -10px;\n}\n.carousel {\n position: relative;\n}\n.carousel-inner {\n position: relative;\n overflow: hidden;\n width: 100%;\n}\n.carousel-inner > .item {\n display: none;\n position: relative;\n -webkit-transition: 0.6s ease-in-out left;\n -o-transition: 0.6s ease-in-out left;\n transition: 0.6s ease-in-out left;\n}\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n line-height: 1;\n}\n@media all and (transform-3d), (-webkit-transform-3d) {\n .carousel-inner > .item {\n -webkit-transition: -webkit-transform 0.6s ease-in-out;\n -moz-transition: -moz-transform 0.6s ease-in-out;\n -o-transition: -o-transform 0.6s ease-in-out;\n transition: transform 0.6s ease-in-out;\n -webkit-backface-visibility: hidden;\n -moz-backface-visibility: hidden;\n backface-visibility: hidden;\n -webkit-perspective: 1000px;\n -moz-perspective: 1000px;\n perspective: 1000px;\n }\n .carousel-inner > .item.next,\n .carousel-inner > .item.active.right {\n -webkit-transform: translate3d(100%, 0, 0);\n transform: translate3d(100%, 0, 0);\n left: 0;\n }\n .carousel-inner > .item.prev,\n .carousel-inner > .item.active.left {\n -webkit-transform: translate3d(-100%, 0, 0);\n transform: translate3d(-100%, 0, 0);\n left: 0;\n }\n .carousel-inner > .item.next.left,\n .carousel-inner > .item.prev.right,\n .carousel-inner > .item.active {\n -webkit-transform: translate3d(0, 0, 0);\n transform: translate3d(0, 0, 0);\n left: 0;\n }\n}\n.carousel-inner > .active,\n.carousel-inner > .next,\n.carousel-inner > .prev {\n display: block;\n}\n.carousel-inner > .active {\n left: 0;\n}\n.carousel-inner > .next,\n.carousel-inner > .prev {\n position: absolute;\n top: 0;\n width: 100%;\n}\n.carousel-inner > .next {\n left: 100%;\n}\n.carousel-inner > .prev {\n left: -100%;\n}\n.carousel-inner > .next.left,\n.carousel-inner > .prev.right {\n left: 0;\n}\n.carousel-inner > .active.left {\n left: -100%;\n}\n.carousel-inner > .active.right {\n left: 100%;\n}\n.carousel-control {\n position: absolute;\n top: 0;\n left: 0;\n bottom: 0;\n width: 15%;\n opacity: 0.5;\n filter: alpha(opacity=50);\n font-size: 20px;\n color: #fff;\n text-align: center;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\n background-color: rgba(0, 0, 0, 0);\n}\n.carousel-control.left {\n background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);\n}\n.carousel-control.right {\n left: auto;\n right: 0;\n background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-image: linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);\n}\n.carousel-control:hover,\n.carousel-control:focus {\n outline: 0;\n color: #fff;\n text-decoration: none;\n opacity: 0.9;\n filter: alpha(opacity=90);\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-left,\n.carousel-control .glyphicon-chevron-right {\n position: absolute;\n top: 50%;\n margin-top: -10px;\n z-index: 5;\n display: inline-block;\n}\n.carousel-control .icon-prev,\n.carousel-control .glyphicon-chevron-left {\n left: 50%;\n margin-left: -10px;\n}\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-right {\n right: 50%;\n margin-right: -10px;\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next {\n width: 20px;\n height: 20px;\n line-height: 1;\n font-family: serif;\n}\n.carousel-control .icon-prev:before {\n content: '\\2039';\n}\n.carousel-control .icon-next:before {\n content: '\\203a';\n}\n.carousel-indicators {\n position: absolute;\n bottom: 10px;\n left: 50%;\n z-index: 15;\n width: 60%;\n margin-left: -30%;\n padding-left: 0;\n list-style: none;\n text-align: center;\n}\n.carousel-indicators li {\n display: inline-block;\n width: 10px;\n height: 10px;\n margin: 1px;\n text-indent: -999px;\n border: 1px solid #fff;\n border-radius: 10px;\n cursor: pointer;\n background-color: #000 \\9;\n background-color: rgba(0, 0, 0, 0);\n}\n.carousel-indicators .active {\n margin: 0;\n width: 12px;\n height: 12px;\n background-color: #fff;\n}\n.carousel-caption {\n position: absolute;\n left: 15%;\n right: 15%;\n bottom: 20px;\n z-index: 10;\n padding-top: 20px;\n padding-bottom: 20px;\n color: #fff;\n text-align: center;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\n}\n.carousel-caption .btn {\n text-shadow: none;\n}\n@media screen and (min-width: 768px) {\n .carousel-control .glyphicon-chevron-left,\n .carousel-control .glyphicon-chevron-right,\n .carousel-control .icon-prev,\n .carousel-control .icon-next {\n width: 30px;\n height: 30px;\n margin-top: -10px;\n font-size: 30px;\n }\n .carousel-control .glyphicon-chevron-left,\n .carousel-control .icon-prev {\n margin-left: -10px;\n }\n .carousel-control .glyphicon-chevron-right,\n .carousel-control .icon-next {\n margin-right: -10px;\n }\n .carousel-caption {\n left: 20%;\n right: 20%;\n padding-bottom: 30px;\n }\n .carousel-indicators {\n bottom: 20px;\n }\n}\n.clearfix:before,\n.clearfix:after,\n.dl-horizontal dd:before,\n.dl-horizontal dd:after,\n.container:before,\n.container:after,\n.container-fluid:before,\n.container-fluid:after,\n.row:before,\n.row:after,\n.form-horizontal .form-group:before,\n.form-horizontal .form-group:after,\n.btn-toolbar:before,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:before,\n.btn-group-vertical > .btn-group:after,\n.nav:before,\n.nav:after,\n.navbar:before,\n.navbar:after,\n.navbar-header:before,\n.navbar-header:after,\n.navbar-collapse:before,\n.navbar-collapse:after,\n.pager:before,\n.pager:after,\n.panel-body:before,\n.panel-body:after,\n.modal-header:before,\n.modal-header:after,\n.modal-footer:before,\n.modal-footer:after {\n content: \" \";\n display: table;\n}\n.clearfix:after,\n.dl-horizontal dd:after,\n.container:after,\n.container-fluid:after,\n.row:after,\n.form-horizontal .form-group:after,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:after,\n.nav:after,\n.navbar:after,\n.navbar-header:after,\n.navbar-collapse:after,\n.pager:after,\n.panel-body:after,\n.modal-header:after,\n.modal-footer:after {\n clear: both;\n}\n.center-block {\n display: block;\n margin-left: auto;\n margin-right: auto;\n}\n.pull-right {\n float: right !important;\n}\n.pull-left {\n float: left !important;\n}\n.hide {\n display: none !important;\n}\n.show {\n display: block !important;\n}\n.invisible {\n visibility: hidden;\n}\n.text-hide {\n font: 0/0 a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n}\n.hidden {\n display: none !important;\n}\n.affix {\n position: fixed;\n}\n@-ms-viewport {\n width: device-width;\n}\n.visible-xs,\n.visible-sm,\n.visible-md,\n.visible-lg {\n display: none !important;\n}\n.visible-xs-block,\n.visible-xs-inline,\n.visible-xs-inline-block,\n.visible-sm-block,\n.visible-sm-inline,\n.visible-sm-inline-block,\n.visible-md-block,\n.visible-md-inline,\n.visible-md-inline-block,\n.visible-lg-block,\n.visible-lg-inline,\n.visible-lg-inline-block {\n display: none !important;\n}\n@media (max-width: 767px) {\n .visible-xs {\n display: block !important;\n }\n table.visible-xs {\n display: table !important;\n }\n tr.visible-xs {\n display: table-row !important;\n }\n th.visible-xs,\n td.visible-xs {\n display: table-cell !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-block {\n display: block !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-inline {\n display: inline !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm {\n display: block !important;\n }\n table.visible-sm {\n display: table !important;\n }\n tr.visible-sm {\n display: table-row !important;\n }\n th.visible-sm,\n td.visible-sm {\n display: table-cell !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-block {\n display: block !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-inline {\n display: inline !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md {\n display: block !important;\n }\n table.visible-md {\n display: table !important;\n }\n tr.visible-md {\n display: table-row !important;\n }\n th.visible-md,\n td.visible-md {\n display: table-cell !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-block {\n display: block !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-inline {\n display: inline !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg {\n display: block !important;\n }\n table.visible-lg {\n display: table !important;\n }\n tr.visible-lg {\n display: table-row !important;\n }\n th.visible-lg,\n td.visible-lg {\n display: table-cell !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-block {\n display: block !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-inline {\n display: inline !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-inline-block {\n display: inline-block !important;\n }\n}\n@media (max-width: 767px) {\n .hidden-xs {\n display: none !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .hidden-sm {\n display: none !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .hidden-md {\n display: none !important;\n }\n}\n@media (min-width: 1200px) {\n .hidden-lg {\n display: none !important;\n }\n}\n.visible-print {\n display: none !important;\n}\n@media print {\n .visible-print {\n display: block !important;\n }\n table.visible-print {\n display: table !important;\n }\n tr.visible-print {\n display: table-row !important;\n }\n th.visible-print,\n td.visible-print {\n display: table-cell !important;\n }\n}\n.visible-print-block {\n display: none !important;\n}\n@media print {\n .visible-print-block {\n display: block !important;\n }\n}\n.visible-print-inline {\n display: none !important;\n}\n@media print {\n .visible-print-inline {\n display: inline !important;\n }\n}\n.visible-print-inline-block {\n display: none !important;\n}\n@media print {\n .visible-print-inline-block {\n display: inline-block !important;\n }\n}\n@media print {\n .hidden-print {\n display: none !important;\n }\n}\n/*# sourceMappingURL=bootstrap.css.map */","/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */\n\n//\n// 1. Set default font family to sans-serif.\n// 2. Prevent iOS and IE text size adjust after device orientation change,\n// without disabling user zoom.\n//\n\nhtml {\n font-family: sans-serif; // 1\n -ms-text-size-adjust: 100%; // 2\n -webkit-text-size-adjust: 100%; // 2\n}\n\n//\n// Remove default margin.\n//\n\nbody {\n margin: 0;\n}\n\n// HTML5 display definitions\n// ==========================================================================\n\n//\n// Correct `block` display not defined for any HTML5 element in IE 8/9.\n// Correct `block` display not defined for `details` or `summary` in IE 10/11\n// and Firefox.\n// Correct `block` display not defined for `main` in IE 11.\n//\n\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nmenu,\nnav,\nsection,\nsummary {\n display: block;\n}\n\n//\n// 1. Correct `inline-block` display not defined in IE 8/9.\n// 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.\n//\n\naudio,\ncanvas,\nprogress,\nvideo {\n display: inline-block; // 1\n vertical-align: baseline; // 2\n}\n\n//\n// Prevent modern browsers from displaying `audio` without controls.\n// Remove excess height in iOS 5 devices.\n//\n\naudio:not([controls]) {\n display: none;\n height: 0;\n}\n\n//\n// Address `[hidden]` styling not present in IE 8/9/10.\n// Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22.\n//\n\n[hidden],\ntemplate {\n display: none;\n}\n\n// Links\n// ==========================================================================\n\n//\n// Remove the gray background color from active links in IE 10.\n//\n\na {\n background-color: transparent;\n}\n\n//\n// Improve readability of focused elements when they are also in an\n// active/hover state.\n//\n\na:active,\na:hover {\n outline: 0;\n}\n\n// Text-level semantics\n// ==========================================================================\n\n//\n// Address styling not present in IE 8/9/10/11, Safari, and Chrome.\n//\n\nabbr[title] {\n border-bottom: 1px dotted;\n}\n\n//\n// Address style set to `bolder` in Firefox 4+, Safari, and Chrome.\n//\n\nb,\nstrong {\n font-weight: bold;\n}\n\n//\n// Address styling not present in Safari and Chrome.\n//\n\ndfn {\n font-style: italic;\n}\n\n//\n// Address variable `h1` font-size and margin within `section` and `article`\n// contexts in Firefox 4+, Safari, and Chrome.\n//\n\nh1 {\n font-size: 2em;\n margin: 0.67em 0;\n}\n\n//\n// Address styling not present in IE 8/9.\n//\n\nmark {\n background: #ff0;\n color: #000;\n}\n\n//\n// Address inconsistent and variable font size in all browsers.\n//\n\nsmall {\n font-size: 80%;\n}\n\n//\n// Prevent `sub` and `sup` affecting `line-height` in all browsers.\n//\n\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\n\nsup {\n top: -0.5em;\n}\n\nsub {\n bottom: -0.25em;\n}\n\n// Embedded content\n// ==========================================================================\n\n//\n// Remove border when inside `a` element in IE 8/9/10.\n//\n\nimg {\n border: 0;\n}\n\n//\n// Correct overflow not hidden in IE 9/10/11.\n//\n\nsvg:not(:root) {\n overflow: hidden;\n}\n\n// Grouping content\n// ==========================================================================\n\n//\n// Address margin not present in IE 8/9 and Safari.\n//\n\nfigure {\n margin: 1em 40px;\n}\n\n//\n// Address differences between Firefox and other browsers.\n//\n\nhr {\n box-sizing: content-box;\n height: 0;\n}\n\n//\n// Contain overflow in all browsers.\n//\n\npre {\n overflow: auto;\n}\n\n//\n// Address odd `em`-unit font size rendering in all browsers.\n//\n\ncode,\nkbd,\npre,\nsamp {\n font-family: monospace, monospace;\n font-size: 1em;\n}\n\n// Forms\n// ==========================================================================\n\n//\n// Known limitation: by default, Chrome and Safari on OS X allow very limited\n// styling of `select`, unless a `border` property is set.\n//\n\n//\n// 1. Correct color not being inherited.\n// Known issue: affects color of disabled elements.\n// 2. Correct font properties not being inherited.\n// 3. Address margins set differently in Firefox 4+, Safari, and Chrome.\n//\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n color: inherit; // 1\n font: inherit; // 2\n margin: 0; // 3\n}\n\n//\n// Address `overflow` set to `hidden` in IE 8/9/10/11.\n//\n\nbutton {\n overflow: visible;\n}\n\n//\n// Address inconsistent `text-transform` inheritance for `button` and `select`.\n// All other form control elements do not inherit `text-transform` values.\n// Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.\n// Correct `select` style inheritance in Firefox.\n//\n\nbutton,\nselect {\n text-transform: none;\n}\n\n//\n// 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`\n// and `video` controls.\n// 2. Correct inability to style clickable `input` types in iOS.\n// 3. Improve usability and consistency of cursor style between image-type\n// `input` and others.\n//\n\nbutton,\nhtml input[type=\"button\"], // 1\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n -webkit-appearance: button; // 2\n cursor: pointer; // 3\n}\n\n//\n// Re-set default cursor for disabled elements.\n//\n\nbutton[disabled],\nhtml input[disabled] {\n cursor: default;\n}\n\n//\n// Remove inner padding and border in Firefox 4+.\n//\n\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n border: 0;\n padding: 0;\n}\n\n//\n// Address Firefox 4+ setting `line-height` on `input` using `!important` in\n// the UA stylesheet.\n//\n\ninput {\n line-height: normal;\n}\n\n//\n// It's recommended that you don't attempt to style these elements.\n// Firefox's implementation doesn't respect box-sizing, padding, or width.\n//\n// 1. Address box sizing set to `content-box` in IE 8/9/10.\n// 2. Remove excess padding in IE 8/9/10.\n//\n\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n box-sizing: border-box; // 1\n padding: 0; // 2\n}\n\n//\n// Fix the cursor style for Chrome's increment/decrement buttons. For certain\n// `font-size` values of the `input`, it causes the cursor style of the\n// decrement button to change from `default` to `text`.\n//\n\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n//\n// 1. Address `appearance` set to `searchfield` in Safari and Chrome.\n// 2. Address `box-sizing` set to `border-box` in Safari and Chrome.\n//\n\ninput[type=\"search\"] {\n -webkit-appearance: textfield; // 1\n box-sizing: content-box; //2\n}\n\n//\n// Remove inner padding and search cancel button in Safari and Chrome on OS X.\n// Safari (but not Chrome) clips the cancel button when the search input has\n// padding (and `textfield` appearance).\n//\n\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n//\n// Define consistent border, margin, and padding.\n//\n\nfieldset {\n border: 1px solid #c0c0c0;\n margin: 0 2px;\n padding: 0.35em 0.625em 0.75em;\n}\n\n//\n// 1. Correct `color` not being inherited in IE 8/9/10/11.\n// 2. Remove padding so people aren't caught out if they zero out fieldsets.\n//\n\nlegend {\n border: 0; // 1\n padding: 0; // 2\n}\n\n//\n// Remove default vertical scrollbar in IE 8/9/10/11.\n//\n\ntextarea {\n overflow: auto;\n}\n\n//\n// Don't inherit the `font-weight` (applied by a rule above).\n// NOTE: the default cannot safely be changed in Chrome and Safari on OS X.\n//\n\noptgroup {\n font-weight: bold;\n}\n\n// Tables\n// ==========================================================================\n\n//\n// Remove most spacing between table cells.\n//\n\ntable {\n border-collapse: collapse;\n border-spacing: 0;\n}\n\ntd,\nth {\n padding: 0;\n}\n","/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */\n\n// ==========================================================================\n// Print styles.\n// Inlined to avoid the additional HTTP request: h5bp.com/r\n// ==========================================================================\n\n@media print {\n *,\n *:before,\n *:after {\n background: transparent !important;\n color: #000 !important; // Black prints faster: h5bp.com/s\n box-shadow: none !important;\n text-shadow: none !important;\n }\n\n a,\n a:visited {\n text-decoration: underline;\n }\n\n a[href]:after {\n content: \" (\" attr(href) \")\";\n }\n\n abbr[title]:after {\n content: \" (\" attr(title) \")\";\n }\n\n // Don't show links that are fragment identifiers,\n // or use the `javascript:` pseudo protocol\n a[href^=\"#\"]:after,\n a[href^=\"javascript:\"]:after {\n content: \"\";\n }\n\n pre,\n blockquote {\n border: 1px solid #999;\n page-break-inside: avoid;\n }\n\n thead {\n display: table-header-group; // h5bp.com/t\n }\n\n tr,\n img {\n page-break-inside: avoid;\n }\n\n img {\n max-width: 100% !important;\n }\n\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n\n h2,\n h3 {\n page-break-after: avoid;\n }\n\n // Bootstrap specific changes start\n\n // Bootstrap components\n .navbar {\n display: none;\n }\n .btn,\n .dropup > .btn {\n > .caret {\n border-top-color: #000 !important;\n }\n }\n .label {\n border: 1px solid #000;\n }\n\n .table {\n border-collapse: collapse !important;\n\n td,\n th {\n background-color: #fff !important;\n }\n }\n .table-bordered {\n th,\n td {\n border: 1px solid #ddd !important;\n }\n }\n\n // Bootstrap specific changes end\n}\n","//\n// Glyphicons for Bootstrap\n//\n// Since icons are fonts, they can be placed anywhere text is placed and are\n// thus automatically sized to match the surrounding child. To use, create an\n// inline element with the appropriate classes, like so:\n//\n// Star\n\n// Import the fonts\n@font-face {\n font-family: 'Glyphicons Halflings';\n src: url('@{icon-font-path}@{icon-font-name}.eot');\n src: url('@{icon-font-path}@{icon-font-name}.eot?#iefix') format('embedded-opentype'),\n url('@{icon-font-path}@{icon-font-name}.woff2') format('woff2'),\n url('@{icon-font-path}@{icon-font-name}.woff') format('woff'),\n url('@{icon-font-path}@{icon-font-name}.ttf') format('truetype'),\n url('@{icon-font-path}@{icon-font-name}.svg#@{icon-font-svg-id}') format('svg');\n}\n\n// Catchall baseclass\n.glyphicon {\n position: relative;\n top: 1px;\n display: inline-block;\n font-family: 'Glyphicons Halflings';\n font-style: normal;\n font-weight: normal;\n line-height: 1;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\n// Individual icons\n.glyphicon-asterisk { &:before { content: \"\\002a\"; } }\n.glyphicon-plus { &:before { content: \"\\002b\"; } }\n.glyphicon-euro,\n.glyphicon-eur { &:before { content: \"\\20ac\"; } }\n.glyphicon-minus { &:before { content: \"\\2212\"; } }\n.glyphicon-cloud { &:before { content: \"\\2601\"; } }\n.glyphicon-envelope { &:before { content: \"\\2709\"; } }\n.glyphicon-pencil { &:before { content: \"\\270f\"; } }\n.glyphicon-glass { &:before { content: \"\\e001\"; } }\n.glyphicon-music { &:before { content: \"\\e002\"; } }\n.glyphicon-search { &:before { content: \"\\e003\"; } }\n.glyphicon-heart { &:before { content: \"\\e005\"; } }\n.glyphicon-star { &:before { content: \"\\e006\"; } }\n.glyphicon-star-empty { &:before { content: \"\\e007\"; } }\n.glyphicon-user { &:before { content: \"\\e008\"; } }\n.glyphicon-film { &:before { content: \"\\e009\"; } }\n.glyphicon-th-large { &:before { content: \"\\e010\"; } }\n.glyphicon-th { &:before { content: \"\\e011\"; } }\n.glyphicon-th-list { &:before { content: \"\\e012\"; } }\n.glyphicon-ok { &:before { content: \"\\e013\"; } }\n.glyphicon-remove { &:before { content: \"\\e014\"; } }\n.glyphicon-zoom-in { &:before { content: \"\\e015\"; } }\n.glyphicon-zoom-out { &:before { content: \"\\e016\"; } }\n.glyphicon-off { &:before { content: \"\\e017\"; } }\n.glyphicon-signal { &:before { content: \"\\e018\"; } }\n.glyphicon-cog { &:before { content: \"\\e019\"; } }\n.glyphicon-trash { &:before { content: \"\\e020\"; } }\n.glyphicon-home { &:before { content: \"\\e021\"; } }\n.glyphicon-file { &:before { content: \"\\e022\"; } }\n.glyphicon-time { &:before { content: \"\\e023\"; } }\n.glyphicon-road { &:before { content: \"\\e024\"; } }\n.glyphicon-download-alt { &:before { content: \"\\e025\"; } }\n.glyphicon-download { &:before { content: \"\\e026\"; } }\n.glyphicon-upload { &:before { content: \"\\e027\"; } }\n.glyphicon-inbox { &:before { content: \"\\e028\"; } }\n.glyphicon-play-circle { &:before { content: \"\\e029\"; } }\n.glyphicon-repeat { &:before { content: \"\\e030\"; } }\n.glyphicon-refresh { &:before { content: \"\\e031\"; } }\n.glyphicon-list-alt { &:before { content: \"\\e032\"; } }\n.glyphicon-lock { &:before { content: \"\\e033\"; } }\n.glyphicon-flag { &:before { content: \"\\e034\"; } }\n.glyphicon-headphones { &:before { content: \"\\e035\"; } }\n.glyphicon-volume-off { &:before { content: \"\\e036\"; } }\n.glyphicon-volume-down { &:before { content: \"\\e037\"; } }\n.glyphicon-volume-up { &:before { content: \"\\e038\"; } }\n.glyphicon-qrcode { &:before { content: \"\\e039\"; } }\n.glyphicon-barcode { &:before { content: \"\\e040\"; } }\n.glyphicon-tag { &:before { content: \"\\e041\"; } }\n.glyphicon-tags { &:before { content: \"\\e042\"; } }\n.glyphicon-book { &:before { content: \"\\e043\"; } }\n.glyphicon-bookmark { &:before { content: \"\\e044\"; } }\n.glyphicon-print { &:before { content: \"\\e045\"; } }\n.glyphicon-camera { &:before { content: \"\\e046\"; } }\n.glyphicon-font { &:before { content: \"\\e047\"; } }\n.glyphicon-bold { &:before { content: \"\\e048\"; } }\n.glyphicon-italic { &:before { content: \"\\e049\"; } }\n.glyphicon-text-height { &:before { content: \"\\e050\"; } }\n.glyphicon-text-width { &:before { content: \"\\e051\"; } }\n.glyphicon-align-left { &:before { content: \"\\e052\"; } }\n.glyphicon-align-center { &:before { content: \"\\e053\"; } }\n.glyphicon-align-right { &:before { content: \"\\e054\"; } }\n.glyphicon-align-justify { &:before { content: \"\\e055\"; } }\n.glyphicon-list { &:before { content: \"\\e056\"; } }\n.glyphicon-indent-left { &:before { content: \"\\e057\"; } }\n.glyphicon-indent-right { &:before { content: \"\\e058\"; } }\n.glyphicon-facetime-video { &:before { content: \"\\e059\"; } }\n.glyphicon-picture { &:before { content: \"\\e060\"; } }\n.glyphicon-map-marker { &:before { content: \"\\e062\"; } }\n.glyphicon-adjust { &:before { content: \"\\e063\"; } }\n.glyphicon-tint { &:before { content: \"\\e064\"; } }\n.glyphicon-edit { &:before { content: \"\\e065\"; } }\n.glyphicon-share { &:before { content: \"\\e066\"; } }\n.glyphicon-check { &:before { content: \"\\e067\"; } }\n.glyphicon-move { &:before { content: \"\\e068\"; } }\n.glyphicon-step-backward { &:before { content: \"\\e069\"; } }\n.glyphicon-fast-backward { &:before { content: \"\\e070\"; } }\n.glyphicon-backward { &:before { content: \"\\e071\"; } }\n.glyphicon-play { &:before { content: \"\\e072\"; } }\n.glyphicon-pause { &:before { content: \"\\e073\"; } }\n.glyphicon-stop { &:before { content: \"\\e074\"; } }\n.glyphicon-forward { &:before { content: \"\\e075\"; } }\n.glyphicon-fast-forward { &:before { content: \"\\e076\"; } }\n.glyphicon-step-forward { &:before { content: \"\\e077\"; } }\n.glyphicon-eject { &:before { content: \"\\e078\"; } }\n.glyphicon-chevron-left { &:before { content: \"\\e079\"; } }\n.glyphicon-chevron-right { &:before { content: \"\\e080\"; } }\n.glyphicon-plus-sign { &:before { content: \"\\e081\"; } }\n.glyphicon-minus-sign { &:before { content: \"\\e082\"; } }\n.glyphicon-remove-sign { &:before { content: \"\\e083\"; } }\n.glyphicon-ok-sign { &:before { content: \"\\e084\"; } }\n.glyphicon-question-sign { &:before { content: \"\\e085\"; } }\n.glyphicon-info-sign { &:before { content: \"\\e086\"; } }\n.glyphicon-screenshot { &:before { content: \"\\e087\"; } }\n.glyphicon-remove-circle { &:before { content: \"\\e088\"; } }\n.glyphicon-ok-circle { &:before { content: \"\\e089\"; } }\n.glyphicon-ban-circle { &:before { content: \"\\e090\"; } }\n.glyphicon-arrow-left { &:before { content: \"\\e091\"; } }\n.glyphicon-arrow-right { &:before { content: \"\\e092\"; } }\n.glyphicon-arrow-up { &:before { content: \"\\e093\"; } }\n.glyphicon-arrow-down { &:before { content: \"\\e094\"; } }\n.glyphicon-share-alt { &:before { content: \"\\e095\"; } }\n.glyphicon-resize-full { &:before { content: \"\\e096\"; } }\n.glyphicon-resize-small { &:before { content: \"\\e097\"; } }\n.glyphicon-exclamation-sign { &:before { content: \"\\e101\"; } }\n.glyphicon-gift { &:before { content: \"\\e102\"; } }\n.glyphicon-leaf { &:before { content: \"\\e103\"; } }\n.glyphicon-fire { &:before { content: \"\\e104\"; } }\n.glyphicon-eye-open { &:before { content: \"\\e105\"; } }\n.glyphicon-eye-close { &:before { content: \"\\e106\"; } }\n.glyphicon-warning-sign { &:before { content: \"\\e107\"; } }\n.glyphicon-plane { &:before { content: \"\\e108\"; } }\n.glyphicon-calendar { &:before { content: \"\\e109\"; } }\n.glyphicon-random { &:before { content: \"\\e110\"; } }\n.glyphicon-comment { &:before { content: \"\\e111\"; } }\n.glyphicon-magnet { &:before { content: \"\\e112\"; } }\n.glyphicon-chevron-up { &:before { content: \"\\e113\"; } }\n.glyphicon-chevron-down { &:before { content: \"\\e114\"; } }\n.glyphicon-retweet { &:before { content: \"\\e115\"; } }\n.glyphicon-shopping-cart { &:before { content: \"\\e116\"; } }\n.glyphicon-folder-close { &:before { content: \"\\e117\"; } }\n.glyphicon-folder-open { &:before { content: \"\\e118\"; } }\n.glyphicon-resize-vertical { &:before { content: \"\\e119\"; } }\n.glyphicon-resize-horizontal { &:before { content: \"\\e120\"; } }\n.glyphicon-hdd { &:before { content: \"\\e121\"; } }\n.glyphicon-bullhorn { &:before { content: \"\\e122\"; } }\n.glyphicon-bell { &:before { content: \"\\e123\"; } }\n.glyphicon-certificate { &:before { content: \"\\e124\"; } }\n.glyphicon-thumbs-up { &:before { content: \"\\e125\"; } }\n.glyphicon-thumbs-down { &:before { content: \"\\e126\"; } }\n.glyphicon-hand-right { &:before { content: \"\\e127\"; } }\n.glyphicon-hand-left { &:before { content: \"\\e128\"; } }\n.glyphicon-hand-up { &:before { content: \"\\e129\"; } }\n.glyphicon-hand-down { &:before { content: \"\\e130\"; } }\n.glyphicon-circle-arrow-right { &:before { content: \"\\e131\"; } }\n.glyphicon-circle-arrow-left { &:before { content: \"\\e132\"; } }\n.glyphicon-circle-arrow-up { &:before { content: \"\\e133\"; } }\n.glyphicon-circle-arrow-down { &:before { content: \"\\e134\"; } }\n.glyphicon-globe { &:before { content: \"\\e135\"; } }\n.glyphicon-wrench { &:before { content: \"\\e136\"; } }\n.glyphicon-tasks { &:before { content: \"\\e137\"; } }\n.glyphicon-filter { &:before { content: \"\\e138\"; } }\n.glyphicon-briefcase { &:before { content: \"\\e139\"; } }\n.glyphicon-fullscreen { &:before { content: \"\\e140\"; } }\n.glyphicon-dashboard { &:before { content: \"\\e141\"; } }\n.glyphicon-paperclip { &:before { content: \"\\e142\"; } }\n.glyphicon-heart-empty { &:before { content: \"\\e143\"; } }\n.glyphicon-link { &:before { content: \"\\e144\"; } }\n.glyphicon-phone { &:before { content: \"\\e145\"; } }\n.glyphicon-pushpin { &:before { content: \"\\e146\"; } }\n.glyphicon-usd { &:before { content: \"\\e148\"; } }\n.glyphicon-gbp { &:before { content: \"\\e149\"; } }\n.glyphicon-sort { &:before { content: \"\\e150\"; } }\n.glyphicon-sort-by-alphabet { &:before { content: \"\\e151\"; } }\n.glyphicon-sort-by-alphabet-alt { &:before { content: \"\\e152\"; } }\n.glyphicon-sort-by-order { &:before { content: \"\\e153\"; } }\n.glyphicon-sort-by-order-alt { &:before { content: \"\\e154\"; } }\n.glyphicon-sort-by-attributes { &:before { content: \"\\e155\"; } }\n.glyphicon-sort-by-attributes-alt { &:before { content: \"\\e156\"; } }\n.glyphicon-unchecked { &:before { content: \"\\e157\"; } }\n.glyphicon-expand { &:before { content: \"\\e158\"; } }\n.glyphicon-collapse-down { &:before { content: \"\\e159\"; } }\n.glyphicon-collapse-up { &:before { content: \"\\e160\"; } }\n.glyphicon-log-in { &:before { content: \"\\e161\"; } }\n.glyphicon-flash { &:before { content: \"\\e162\"; } }\n.glyphicon-log-out { &:before { content: \"\\e163\"; } }\n.glyphicon-new-window { &:before { content: \"\\e164\"; } }\n.glyphicon-record { &:before { content: \"\\e165\"; } }\n.glyphicon-save { &:before { content: \"\\e166\"; } }\n.glyphicon-open { &:before { content: \"\\e167\"; } }\n.glyphicon-saved { &:before { content: \"\\e168\"; } }\n.glyphicon-import { &:before { content: \"\\e169\"; } }\n.glyphicon-export { &:before { content: \"\\e170\"; } }\n.glyphicon-send { &:before { content: \"\\e171\"; } }\n.glyphicon-floppy-disk { &:before { content: \"\\e172\"; } }\n.glyphicon-floppy-saved { &:before { content: \"\\e173\"; } }\n.glyphicon-floppy-remove { &:before { content: \"\\e174\"; } }\n.glyphicon-floppy-save { &:before { content: \"\\e175\"; } }\n.glyphicon-floppy-open { &:before { content: \"\\e176\"; } }\n.glyphicon-credit-card { &:before { content: \"\\e177\"; } }\n.glyphicon-transfer { &:before { content: \"\\e178\"; } }\n.glyphicon-cutlery { &:before { content: \"\\e179\"; } }\n.glyphicon-header { &:before { content: \"\\e180\"; } }\n.glyphicon-compressed { &:before { content: \"\\e181\"; } }\n.glyphicon-earphone { &:before { content: \"\\e182\"; } }\n.glyphicon-phone-alt { &:before { content: \"\\e183\"; } }\n.glyphicon-tower { &:before { content: \"\\e184\"; } }\n.glyphicon-stats { &:before { content: \"\\e185\"; } }\n.glyphicon-sd-video { &:before { content: \"\\e186\"; } }\n.glyphicon-hd-video { &:before { content: \"\\e187\"; } }\n.glyphicon-subtitles { &:before { content: \"\\e188\"; } }\n.glyphicon-sound-stereo { &:before { content: \"\\e189\"; } }\n.glyphicon-sound-dolby { &:before { content: \"\\e190\"; } }\n.glyphicon-sound-5-1 { &:before { content: \"\\e191\"; } }\n.glyphicon-sound-6-1 { &:before { content: \"\\e192\"; } }\n.glyphicon-sound-7-1 { &:before { content: \"\\e193\"; } }\n.glyphicon-copyright-mark { &:before { content: \"\\e194\"; } }\n.glyphicon-registration-mark { &:before { content: \"\\e195\"; } }\n.glyphicon-cloud-download { &:before { content: \"\\e197\"; } }\n.glyphicon-cloud-upload { &:before { content: \"\\e198\"; } }\n.glyphicon-tree-conifer { &:before { content: \"\\e199\"; } }\n.glyphicon-tree-deciduous { &:before { content: \"\\e200\"; } }\n.glyphicon-cd { &:before { content: \"\\e201\"; } }\n.glyphicon-save-file { &:before { content: \"\\e202\"; } }\n.glyphicon-open-file { &:before { content: \"\\e203\"; } }\n.glyphicon-level-up { &:before { content: \"\\e204\"; } }\n.glyphicon-copy { &:before { content: \"\\e205\"; } }\n.glyphicon-paste { &:before { content: \"\\e206\"; } }\n// The following 2 Glyphicons are omitted for the time being because\n// they currently use Unicode codepoints that are outside the\n// Basic Multilingual Plane (BMP). Older buggy versions of WebKit can't handle\n// non-BMP codepoints in CSS string escapes, and thus can't display these two icons.\n// Notably, the bug affects some older versions of the Android Browser.\n// More info: https://github.com/twbs/bootstrap/issues/10106\n// .glyphicon-door { &:before { content: \"\\1f6aa\"; } }\n// .glyphicon-key { &:before { content: \"\\1f511\"; } }\n.glyphicon-alert { &:before { content: \"\\e209\"; } }\n.glyphicon-equalizer { &:before { content: \"\\e210\"; } }\n.glyphicon-king { &:before { content: \"\\e211\"; } }\n.glyphicon-queen { &:before { content: \"\\e212\"; } }\n.glyphicon-pawn { &:before { content: \"\\e213\"; } }\n.glyphicon-bishop { &:before { content: \"\\e214\"; } }\n.glyphicon-knight { &:before { content: \"\\e215\"; } }\n.glyphicon-baby-formula { &:before { content: \"\\e216\"; } }\n.glyphicon-tent { &:before { content: \"\\26fa\"; } }\n.glyphicon-blackboard { &:before { content: \"\\e218\"; } }\n.glyphicon-bed { &:before { content: \"\\e219\"; } }\n.glyphicon-apple { &:before { content: \"\\f8ff\"; } }\n.glyphicon-erase { &:before { content: \"\\e221\"; } }\n.glyphicon-hourglass { &:before { content: \"\\231b\"; } }\n.glyphicon-lamp { &:before { content: \"\\e223\"; } }\n.glyphicon-duplicate { &:before { content: \"\\e224\"; } }\n.glyphicon-piggy-bank { &:before { content: \"\\e225\"; } }\n.glyphicon-scissors { &:before { content: \"\\e226\"; } }\n.glyphicon-bitcoin { &:before { content: \"\\e227\"; } }\n.glyphicon-btc { &:before { content: \"\\e227\"; } }\n.glyphicon-xbt { &:before { content: \"\\e227\"; } }\n.glyphicon-yen { &:before { content: \"\\00a5\"; } }\n.glyphicon-jpy { &:before { content: \"\\00a5\"; } }\n.glyphicon-ruble { &:before { content: \"\\20bd\"; } }\n.glyphicon-rub { &:before { content: \"\\20bd\"; } }\n.glyphicon-scale { &:before { content: \"\\e230\"; } }\n.glyphicon-ice-lolly { &:before { content: \"\\e231\"; } }\n.glyphicon-ice-lolly-tasted { &:before { content: \"\\e232\"; } }\n.glyphicon-education { &:before { content: \"\\e233\"; } }\n.glyphicon-option-horizontal { &:before { content: \"\\e234\"; } }\n.glyphicon-option-vertical { &:before { content: \"\\e235\"; } }\n.glyphicon-menu-hamburger { &:before { content: \"\\e236\"; } }\n.glyphicon-modal-window { &:before { content: \"\\e237\"; } }\n.glyphicon-oil { &:before { content: \"\\e238\"; } }\n.glyphicon-grain { &:before { content: \"\\e239\"; } }\n.glyphicon-sunglasses { &:before { content: \"\\e240\"; } }\n.glyphicon-text-size { &:before { content: \"\\e241\"; } }\n.glyphicon-text-color { &:before { content: \"\\e242\"; } }\n.glyphicon-text-background { &:before { content: \"\\e243\"; } }\n.glyphicon-object-align-top { &:before { content: \"\\e244\"; } }\n.glyphicon-object-align-bottom { &:before { content: \"\\e245\"; } }\n.glyphicon-object-align-horizontal{ &:before { content: \"\\e246\"; } }\n.glyphicon-object-align-left { &:before { content: \"\\e247\"; } }\n.glyphicon-object-align-vertical { &:before { content: \"\\e248\"; } }\n.glyphicon-object-align-right { &:before { content: \"\\e249\"; } }\n.glyphicon-triangle-right { &:before { content: \"\\e250\"; } }\n.glyphicon-triangle-left { &:before { content: \"\\e251\"; } }\n.glyphicon-triangle-bottom { &:before { content: \"\\e252\"; } }\n.glyphicon-triangle-top { &:before { content: \"\\e253\"; } }\n.glyphicon-console { &:before { content: \"\\e254\"; } }\n.glyphicon-superscript { &:before { content: \"\\e255\"; } }\n.glyphicon-subscript { &:before { content: \"\\e256\"; } }\n.glyphicon-menu-left { &:before { content: \"\\e257\"; } }\n.glyphicon-menu-right { &:before { content: \"\\e258\"; } }\n.glyphicon-menu-down { &:before { content: \"\\e259\"; } }\n.glyphicon-menu-up { &:before { content: \"\\e260\"; } }\n","//\n// Scaffolding\n// --------------------------------------------------\n\n\n// Reset the box-sizing\n//\n// Heads up! This reset may cause conflicts with some third-party widgets.\n// For recommendations on resolving such conflicts, see\n// http://getbootstrap.com/getting-started/#third-box-sizing\n* {\n .box-sizing(border-box);\n}\n*:before,\n*:after {\n .box-sizing(border-box);\n}\n\n\n// Body reset\n\nhtml {\n font-size: 10px;\n -webkit-tap-highlight-color: rgba(0,0,0,0);\n}\n\nbody {\n font-family: @font-family-base;\n font-size: @font-size-base;\n line-height: @line-height-base;\n color: @text-color;\n background-color: @body-bg;\n}\n\n// Reset fonts for relevant elements\ninput,\nbutton,\nselect,\ntextarea {\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\n\n// Links\n\na {\n color: @link-color;\n text-decoration: none;\n\n &:hover,\n &:focus {\n color: @link-hover-color;\n text-decoration: @link-hover-decoration;\n }\n\n &:focus {\n .tab-focus();\n }\n}\n\n\n// Figures\n//\n// We reset this here because previously Normalize had no `figure` margins. This\n// ensures we don't break anyone's use of the element.\n\nfigure {\n margin: 0;\n}\n\n\n// Images\n\nimg {\n vertical-align: middle;\n}\n\n// Responsive images (ensure images don't scale beyond their parents)\n.img-responsive {\n .img-responsive();\n}\n\n// Rounded corners\n.img-rounded {\n border-radius: @border-radius-large;\n}\n\n// Image thumbnails\n//\n// Heads up! This is mixin-ed into thumbnails.less for `.thumbnail`.\n.img-thumbnail {\n padding: @thumbnail-padding;\n line-height: @line-height-base;\n background-color: @thumbnail-bg;\n border: 1px solid @thumbnail-border;\n border-radius: @thumbnail-border-radius;\n .transition(all .2s ease-in-out);\n\n // Keep them at most 100% wide\n .img-responsive(inline-block);\n}\n\n// Perfect circle\n.img-circle {\n border-radius: 50%; // set radius in percents\n}\n\n\n// Horizontal rules\n\nhr {\n margin-top: @line-height-computed;\n margin-bottom: @line-height-computed;\n border: 0;\n border-top: 1px solid @hr-border;\n}\n\n\n// Only display content to screen readers\n//\n// See: http://a11yproject.com/posts/how-to-hide-content/\n\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n clip: rect(0,0,0,0);\n border: 0;\n}\n\n// Use in conjunction with .sr-only to only display content when it's focused.\n// Useful for \"Skip to main content\" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1\n// Credit: HTML5 Boilerplate\n\n.sr-only-focusable {\n &:active,\n &:focus {\n position: static;\n width: auto;\n height: auto;\n margin: 0;\n overflow: visible;\n clip: auto;\n }\n}\n\n\n// iOS \"clickable elements\" fix for role=\"button\"\n//\n// Fixes \"clickability\" issue (and more generally, the firing of events such as focus as well)\n// for traditionally non-focusable elements with role=\"button\"\n// see https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile\n\n[role=\"button\"] {\n cursor: pointer;\n}\n","// Vendor Prefixes\n//\n// All vendor mixins are deprecated as of v3.2.0 due to the introduction of\n// Autoprefixer in our Gruntfile. They have been removed in v4.\n\n// - Animations\n// - Backface visibility\n// - Box shadow\n// - Box sizing\n// - Content columns\n// - Hyphens\n// - Placeholder text\n// - Transformations\n// - Transitions\n// - User Select\n\n\n// Animations\n.animation(@animation) {\n -webkit-animation: @animation;\n -o-animation: @animation;\n animation: @animation;\n}\n.animation-name(@name) {\n -webkit-animation-name: @name;\n animation-name: @name;\n}\n.animation-duration(@duration) {\n -webkit-animation-duration: @duration;\n animation-duration: @duration;\n}\n.animation-timing-function(@timing-function) {\n -webkit-animation-timing-function: @timing-function;\n animation-timing-function: @timing-function;\n}\n.animation-delay(@delay) {\n -webkit-animation-delay: @delay;\n animation-delay: @delay;\n}\n.animation-iteration-count(@iteration-count) {\n -webkit-animation-iteration-count: @iteration-count;\n animation-iteration-count: @iteration-count;\n}\n.animation-direction(@direction) {\n -webkit-animation-direction: @direction;\n animation-direction: @direction;\n}\n.animation-fill-mode(@fill-mode) {\n -webkit-animation-fill-mode: @fill-mode;\n animation-fill-mode: @fill-mode;\n}\n\n// Backface visibility\n// Prevent browsers from flickering when using CSS 3D transforms.\n// Default value is `visible`, but can be changed to `hidden`\n\n.backface-visibility(@visibility) {\n -webkit-backface-visibility: @visibility;\n -moz-backface-visibility: @visibility;\n backface-visibility: @visibility;\n}\n\n// Drop shadows\n//\n// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's\n// supported browsers that have box shadow capabilities now support it.\n\n.box-shadow(@shadow) {\n -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1\n box-shadow: @shadow;\n}\n\n// Box sizing\n.box-sizing(@boxmodel) {\n -webkit-box-sizing: @boxmodel;\n -moz-box-sizing: @boxmodel;\n box-sizing: @boxmodel;\n}\n\n// CSS3 Content Columns\n.content-columns(@column-count; @column-gap: @grid-gutter-width) {\n -webkit-column-count: @column-count;\n -moz-column-count: @column-count;\n column-count: @column-count;\n -webkit-column-gap: @column-gap;\n -moz-column-gap: @column-gap;\n column-gap: @column-gap;\n}\n\n// Optional hyphenation\n.hyphens(@mode: auto) {\n word-wrap: break-word;\n -webkit-hyphens: @mode;\n -moz-hyphens: @mode;\n -ms-hyphens: @mode; // IE10+\n -o-hyphens: @mode;\n hyphens: @mode;\n}\n\n// Placeholder text\n.placeholder(@color: @input-color-placeholder) {\n // Firefox\n &::-moz-placeholder {\n color: @color;\n opacity: 1; // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526\n }\n &:-ms-input-placeholder { color: @color; } // Internet Explorer 10+\n &::-webkit-input-placeholder { color: @color; } // Safari and Chrome\n}\n\n// Transformations\n.scale(@ratio) {\n -webkit-transform: scale(@ratio);\n -ms-transform: scale(@ratio); // IE9 only\n -o-transform: scale(@ratio);\n transform: scale(@ratio);\n}\n.scale(@ratioX; @ratioY) {\n -webkit-transform: scale(@ratioX, @ratioY);\n -ms-transform: scale(@ratioX, @ratioY); // IE9 only\n -o-transform: scale(@ratioX, @ratioY);\n transform: scale(@ratioX, @ratioY);\n}\n.scaleX(@ratio) {\n -webkit-transform: scaleX(@ratio);\n -ms-transform: scaleX(@ratio); // IE9 only\n -o-transform: scaleX(@ratio);\n transform: scaleX(@ratio);\n}\n.scaleY(@ratio) {\n -webkit-transform: scaleY(@ratio);\n -ms-transform: scaleY(@ratio); // IE9 only\n -o-transform: scaleY(@ratio);\n transform: scaleY(@ratio);\n}\n.skew(@x; @y) {\n -webkit-transform: skewX(@x) skewY(@y);\n -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+\n -o-transform: skewX(@x) skewY(@y);\n transform: skewX(@x) skewY(@y);\n}\n.translate(@x; @y) {\n -webkit-transform: translate(@x, @y);\n -ms-transform: translate(@x, @y); // IE9 only\n -o-transform: translate(@x, @y);\n transform: translate(@x, @y);\n}\n.translate3d(@x; @y; @z) {\n -webkit-transform: translate3d(@x, @y, @z);\n transform: translate3d(@x, @y, @z);\n}\n.rotate(@degrees) {\n -webkit-transform: rotate(@degrees);\n -ms-transform: rotate(@degrees); // IE9 only\n -o-transform: rotate(@degrees);\n transform: rotate(@degrees);\n}\n.rotateX(@degrees) {\n -webkit-transform: rotateX(@degrees);\n -ms-transform: rotateX(@degrees); // IE9 only\n -o-transform: rotateX(@degrees);\n transform: rotateX(@degrees);\n}\n.rotateY(@degrees) {\n -webkit-transform: rotateY(@degrees);\n -ms-transform: rotateY(@degrees); // IE9 only\n -o-transform: rotateY(@degrees);\n transform: rotateY(@degrees);\n}\n.perspective(@perspective) {\n -webkit-perspective: @perspective;\n -moz-perspective: @perspective;\n perspective: @perspective;\n}\n.perspective-origin(@perspective) {\n -webkit-perspective-origin: @perspective;\n -moz-perspective-origin: @perspective;\n perspective-origin: @perspective;\n}\n.transform-origin(@origin) {\n -webkit-transform-origin: @origin;\n -moz-transform-origin: @origin;\n -ms-transform-origin: @origin; // IE9 only\n transform-origin: @origin;\n}\n\n\n// Transitions\n\n.transition(@transition) {\n -webkit-transition: @transition;\n -o-transition: @transition;\n transition: @transition;\n}\n.transition-property(@transition-property) {\n -webkit-transition-property: @transition-property;\n transition-property: @transition-property;\n}\n.transition-delay(@transition-delay) {\n -webkit-transition-delay: @transition-delay;\n transition-delay: @transition-delay;\n}\n.transition-duration(@transition-duration) {\n -webkit-transition-duration: @transition-duration;\n transition-duration: @transition-duration;\n}\n.transition-timing-function(@timing-function) {\n -webkit-transition-timing-function: @timing-function;\n transition-timing-function: @timing-function;\n}\n.transition-transform(@transition) {\n -webkit-transition: -webkit-transform @transition;\n -moz-transition: -moz-transform @transition;\n -o-transition: -o-transform @transition;\n transition: transform @transition;\n}\n\n\n// User select\n// For selecting text on the page\n\n.user-select(@select) {\n -webkit-user-select: @select;\n -moz-user-select: @select;\n -ms-user-select: @select; // IE10+\n user-select: @select;\n}\n","// WebKit-style focus\n\n.tab-focus() {\n // Default\n outline: thin dotted;\n // WebKit\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\n","// Image Mixins\n// - Responsive image\n// - Retina image\n\n\n// Responsive image\n//\n// Keep images from scaling beyond the width of their parents.\n.img-responsive(@display: block) {\n display: @display;\n max-width: 100%; // Part 1: Set a maximum relative to the parent\n height: auto; // Part 2: Scale the height according to the width, otherwise you get stretching\n}\n\n\n// Retina image\n//\n// Short retina mixin for setting background-image and -size. Note that the\n// spelling of `min--moz-device-pixel-ratio` is intentional.\n.img-retina(@file-1x; @file-2x; @width-1x; @height-1x) {\n background-image: url(\"@{file-1x}\");\n\n @media\n only screen and (-webkit-min-device-pixel-ratio: 2),\n only screen and ( min--moz-device-pixel-ratio: 2),\n only screen and ( -o-min-device-pixel-ratio: 2/1),\n only screen and ( min-device-pixel-ratio: 2),\n only screen and ( min-resolution: 192dpi),\n only screen and ( min-resolution: 2dppx) {\n background-image: url(\"@{file-2x}\");\n background-size: @width-1x @height-1x;\n }\n}\n","//\n// Typography\n// --------------------------------------------------\n\n\n// Headings\n// -------------------------\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n font-family: @headings-font-family;\n font-weight: @headings-font-weight;\n line-height: @headings-line-height;\n color: @headings-color;\n\n small,\n .small {\n font-weight: normal;\n line-height: 1;\n color: @headings-small-color;\n }\n}\n\nh1, .h1,\nh2, .h2,\nh3, .h3 {\n margin-top: @line-height-computed;\n margin-bottom: (@line-height-computed / 2);\n\n small,\n .small {\n font-size: 65%;\n }\n}\nh4, .h4,\nh5, .h5,\nh6, .h6 {\n margin-top: (@line-height-computed / 2);\n margin-bottom: (@line-height-computed / 2);\n\n small,\n .small {\n font-size: 75%;\n }\n}\n\nh1, .h1 { font-size: @font-size-h1; }\nh2, .h2 { font-size: @font-size-h2; }\nh3, .h3 { font-size: @font-size-h3; }\nh4, .h4 { font-size: @font-size-h4; }\nh5, .h5 { font-size: @font-size-h5; }\nh6, .h6 { font-size: @font-size-h6; }\n\n\n// Body text\n// -------------------------\n\np {\n margin: 0 0 (@line-height-computed / 2);\n}\n\n.lead {\n margin-bottom: @line-height-computed;\n font-size: floor((@font-size-base * 1.15));\n font-weight: 300;\n line-height: 1.4;\n\n @media (min-width: @screen-sm-min) {\n font-size: (@font-size-base * 1.5);\n }\n}\n\n\n// Emphasis & misc\n// -------------------------\n\n// Ex: (12px small font / 14px base font) * 100% = about 85%\nsmall,\n.small {\n font-size: floor((100% * @font-size-small / @font-size-base));\n}\n\nmark,\n.mark {\n background-color: @state-warning-bg;\n padding: .2em;\n}\n\n// Alignment\n.text-left { text-align: left; }\n.text-right { text-align: right; }\n.text-center { text-align: center; }\n.text-justify { text-align: justify; }\n.text-nowrap { white-space: nowrap; }\n\n// Transformation\n.text-lowercase { text-transform: lowercase; }\n.text-uppercase { text-transform: uppercase; }\n.text-capitalize { text-transform: capitalize; }\n\n// Contextual colors\n.text-muted {\n color: @text-muted;\n}\n.text-primary {\n .text-emphasis-variant(@brand-primary);\n}\n.text-success {\n .text-emphasis-variant(@state-success-text);\n}\n.text-info {\n .text-emphasis-variant(@state-info-text);\n}\n.text-warning {\n .text-emphasis-variant(@state-warning-text);\n}\n.text-danger {\n .text-emphasis-variant(@state-danger-text);\n}\n\n// Contextual backgrounds\n// For now we'll leave these alongside the text classes until v4 when we can\n// safely shift things around (per SemVer rules).\n.bg-primary {\n // Given the contrast here, this is the only class to have its color inverted\n // automatically.\n color: #fff;\n .bg-variant(@brand-primary);\n}\n.bg-success {\n .bg-variant(@state-success-bg);\n}\n.bg-info {\n .bg-variant(@state-info-bg);\n}\n.bg-warning {\n .bg-variant(@state-warning-bg);\n}\n.bg-danger {\n .bg-variant(@state-danger-bg);\n}\n\n\n// Page header\n// -------------------------\n\n.page-header {\n padding-bottom: ((@line-height-computed / 2) - 1);\n margin: (@line-height-computed * 2) 0 @line-height-computed;\n border-bottom: 1px solid @page-header-border-color;\n}\n\n\n// Lists\n// -------------------------\n\n// Unordered and Ordered lists\nul,\nol {\n margin-top: 0;\n margin-bottom: (@line-height-computed / 2);\n ul,\n ol {\n margin-bottom: 0;\n }\n}\n\n// List options\n\n// Unstyled keeps list items block level, just removes default browser padding and list-style\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n\n// Inline turns list items into inline-block\n.list-inline {\n .list-unstyled();\n margin-left: -5px;\n\n > li {\n display: inline-block;\n padding-left: 5px;\n padding-right: 5px;\n }\n}\n\n// Description Lists\ndl {\n margin-top: 0; // Remove browser default\n margin-bottom: @line-height-computed;\n}\ndt,\ndd {\n line-height: @line-height-base;\n}\ndt {\n font-weight: bold;\n}\ndd {\n margin-left: 0; // Undo browser default\n}\n\n// Horizontal description lists\n//\n// Defaults to being stacked without any of the below styles applied, until the\n// grid breakpoint is reached (default of ~768px).\n\n.dl-horizontal {\n dd {\n &:extend(.clearfix all); // Clear the floated `dt` if an empty `dd` is present\n }\n\n @media (min-width: @dl-horizontal-breakpoint) {\n dt {\n float: left;\n width: (@dl-horizontal-offset - 20);\n clear: left;\n text-align: right;\n .text-overflow();\n }\n dd {\n margin-left: @dl-horizontal-offset;\n }\n }\n}\n\n\n// Misc\n// -------------------------\n\n// Abbreviations and acronyms\nabbr[title],\n// Add data-* attribute to help out our tooltip plugin, per https://github.com/twbs/bootstrap/issues/5257\nabbr[data-original-title] {\n cursor: help;\n border-bottom: 1px dotted @abbr-border-color;\n}\n.initialism {\n font-size: 90%;\n .text-uppercase();\n}\n\n// Blockquotes\nblockquote {\n padding: (@line-height-computed / 2) @line-height-computed;\n margin: 0 0 @line-height-computed;\n font-size: @blockquote-font-size;\n border-left: 5px solid @blockquote-border-color;\n\n p,\n ul,\n ol {\n &:last-child {\n margin-bottom: 0;\n }\n }\n\n // Note: Deprecated small and .small as of v3.1.0\n // Context: https://github.com/twbs/bootstrap/issues/11660\n footer,\n small,\n .small {\n display: block;\n font-size: 80%; // back to default font-size\n line-height: @line-height-base;\n color: @blockquote-small-color;\n\n &:before {\n content: '\\2014 \\00A0'; // em dash, nbsp\n }\n }\n}\n\n// Opposite alignment of blockquote\n//\n// Heads up: `blockquote.pull-right` has been deprecated as of v3.1.0.\n.blockquote-reverse,\nblockquote.pull-right {\n padding-right: 15px;\n padding-left: 0;\n border-right: 5px solid @blockquote-border-color;\n border-left: 0;\n text-align: right;\n\n // Account for citation\n footer,\n small,\n .small {\n &:before { content: ''; }\n &:after {\n content: '\\00A0 \\2014'; // nbsp, em dash\n }\n }\n}\n\n// Addresses\naddress {\n margin-bottom: @line-height-computed;\n font-style: normal;\n line-height: @line-height-base;\n}\n","// Typography\n\n.text-emphasis-variant(@color) {\n color: @color;\n a&:hover,\n a&:focus {\n color: darken(@color, 10%);\n }\n}\n","// Contextual backgrounds\n\n.bg-variant(@color) {\n background-color: @color;\n a&:hover,\n a&:focus {\n background-color: darken(@color, 10%);\n }\n}\n","// Text overflow\n// Requires inline-block or block for proper styling\n\n.text-overflow() {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n","//\n// Code (inline and block)\n// --------------------------------------------------\n\n\n// Inline and block code styles\ncode,\nkbd,\npre,\nsamp {\n font-family: @font-family-monospace;\n}\n\n// Inline code\ncode {\n padding: 2px 4px;\n font-size: 90%;\n color: @code-color;\n background-color: @code-bg;\n border-radius: @border-radius-base;\n}\n\n// User input typically entered via keyboard\nkbd {\n padding: 2px 4px;\n font-size: 90%;\n color: @kbd-color;\n background-color: @kbd-bg;\n border-radius: @border-radius-small;\n box-shadow: inset 0 -1px 0 rgba(0,0,0,.25);\n\n kbd {\n padding: 0;\n font-size: 100%;\n font-weight: bold;\n box-shadow: none;\n }\n}\n\n// Blocks of code\npre {\n display: block;\n padding: ((@line-height-computed - 1) / 2);\n margin: 0 0 (@line-height-computed / 2);\n font-size: (@font-size-base - 1); // 14px to 13px\n line-height: @line-height-base;\n word-break: break-all;\n word-wrap: break-word;\n color: @pre-color;\n background-color: @pre-bg;\n border: 1px solid @pre-border-color;\n border-radius: @border-radius-base;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n padding: 0;\n font-size: inherit;\n color: inherit;\n white-space: pre-wrap;\n background-color: transparent;\n border-radius: 0;\n }\n}\n\n// Enable scrollable blocks of code\n.pre-scrollable {\n max-height: @pre-scrollable-max-height;\n overflow-y: scroll;\n}\n","//\n// Grid system\n// --------------------------------------------------\n\n\n// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n.container {\n .container-fixed();\n\n @media (min-width: @screen-sm-min) {\n width: @container-sm;\n }\n @media (min-width: @screen-md-min) {\n width: @container-md;\n }\n @media (min-width: @screen-lg-min) {\n width: @container-lg;\n }\n}\n\n\n// Fluid container\n//\n// Utilizes the mixin meant for fixed width containers, but without any defined\n// width for fluid, full width layouts.\n\n.container-fluid {\n .container-fixed();\n}\n\n\n// Row\n//\n// Rows contain and clear the floats of your columns.\n\n.row {\n .make-row();\n}\n\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n.make-grid-columns();\n\n\n// Extra small grid\n//\n// Columns, offsets, pushes, and pulls for extra small devices like\n// smartphones.\n\n.make-grid(xs);\n\n\n// Small grid\n//\n// Columns, offsets, pushes, and pulls for the small device range, from phones\n// to tablets.\n\n@media (min-width: @screen-sm-min) {\n .make-grid(sm);\n}\n\n\n// Medium grid\n//\n// Columns, offsets, pushes, and pulls for the desktop device range.\n\n@media (min-width: @screen-md-min) {\n .make-grid(md);\n}\n\n\n// Large grid\n//\n// Columns, offsets, pushes, and pulls for the large desktop device range.\n\n@media (min-width: @screen-lg-min) {\n .make-grid(lg);\n}\n","// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n// Centered container element\n.container-fixed(@gutter: @grid-gutter-width) {\n margin-right: auto;\n margin-left: auto;\n padding-left: floor((@gutter / 2));\n padding-right: ceil((@gutter / 2));\n &:extend(.clearfix all);\n}\n\n// Creates a wrapper for a series of columns\n.make-row(@gutter: @grid-gutter-width) {\n margin-left: ceil((@gutter / -2));\n margin-right: floor((@gutter / -2));\n &:extend(.clearfix all);\n}\n\n// Generate the extra small columns\n.make-xs-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n float: left;\n width: percentage((@columns / @grid-columns));\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n}\n.make-xs-column-offset(@columns) {\n margin-left: percentage((@columns / @grid-columns));\n}\n.make-xs-column-push(@columns) {\n left: percentage((@columns / @grid-columns));\n}\n.make-xs-column-pull(@columns) {\n right: percentage((@columns / @grid-columns));\n}\n\n// Generate the small columns\n.make-sm-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-sm-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-offset(@columns) {\n @media (min-width: @screen-sm-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-push(@columns) {\n @media (min-width: @screen-sm-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-pull(@columns) {\n @media (min-width: @screen-sm-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n// Generate the medium columns\n.make-md-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-md-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-offset(@columns) {\n @media (min-width: @screen-md-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-push(@columns) {\n @media (min-width: @screen-md-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-pull(@columns) {\n @media (min-width: @screen-md-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n// Generate the large columns\n.make-lg-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-lg-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-offset(@columns) {\n @media (min-width: @screen-lg-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-push(@columns) {\n @media (min-width: @screen-lg-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-pull(@columns) {\n @media (min-width: @screen-lg-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n","// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `@grid-columns`.\n\n.make-grid-columns() {\n // Common styles for all sizes of grid columns, widths 1-12\n .col(@index) { // initial\n @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n .col((@index + 1), @item);\n }\n .col(@index, @list) when (@index =< @grid-columns) { // general; \"=<\" isn't a typo\n @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n .col((@index + 1), ~\"@{list}, @{item}\");\n }\n .col(@index, @list) when (@index > @grid-columns) { // terminal\n @{list} {\n position: relative;\n // Prevent columns from collapsing when empty\n min-height: 1px;\n // Inner gutter via padding\n padding-left: ceil((@grid-gutter-width / 2));\n padding-right: floor((@grid-gutter-width / 2));\n }\n }\n .col(1); // kickstart it\n}\n\n.float-grid-columns(@class) {\n .col(@index) { // initial\n @item: ~\".col-@{class}-@{index}\";\n .col((@index + 1), @item);\n }\n .col(@index, @list) when (@index =< @grid-columns) { // general\n @item: ~\".col-@{class}-@{index}\";\n .col((@index + 1), ~\"@{list}, @{item}\");\n }\n .col(@index, @list) when (@index > @grid-columns) { // terminal\n @{list} {\n float: left;\n }\n }\n .col(1); // kickstart it\n}\n\n.calc-grid-column(@index, @class, @type) when (@type = width) and (@index > 0) {\n .col-@{class}-@{index} {\n width: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = push) and (@index > 0) {\n .col-@{class}-push-@{index} {\n left: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = push) and (@index = 0) {\n .col-@{class}-push-0 {\n left: auto;\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = pull) and (@index > 0) {\n .col-@{class}-pull-@{index} {\n right: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = pull) and (@index = 0) {\n .col-@{class}-pull-0 {\n right: auto;\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = offset) {\n .col-@{class}-offset-@{index} {\n margin-left: percentage((@index / @grid-columns));\n }\n}\n\n// Basic looping in LESS\n.loop-grid-columns(@index, @class, @type) when (@index >= 0) {\n .calc-grid-column(@index, @class, @type);\n // next iteration\n .loop-grid-columns((@index - 1), @class, @type);\n}\n\n// Create grid for specific class\n.make-grid(@class) {\n .float-grid-columns(@class);\n .loop-grid-columns(@grid-columns, @class, width);\n .loop-grid-columns(@grid-columns, @class, pull);\n .loop-grid-columns(@grid-columns, @class, push);\n .loop-grid-columns(@grid-columns, @class, offset);\n}\n","//\n// Tables\n// --------------------------------------------------\n\n\ntable {\n background-color: @table-bg;\n}\ncaption {\n padding-top: @table-cell-padding;\n padding-bottom: @table-cell-padding;\n color: @text-muted;\n text-align: left;\n}\nth {\n text-align: left;\n}\n\n\n// Baseline styles\n\n.table {\n width: 100%;\n max-width: 100%;\n margin-bottom: @line-height-computed;\n // Cells\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n padding: @table-cell-padding;\n line-height: @line-height-base;\n vertical-align: top;\n border-top: 1px solid @table-border-color;\n }\n }\n }\n // Bottom align for column headings\n > thead > tr > th {\n vertical-align: bottom;\n border-bottom: 2px solid @table-border-color;\n }\n // Remove top border from thead by default\n > caption + thead,\n > colgroup + thead,\n > thead:first-child {\n > tr:first-child {\n > th,\n > td {\n border-top: 0;\n }\n }\n }\n // Account for multiple tbody instances\n > tbody + tbody {\n border-top: 2px solid @table-border-color;\n }\n\n // Nesting\n .table {\n background-color: @body-bg;\n }\n}\n\n\n// Condensed table w/ half padding\n\n.table-condensed {\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n padding: @table-condensed-cell-padding;\n }\n }\n }\n}\n\n\n// Bordered version\n//\n// Add borders all around the table and between all the columns.\n\n.table-bordered {\n border: 1px solid @table-border-color;\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n border: 1px solid @table-border-color;\n }\n }\n }\n > thead > tr {\n > th,\n > td {\n border-bottom-width: 2px;\n }\n }\n}\n\n\n// Zebra-striping\n//\n// Default zebra-stripe styles (alternating gray and transparent backgrounds)\n\n.table-striped {\n > tbody > tr:nth-of-type(odd) {\n background-color: @table-bg-accent;\n }\n}\n\n\n// Hover effect\n//\n// Placed here since it has to come after the potential zebra striping\n\n.table-hover {\n > tbody > tr:hover {\n background-color: @table-bg-hover;\n }\n}\n\n\n// Table cell sizing\n//\n// Reset default table behavior\n\ntable col[class*=\"col-\"] {\n position: static; // Prevent border hiding in Firefox and IE9-11 (see https://github.com/twbs/bootstrap/issues/11623)\n float: none;\n display: table-column;\n}\ntable {\n td,\n th {\n &[class*=\"col-\"] {\n position: static; // Prevent border hiding in Firefox and IE9-11 (see https://github.com/twbs/bootstrap/issues/11623)\n float: none;\n display: table-cell;\n }\n }\n}\n\n\n// Table backgrounds\n//\n// Exact selectors below required to override `.table-striped` and prevent\n// inheritance to nested tables.\n\n// Generate the contextual variants\n.table-row-variant(active; @table-bg-active);\n.table-row-variant(success; @state-success-bg);\n.table-row-variant(info; @state-info-bg);\n.table-row-variant(warning; @state-warning-bg);\n.table-row-variant(danger; @state-danger-bg);\n\n\n// Responsive tables\n//\n// Wrap your tables in `.table-responsive` and we'll make them mobile friendly\n// by enabling horizontal scrolling. Only applies <768px. Everything above that\n// will display normally.\n\n.table-responsive {\n overflow-x: auto;\n min-height: 0.01%; // Workaround for IE9 bug (see https://github.com/twbs/bootstrap/issues/14837)\n\n @media screen and (max-width: @screen-xs-max) {\n width: 100%;\n margin-bottom: (@line-height-computed * 0.75);\n overflow-y: hidden;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n border: 1px solid @table-border-color;\n\n // Tighten up spacing\n > .table {\n margin-bottom: 0;\n\n // Ensure the content doesn't wrap\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n white-space: nowrap;\n }\n }\n }\n }\n\n // Special overrides for the bordered tables\n > .table-bordered {\n border: 0;\n\n // Nuke the appropriate borders so that the parent can handle them\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th:first-child,\n > td:first-child {\n border-left: 0;\n }\n > th:last-child,\n > td:last-child {\n border-right: 0;\n }\n }\n }\n\n // Only nuke the last row's bottom-border in `tbody` and `tfoot` since\n // chances are there will be only one `tr` in a `thead` and that would\n // remove the border altogether.\n > tbody,\n > tfoot {\n > tr:last-child {\n > th,\n > td {\n border-bottom: 0;\n }\n }\n }\n\n }\n }\n}\n","// Tables\n\n.table-row-variant(@state; @background) {\n // Exact selectors below required to override `.table-striped` and prevent\n // inheritance to nested tables.\n .table > thead > tr,\n .table > tbody > tr,\n .table > tfoot > tr {\n > td.@{state},\n > th.@{state},\n &.@{state} > td,\n &.@{state} > th {\n background-color: @background;\n }\n }\n\n // Hover states for `.table-hover`\n // Note: this is not available for cells or rows within `thead` or `tfoot`.\n .table-hover > tbody > tr {\n > td.@{state}:hover,\n > th.@{state}:hover,\n &.@{state}:hover > td,\n &:hover > .@{state},\n &.@{state}:hover > th {\n background-color: darken(@background, 5%);\n }\n }\n}\n","//\n// Forms\n// --------------------------------------------------\n\n\n// Normalize non-controls\n//\n// Restyle and baseline non-control form elements.\n\nfieldset {\n padding: 0;\n margin: 0;\n border: 0;\n // Chrome and Firefox set a `min-width: min-content;` on fieldsets,\n // so we reset that to ensure it behaves more like a standard block element.\n // See https://github.com/twbs/bootstrap/issues/12359.\n min-width: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n padding: 0;\n margin-bottom: @line-height-computed;\n font-size: (@font-size-base * 1.5);\n line-height: inherit;\n color: @legend-color;\n border: 0;\n border-bottom: 1px solid @legend-border-color;\n}\n\nlabel {\n display: inline-block;\n max-width: 100%; // Force IE8 to wrap long content (see https://github.com/twbs/bootstrap/issues/13141)\n margin-bottom: 5px;\n font-weight: bold;\n}\n\n\n// Normalize form controls\n//\n// While most of our form styles require extra classes, some basic normalization\n// is required to ensure optimum display with or without those classes to better\n// address browser inconsistencies.\n\n// Override content-box in Normalize (* isn't specific enough)\ninput[type=\"search\"] {\n .box-sizing(border-box);\n}\n\n// Position radios and checkboxes better\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n margin: 4px 0 0;\n margin-top: 1px \\9; // IE8-9\n line-height: normal;\n}\n\ninput[type=\"file\"] {\n display: block;\n}\n\n// Make range inputs behave like textual form controls\ninput[type=\"range\"] {\n display: block;\n width: 100%;\n}\n\n// Make multiple select elements height not fixed\nselect[multiple],\nselect[size] {\n height: auto;\n}\n\n// Focus for file, radio, and checkbox\ninput[type=\"file\"]:focus,\ninput[type=\"radio\"]:focus,\ninput[type=\"checkbox\"]:focus {\n .tab-focus();\n}\n\n// Adjust output element\noutput {\n display: block;\n padding-top: (@padding-base-vertical + 1);\n font-size: @font-size-base;\n line-height: @line-height-base;\n color: @input-color;\n}\n\n\n// Common form controls\n//\n// Shared size and type resets for form controls. Apply `.form-control` to any\n// of the following form controls:\n//\n// select\n// textarea\n// input[type=\"text\"]\n// input[type=\"password\"]\n// input[type=\"datetime\"]\n// input[type=\"datetime-local\"]\n// input[type=\"date\"]\n// input[type=\"month\"]\n// input[type=\"time\"]\n// input[type=\"week\"]\n// input[type=\"number\"]\n// input[type=\"email\"]\n// input[type=\"url\"]\n// input[type=\"search\"]\n// input[type=\"tel\"]\n// input[type=\"color\"]\n\n.form-control {\n display: block;\n width: 100%;\n height: @input-height-base; // Make inputs at least the height of their button counterpart (base line-height + padding + border)\n padding: @padding-base-vertical @padding-base-horizontal;\n font-size: @font-size-base;\n line-height: @line-height-base;\n color: @input-color;\n background-color: @input-bg;\n background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214\n border: 1px solid @input-border;\n border-radius: @input-border-radius; // Note: This has no effect on s in CSS.\n .box-shadow(inset 0 1px 1px rgba(0,0,0,.075));\n .transition(~\"border-color ease-in-out .15s, box-shadow ease-in-out .15s\");\n\n // Customize the `:focus` state to imitate native WebKit styles.\n .form-control-focus();\n\n // Placeholder\n .placeholder();\n\n // Unstyle the caret on ``\n// element gets special love because it's special, and that's a fact!\n.input-size(@input-height; @padding-vertical; @padding-horizontal; @font-size; @line-height; @border-radius) {\n height: @input-height;\n padding: @padding-vertical @padding-horizontal;\n font-size: @font-size;\n line-height: @line-height;\n border-radius: @border-radius;\n\n select& {\n height: @input-height;\n line-height: @input-height;\n }\n\n textarea&,\n select[multiple]& {\n height: auto;\n }\n}\n","//\n// Buttons\n// --------------------------------------------------\n\n\n// Base styles\n// --------------------------------------------------\n\n.btn {\n display: inline-block;\n margin-bottom: 0; // For input.btn\n font-weight: @btn-font-weight;\n text-align: center;\n vertical-align: middle;\n touch-action: manipulation;\n cursor: pointer;\n background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214\n border: 1px solid transparent;\n white-space: nowrap;\n .button-size(@padding-base-vertical; @padding-base-horizontal; @font-size-base; @line-height-base; @btn-border-radius-base);\n .user-select(none);\n\n &,\n &:active,\n &.active {\n &:focus,\n &.focus {\n .tab-focus();\n }\n }\n\n &:hover,\n &:focus,\n &.focus {\n color: @btn-default-color;\n text-decoration: none;\n }\n\n &:active,\n &.active {\n outline: 0;\n background-image: none;\n .box-shadow(inset 0 3px 5px rgba(0,0,0,.125));\n }\n\n &.disabled,\n &[disabled],\n fieldset[disabled] & {\n cursor: @cursor-disabled;\n .opacity(.65);\n .box-shadow(none);\n }\n\n a& {\n &.disabled,\n fieldset[disabled] & {\n pointer-events: none; // Future-proof disabling of clicks on `` elements\n }\n }\n}\n\n\n// Alternate buttons\n// --------------------------------------------------\n\n.btn-default {\n .button-variant(@btn-default-color; @btn-default-bg; @btn-default-border);\n}\n.btn-primary {\n .button-variant(@btn-primary-color; @btn-primary-bg; @btn-primary-border);\n}\n// Success appears as green\n.btn-success {\n .button-variant(@btn-success-color; @btn-success-bg; @btn-success-border);\n}\n// Info appears as blue-green\n.btn-info {\n .button-variant(@btn-info-color; @btn-info-bg; @btn-info-border);\n}\n// Warning appears as orange\n.btn-warning {\n .button-variant(@btn-warning-color; @btn-warning-bg; @btn-warning-border);\n}\n// Danger and error appear as red\n.btn-danger {\n .button-variant(@btn-danger-color; @btn-danger-bg; @btn-danger-border);\n}\n\n\n// Link buttons\n// -------------------------\n\n// Make a button look and behave like a link\n.btn-link {\n color: @link-color;\n font-weight: normal;\n border-radius: 0;\n\n &,\n &:active,\n &.active,\n &[disabled],\n fieldset[disabled] & {\n background-color: transparent;\n .box-shadow(none);\n }\n &,\n &:hover,\n &:focus,\n &:active {\n border-color: transparent;\n }\n &:hover,\n &:focus {\n color: @link-hover-color;\n text-decoration: @link-hover-decoration;\n background-color: transparent;\n }\n &[disabled],\n fieldset[disabled] & {\n &:hover,\n &:focus {\n color: @btn-link-disabled-color;\n text-decoration: none;\n }\n }\n}\n\n\n// Button Sizes\n// --------------------------------------------------\n\n.btn-lg {\n // line-height: ensure even-numbered height of button next to large input\n .button-size(@padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @btn-border-radius-large);\n}\n.btn-sm {\n // line-height: ensure proper height of button next to small input\n .button-size(@padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @btn-border-radius-small);\n}\n.btn-xs {\n .button-size(@padding-xs-vertical; @padding-xs-horizontal; @font-size-small; @line-height-small; @btn-border-radius-small);\n}\n\n\n// Block button\n// --------------------------------------------------\n\n.btn-block {\n display: block;\n width: 100%;\n}\n\n// Vertically space out multiple block buttons\n.btn-block + .btn-block {\n margin-top: 5px;\n}\n\n// Specificity overrides\ninput[type=\"submit\"],\ninput[type=\"reset\"],\ninput[type=\"button\"] {\n &.btn-block {\n width: 100%;\n }\n}\n","// Button variants\n//\n// Easily pump out default styles, as well as :hover, :focus, :active,\n// and disabled options for all buttons\n\n.button-variant(@color; @background; @border) {\n color: @color;\n background-color: @background;\n border-color: @border;\n\n &:focus,\n &.focus {\n color: @color;\n background-color: darken(@background, 10%);\n border-color: darken(@border, 25%);\n }\n &:hover {\n color: @color;\n background-color: darken(@background, 10%);\n border-color: darken(@border, 12%);\n }\n &:active,\n &.active,\n .open > .dropdown-toggle& {\n color: @color;\n background-color: darken(@background, 10%);\n border-color: darken(@border, 12%);\n\n &:hover,\n &:focus,\n &.focus {\n color: @color;\n background-color: darken(@background, 17%);\n border-color: darken(@border, 25%);\n }\n }\n &:active,\n &.active,\n .open > .dropdown-toggle& {\n background-image: none;\n }\n &.disabled,\n &[disabled],\n fieldset[disabled] & {\n &:hover,\n &:focus,\n &.focus {\n background-color: @background;\n border-color: @border;\n }\n }\n\n .badge {\n color: @background;\n background-color: @color;\n }\n}\n\n// Button sizes\n.button-size(@padding-vertical; @padding-horizontal; @font-size; @line-height; @border-radius) {\n padding: @padding-vertical @padding-horizontal;\n font-size: @font-size;\n line-height: @line-height;\n border-radius: @border-radius;\n}\n","// Opacity\n\n.opacity(@opacity) {\n opacity: @opacity;\n // IE8 filter\n @opacity-ie: (@opacity * 100);\n filter: ~\"alpha(opacity=@{opacity-ie})\";\n}\n","//\n// Component animations\n// --------------------------------------------------\n\n// Heads up!\n//\n// We don't use the `.opacity()` mixin here since it causes a bug with text\n// fields in IE7-8. Source: https://github.com/twbs/bootstrap/pull/3552.\n\n.fade {\n opacity: 0;\n .transition(opacity .15s linear);\n &.in {\n opacity: 1;\n }\n}\n\n.collapse {\n display: none;\n\n &.in { display: block; }\n tr&.in { display: table-row; }\n tbody&.in { display: table-row-group; }\n}\n\n.collapsing {\n position: relative;\n height: 0;\n overflow: hidden;\n .transition-property(~\"height, visibility\");\n .transition-duration(.35s);\n .transition-timing-function(ease);\n}\n","//\n// Dropdown menus\n// --------------------------------------------------\n\n\n// Dropdown arrow/caret\n.caret {\n display: inline-block;\n width: 0;\n height: 0;\n margin-left: 2px;\n vertical-align: middle;\n border-top: @caret-width-base dashed;\n border-top: @caret-width-base solid ~\"\\9\"; // IE8\n border-right: @caret-width-base solid transparent;\n border-left: @caret-width-base solid transparent;\n}\n\n// The dropdown wrapper (div)\n.dropup,\n.dropdown {\n position: relative;\n}\n\n// Prevent the focus on the dropdown toggle when closing dropdowns\n.dropdown-toggle:focus {\n outline: 0;\n}\n\n// The dropdown menu (ul)\n.dropdown-menu {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: @zindex-dropdown;\n display: none; // none by default, but block on \"open\" of the menu\n float: left;\n min-width: 160px;\n padding: 5px 0;\n margin: 2px 0 0; // override default ul\n list-style: none;\n font-size: @font-size-base;\n text-align: left; // Ensures proper alignment if parent has it changed (e.g., modal footer)\n background-color: @dropdown-bg;\n border: 1px solid @dropdown-fallback-border; // IE8 fallback\n border: 1px solid @dropdown-border;\n border-radius: @border-radius-base;\n .box-shadow(0 6px 12px rgba(0,0,0,.175));\n background-clip: padding-box;\n\n // Aligns the dropdown menu to right\n //\n // Deprecated as of 3.1.0 in favor of `.dropdown-menu-[dir]`\n &.pull-right {\n right: 0;\n left: auto;\n }\n\n // Dividers (basically an hr) within the dropdown\n .divider {\n .nav-divider(@dropdown-divider-bg);\n }\n\n // Links within the dropdown menu\n > li > a {\n display: block;\n padding: 3px 20px;\n clear: both;\n font-weight: normal;\n line-height: @line-height-base;\n color: @dropdown-link-color;\n white-space: nowrap; // prevent links from randomly breaking onto new lines\n }\n}\n\n// Hover/Focus state\n.dropdown-menu > li > a {\n &:hover,\n &:focus {\n text-decoration: none;\n color: @dropdown-link-hover-color;\n background-color: @dropdown-link-hover-bg;\n }\n}\n\n// Active state\n.dropdown-menu > .active > a {\n &,\n &:hover,\n &:focus {\n color: @dropdown-link-active-color;\n text-decoration: none;\n outline: 0;\n background-color: @dropdown-link-active-bg;\n }\n}\n\n// Disabled state\n//\n// Gray out text and ensure the hover/focus state remains gray\n\n.dropdown-menu > .disabled > a {\n &,\n &:hover,\n &:focus {\n color: @dropdown-link-disabled-color;\n }\n\n // Nuke hover/focus effects\n &:hover,\n &:focus {\n text-decoration: none;\n background-color: transparent;\n background-image: none; // Remove CSS gradient\n .reset-filter();\n cursor: @cursor-disabled;\n }\n}\n\n// Open state for the dropdown\n.open {\n // Show the menu\n > .dropdown-menu {\n display: block;\n }\n\n // Remove the outline when :focus is triggered\n > a {\n outline: 0;\n }\n}\n\n// Menu positioning\n//\n// Add extra class to `.dropdown-menu` to flip the alignment of the dropdown\n// menu with the parent.\n.dropdown-menu-right {\n left: auto; // Reset the default from `.dropdown-menu`\n right: 0;\n}\n// With v3, we enabled auto-flipping if you have a dropdown within a right\n// aligned nav component. To enable the undoing of that, we provide an override\n// to restore the default dropdown menu alignment.\n//\n// This is only for left-aligning a dropdown menu within a `.navbar-right` or\n// `.pull-right` nav component.\n.dropdown-menu-left {\n left: 0;\n right: auto;\n}\n\n// Dropdown section headers\n.dropdown-header {\n display: block;\n padding: 3px 20px;\n font-size: @font-size-small;\n line-height: @line-height-base;\n color: @dropdown-header-color;\n white-space: nowrap; // as with > li > a\n}\n\n// Backdrop to catch body clicks on mobile, etc.\n.dropdown-backdrop {\n position: fixed;\n left: 0;\n right: 0;\n bottom: 0;\n top: 0;\n z-index: (@zindex-dropdown - 10);\n}\n\n// Right aligned dropdowns\n.pull-right > .dropdown-menu {\n right: 0;\n left: auto;\n}\n\n// Allow for dropdowns to go bottom up (aka, dropup-menu)\n//\n// Just add .dropup after the standard .dropdown class and you're set, bro.\n// TODO: abstract this so that the navbar fixed styles are not placed here?\n\n.dropup,\n.navbar-fixed-bottom .dropdown {\n // Reverse the caret\n .caret {\n border-top: 0;\n border-bottom: @caret-width-base dashed;\n border-bottom: @caret-width-base solid ~\"\\9\"; // IE8\n content: \"\";\n }\n // Different positioning for bottom up menu\n .dropdown-menu {\n top: auto;\n bottom: 100%;\n margin-bottom: 2px;\n }\n}\n\n\n// Component alignment\n//\n// Reiterate per navbar.less and the modified component alignment there.\n\n@media (min-width: @grid-float-breakpoint) {\n .navbar-right {\n .dropdown-menu {\n .dropdown-menu-right();\n }\n // Necessary for overrides of the default right aligned menu.\n // Will remove come v4 in all likelihood.\n .dropdown-menu-left {\n .dropdown-menu-left();\n }\n }\n}\n","// Horizontal dividers\n//\n// Dividers (basically an hr) within dropdowns and nav lists\n\n.nav-divider(@color: #e5e5e5) {\n height: 1px;\n margin: ((@line-height-computed / 2) - 1) 0;\n overflow: hidden;\n background-color: @color;\n}\n","// Reset filters for IE\n//\n// When you need to remove a gradient background, do not forget to use this to reset\n// the IE filter for IE9 and below.\n\n.reset-filter() {\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(enabled = false)\"));\n}\n","//\n// Button groups\n// --------------------------------------------------\n\n// Make the div behave like a button\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: inline-block;\n vertical-align: middle; // match .btn alignment given font-size hack above\n > .btn {\n position: relative;\n float: left;\n // Bring the \"active\" button to the front\n &:hover,\n &:focus,\n &:active,\n &.active {\n z-index: 2;\n }\n }\n}\n\n// Prevent double borders when buttons are next to each other\n.btn-group {\n .btn + .btn,\n .btn + .btn-group,\n .btn-group + .btn,\n .btn-group + .btn-group {\n margin-left: -1px;\n }\n}\n\n// Optional: Group multiple button groups together for a toolbar\n.btn-toolbar {\n margin-left: -5px; // Offset the first child's margin\n &:extend(.clearfix all);\n\n .btn,\n .btn-group,\n .input-group {\n float: left;\n }\n > .btn,\n > .btn-group,\n > .input-group {\n margin-left: 5px;\n }\n}\n\n.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {\n border-radius: 0;\n}\n\n// Set corners individual because sometimes a single button can be in a .btn-group and we need :first-child and :last-child to both match\n.btn-group > .btn:first-child {\n margin-left: 0;\n &:not(:last-child):not(.dropdown-toggle) {\n .border-right-radius(0);\n }\n}\n// Need .dropdown-toggle since :last-child doesn't apply given a .dropdown-menu immediately after it\n.btn-group > .btn:last-child:not(:first-child),\n.btn-group > .dropdown-toggle:not(:first-child) {\n .border-left-radius(0);\n}\n\n// Custom edits for including btn-groups within btn-groups (useful for including dropdown buttons within a btn-group)\n.btn-group > .btn-group {\n float: left;\n}\n.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group > .btn-group:first-child:not(:last-child) {\n > .btn:last-child,\n > .dropdown-toggle {\n .border-right-radius(0);\n }\n}\n.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child {\n .border-left-radius(0);\n}\n\n// On active and open, don't show outline\n.btn-group .dropdown-toggle:active,\n.btn-group.open .dropdown-toggle {\n outline: 0;\n}\n\n\n// Sizing\n//\n// Remix the default button sizing classes into new ones for easier manipulation.\n\n.btn-group-xs > .btn { &:extend(.btn-xs); }\n.btn-group-sm > .btn { &:extend(.btn-sm); }\n.btn-group-lg > .btn { &:extend(.btn-lg); }\n\n\n// Split button dropdowns\n// ----------------------\n\n// Give the line between buttons some depth\n.btn-group > .btn + .dropdown-toggle {\n padding-left: 8px;\n padding-right: 8px;\n}\n.btn-group > .btn-lg + .dropdown-toggle {\n padding-left: 12px;\n padding-right: 12px;\n}\n\n// The clickable button for toggling the menu\n// Remove the gradient and set the same inset shadow as the :active state\n.btn-group.open .dropdown-toggle {\n .box-shadow(inset 0 3px 5px rgba(0,0,0,.125));\n\n // Show no shadow for `.btn-link` since it has no other button styles.\n &.btn-link {\n .box-shadow(none);\n }\n}\n\n\n// Reposition the caret\n.btn .caret {\n margin-left: 0;\n}\n// Carets in other button sizes\n.btn-lg .caret {\n border-width: @caret-width-large @caret-width-large 0;\n border-bottom-width: 0;\n}\n// Upside down carets for .dropup\n.dropup .btn-lg .caret {\n border-width: 0 @caret-width-large @caret-width-large;\n}\n\n\n// Vertical button groups\n// ----------------------\n\n.btn-group-vertical {\n > .btn,\n > .btn-group,\n > .btn-group > .btn {\n display: block;\n float: none;\n width: 100%;\n max-width: 100%;\n }\n\n // Clear floats so dropdown menus can be properly placed\n > .btn-group {\n &:extend(.clearfix all);\n > .btn {\n float: none;\n }\n }\n\n > .btn + .btn,\n > .btn + .btn-group,\n > .btn-group + .btn,\n > .btn-group + .btn-group {\n margin-top: -1px;\n margin-left: 0;\n }\n}\n\n.btn-group-vertical > .btn {\n &:not(:first-child):not(:last-child) {\n border-radius: 0;\n }\n &:first-child:not(:last-child) {\n .border-top-radius(@btn-border-radius-base);\n .border-bottom-radius(0);\n }\n &:last-child:not(:first-child) {\n .border-top-radius(0);\n .border-bottom-radius(@btn-border-radius-base);\n }\n}\n.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group-vertical > .btn-group:first-child:not(:last-child) {\n > .btn:last-child,\n > .dropdown-toggle {\n .border-bottom-radius(0);\n }\n}\n.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {\n .border-top-radius(0);\n}\n\n\n// Justified button groups\n// ----------------------\n\n.btn-group-justified {\n display: table;\n width: 100%;\n table-layout: fixed;\n border-collapse: separate;\n > .btn,\n > .btn-group {\n float: none;\n display: table-cell;\n width: 1%;\n }\n > .btn-group .btn {\n width: 100%;\n }\n\n > .btn-group .dropdown-menu {\n left: auto;\n }\n}\n\n\n// Checkbox and radio options\n//\n// In order to support the browser's form validation feedback, powered by the\n// `required` attribute, we have to \"hide\" the inputs via `clip`. We cannot use\n// `display: none;` or `visibility: hidden;` as that also hides the popover.\n// Simply visually hiding the inputs via `opacity` would leave them clickable in\n// certain cases which is prevented by using `clip` and `pointer-events`.\n// This way, we ensure a DOM element is visible to position the popover from.\n//\n// See https://github.com/twbs/bootstrap/pull/12794 and\n// https://github.com/twbs/bootstrap/pull/14559 for more information.\n\n[data-toggle=\"buttons\"] {\n > .btn,\n > .btn-group > .btn {\n input[type=\"radio\"],\n input[type=\"checkbox\"] {\n position: absolute;\n clip: rect(0,0,0,0);\n pointer-events: none;\n }\n }\n}\n","// Single side border-radius\n\n.border-top-radius(@radius) {\n border-top-right-radius: @radius;\n border-top-left-radius: @radius;\n}\n.border-right-radius(@radius) {\n border-bottom-right-radius: @radius;\n border-top-right-radius: @radius;\n}\n.border-bottom-radius(@radius) {\n border-bottom-right-radius: @radius;\n border-bottom-left-radius: @radius;\n}\n.border-left-radius(@radius) {\n border-bottom-left-radius: @radius;\n border-top-left-radius: @radius;\n}\n","//\n// Input groups\n// --------------------------------------------------\n\n// Base styles\n// -------------------------\n.input-group {\n position: relative; // For dropdowns\n display: table;\n border-collapse: separate; // prevent input groups from inheriting border styles from table cells when placed within a table\n\n // Undo padding and float of grid classes\n &[class*=\"col-\"] {\n float: none;\n padding-left: 0;\n padding-right: 0;\n }\n\n .form-control {\n // Ensure that the input is always above the *appended* addon button for\n // proper border colors.\n position: relative;\n z-index: 2;\n\n // IE9 fubars the placeholder attribute in text inputs and the arrows on\n // select elements in input groups. To fix it, we float the input. Details:\n // https://github.com/twbs/bootstrap/issues/11561#issuecomment-28936855\n float: left;\n\n width: 100%;\n margin-bottom: 0;\n \n &:focus {\n z-index: 3;\n }\n }\n}\n\n// Sizing options\n//\n// Remix the default form control sizing classes into new ones for easier\n// manipulation.\n\n.input-group-lg > .form-control,\n.input-group-lg > .input-group-addon,\n.input-group-lg > .input-group-btn > .btn {\n .input-lg();\n}\n.input-group-sm > .form-control,\n.input-group-sm > .input-group-addon,\n.input-group-sm > .input-group-btn > .btn {\n .input-sm();\n}\n\n\n// Display as table-cell\n// -------------------------\n.input-group-addon,\n.input-group-btn,\n.input-group .form-control {\n display: table-cell;\n\n &:not(:first-child):not(:last-child) {\n border-radius: 0;\n }\n}\n// Addon and addon wrapper for buttons\n.input-group-addon,\n.input-group-btn {\n width: 1%;\n white-space: nowrap;\n vertical-align: middle; // Match the inputs\n}\n\n// Text input groups\n// -------------------------\n.input-group-addon {\n padding: @padding-base-vertical @padding-base-horizontal;\n font-size: @font-size-base;\n font-weight: normal;\n line-height: 1;\n color: @input-color;\n text-align: center;\n background-color: @input-group-addon-bg;\n border: 1px solid @input-group-addon-border-color;\n border-radius: @input-border-radius;\n\n // Sizing\n &.input-sm {\n padding: @padding-small-vertical @padding-small-horizontal;\n font-size: @font-size-small;\n border-radius: @input-border-radius-small;\n }\n &.input-lg {\n padding: @padding-large-vertical @padding-large-horizontal;\n font-size: @font-size-large;\n border-radius: @input-border-radius-large;\n }\n\n // Nuke default margins from checkboxes and radios to vertically center within.\n input[type=\"radio\"],\n input[type=\"checkbox\"] {\n margin-top: 0;\n }\n}\n\n// Reset rounded corners\n.input-group .form-control:first-child,\n.input-group-addon:first-child,\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group > .btn,\n.input-group-btn:first-child > .dropdown-toggle,\n.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group-btn:last-child > .btn-group:not(:last-child) > .btn {\n .border-right-radius(0);\n}\n.input-group-addon:first-child {\n border-right: 0;\n}\n.input-group .form-control:last-child,\n.input-group-addon:last-child,\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group > .btn,\n.input-group-btn:last-child > .dropdown-toggle,\n.input-group-btn:first-child > .btn:not(:first-child),\n.input-group-btn:first-child > .btn-group:not(:first-child) > .btn {\n .border-left-radius(0);\n}\n.input-group-addon:last-child {\n border-left: 0;\n}\n\n// Button input groups\n// -------------------------\n.input-group-btn {\n position: relative;\n // Jankily prevent input button groups from wrapping with `white-space` and\n // `font-size` in combination with `inline-block` on buttons.\n font-size: 0;\n white-space: nowrap;\n\n // Negative margin for spacing, position for bringing hovered/focused/actived\n // element above the siblings.\n > .btn {\n position: relative;\n + .btn {\n margin-left: -1px;\n }\n // Bring the \"active\" button to the front\n &:hover,\n &:focus,\n &:active {\n z-index: 2;\n }\n }\n\n // Negative margin to only have a 1px border between the two\n &:first-child {\n > .btn,\n > .btn-group {\n margin-right: -1px;\n }\n }\n &:last-child {\n > .btn,\n > .btn-group {\n z-index: 2;\n margin-left: -1px;\n }\n }\n}\n","//\n// Navs\n// --------------------------------------------------\n\n\n// Base class\n// --------------------------------------------------\n\n.nav {\n margin-bottom: 0;\n padding-left: 0; // Override default ul/ol\n list-style: none;\n &:extend(.clearfix all);\n\n > li {\n position: relative;\n display: block;\n\n > a {\n position: relative;\n display: block;\n padding: @nav-link-padding;\n &:hover,\n &:focus {\n text-decoration: none;\n background-color: @nav-link-hover-bg;\n }\n }\n\n // Disabled state sets text to gray and nukes hover/tab effects\n &.disabled > a {\n color: @nav-disabled-link-color;\n\n &:hover,\n &:focus {\n color: @nav-disabled-link-hover-color;\n text-decoration: none;\n background-color: transparent;\n cursor: @cursor-disabled;\n }\n }\n }\n\n // Open dropdowns\n .open > a {\n &,\n &:hover,\n &:focus {\n background-color: @nav-link-hover-bg;\n border-color: @link-color;\n }\n }\n\n // Nav dividers (deprecated with v3.0.1)\n //\n // This should have been removed in v3 with the dropping of `.nav-list`, but\n // we missed it. We don't currently support this anywhere, but in the interest\n // of maintaining backward compatibility in case you use it, it's deprecated.\n .nav-divider {\n .nav-divider();\n }\n\n // Prevent IE8 from misplacing imgs\n //\n // See https://github.com/h5bp/html5-boilerplate/issues/984#issuecomment-3985989\n > li > a > img {\n max-width: none;\n }\n}\n\n\n// Tabs\n// -------------------------\n\n// Give the tabs something to sit on\n.nav-tabs {\n border-bottom: 1px solid @nav-tabs-border-color;\n > li {\n float: left;\n // Make the list-items overlay the bottom border\n margin-bottom: -1px;\n\n // Actual tabs (as links)\n > a {\n margin-right: 2px;\n line-height: @line-height-base;\n border: 1px solid transparent;\n border-radius: @border-radius-base @border-radius-base 0 0;\n &:hover {\n border-color: @nav-tabs-link-hover-border-color @nav-tabs-link-hover-border-color @nav-tabs-border-color;\n }\n }\n\n // Active state, and its :hover to override normal :hover\n &.active > a {\n &,\n &:hover,\n &:focus {\n color: @nav-tabs-active-link-hover-color;\n background-color: @nav-tabs-active-link-hover-bg;\n border: 1px solid @nav-tabs-active-link-hover-border-color;\n border-bottom-color: transparent;\n cursor: default;\n }\n }\n }\n // pulling this in mainly for less shorthand\n &.nav-justified {\n .nav-justified();\n .nav-tabs-justified();\n }\n}\n\n\n// Pills\n// -------------------------\n.nav-pills {\n > li {\n float: left;\n\n // Links rendered as pills\n > a {\n border-radius: @nav-pills-border-radius;\n }\n + li {\n margin-left: 2px;\n }\n\n // Active state\n &.active > a {\n &,\n &:hover,\n &:focus {\n color: @nav-pills-active-link-hover-color;\n background-color: @nav-pills-active-link-hover-bg;\n }\n }\n }\n}\n\n\n// Stacked pills\n.nav-stacked {\n > li {\n float: none;\n + li {\n margin-top: 2px;\n margin-left: 0; // no need for this gap between nav items\n }\n }\n}\n\n\n// Nav variations\n// --------------------------------------------------\n\n// Justified nav links\n// -------------------------\n\n.nav-justified {\n width: 100%;\n\n > li {\n float: none;\n > a {\n text-align: center;\n margin-bottom: 5px;\n }\n }\n\n > .dropdown .dropdown-menu {\n top: auto;\n left: auto;\n }\n\n @media (min-width: @screen-sm-min) {\n > li {\n display: table-cell;\n width: 1%;\n > a {\n margin-bottom: 0;\n }\n }\n }\n}\n\n// Move borders to anchors instead of bottom of list\n//\n// Mixin for adding on top the shared `.nav-justified` styles for our tabs\n.nav-tabs-justified {\n border-bottom: 0;\n\n > li > a {\n // Override margin from .nav-tabs\n margin-right: 0;\n border-radius: @border-radius-base;\n }\n\n > .active > a,\n > .active > a:hover,\n > .active > a:focus {\n border: 1px solid @nav-tabs-justified-link-border-color;\n }\n\n @media (min-width: @screen-sm-min) {\n > li > a {\n border-bottom: 1px solid @nav-tabs-justified-link-border-color;\n border-radius: @border-radius-base @border-radius-base 0 0;\n }\n > .active > a,\n > .active > a:hover,\n > .active > a:focus {\n border-bottom-color: @nav-tabs-justified-active-link-border-color;\n }\n }\n}\n\n\n// Tabbable tabs\n// -------------------------\n\n// Hide tabbable panes to start, show them when `.active`\n.tab-content {\n > .tab-pane {\n display: none;\n }\n > .active {\n display: block;\n }\n}\n\n\n// Dropdowns\n// -------------------------\n\n// Specific dropdowns\n.nav-tabs .dropdown-menu {\n // make dropdown border overlap tab border\n margin-top: -1px;\n // Remove the top rounded corners here since there is a hard edge above the menu\n .border-top-radius(0);\n}\n","//\n// Navbars\n// --------------------------------------------------\n\n\n// Wrapper and base class\n//\n// Provide a static navbar from which we expand to create full-width, fixed, and\n// other navbar variations.\n\n.navbar {\n position: relative;\n min-height: @navbar-height; // Ensure a navbar always shows (e.g., without a .navbar-brand in collapsed mode)\n margin-bottom: @navbar-margin-bottom;\n border: 1px solid transparent;\n\n // Prevent floats from breaking the navbar\n &:extend(.clearfix all);\n\n @media (min-width: @grid-float-breakpoint) {\n border-radius: @navbar-border-radius;\n }\n}\n\n\n// Navbar heading\n//\n// Groups `.navbar-brand` and `.navbar-toggle` into a single component for easy\n// styling of responsive aspects.\n\n.navbar-header {\n &:extend(.clearfix all);\n\n @media (min-width: @grid-float-breakpoint) {\n float: left;\n }\n}\n\n\n// Navbar collapse (body)\n//\n// Group your navbar content into this for easy collapsing and expanding across\n// various device sizes. By default, this content is collapsed when <768px, but\n// will expand past that for a horizontal display.\n//\n// To start (on mobile devices) the navbar links, forms, and buttons are stacked\n// vertically and include a `max-height` to overflow in case you have too much\n// content for the user's viewport.\n\n.navbar-collapse {\n overflow-x: visible;\n padding-right: @navbar-padding-horizontal;\n padding-left: @navbar-padding-horizontal;\n border-top: 1px solid transparent;\n box-shadow: inset 0 1px 0 rgba(255,255,255,.1);\n &:extend(.clearfix all);\n -webkit-overflow-scrolling: touch;\n\n &.in {\n overflow-y: auto;\n }\n\n @media (min-width: @grid-float-breakpoint) {\n width: auto;\n border-top: 0;\n box-shadow: none;\n\n &.collapse {\n display: block !important;\n height: auto !important;\n padding-bottom: 0; // Override default setting\n overflow: visible !important;\n }\n\n &.in {\n overflow-y: visible;\n }\n\n // Undo the collapse side padding for navbars with containers to ensure\n // alignment of right-aligned contents.\n .navbar-fixed-top &,\n .navbar-static-top &,\n .navbar-fixed-bottom & {\n padding-left: 0;\n padding-right: 0;\n }\n }\n}\n\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n .navbar-collapse {\n max-height: @navbar-collapse-max-height;\n\n @media (max-device-width: @screen-xs-min) and (orientation: landscape) {\n max-height: 200px;\n }\n }\n}\n\n\n// Both navbar header and collapse\n//\n// When a container is present, change the behavior of the header and collapse.\n\n.container,\n.container-fluid {\n > .navbar-header,\n > .navbar-collapse {\n margin-right: -@navbar-padding-horizontal;\n margin-left: -@navbar-padding-horizontal;\n\n @media (min-width: @grid-float-breakpoint) {\n margin-right: 0;\n margin-left: 0;\n }\n }\n}\n\n\n//\n// Navbar alignment options\n//\n// Display the navbar across the entirety of the page or fixed it to the top or\n// bottom of the page.\n\n// Static top (unfixed, but 100% wide) navbar\n.navbar-static-top {\n z-index: @zindex-navbar;\n border-width: 0 0 1px;\n\n @media (min-width: @grid-float-breakpoint) {\n border-radius: 0;\n }\n}\n\n// Fix the top/bottom navbars when screen real estate supports it\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n position: fixed;\n right: 0;\n left: 0;\n z-index: @zindex-navbar-fixed;\n\n // Undo the rounded corners\n @media (min-width: @grid-float-breakpoint) {\n border-radius: 0;\n }\n}\n.navbar-fixed-top {\n top: 0;\n border-width: 0 0 1px;\n}\n.navbar-fixed-bottom {\n bottom: 0;\n margin-bottom: 0; // override .navbar defaults\n border-width: 1px 0 0;\n}\n\n\n// Brand/project name\n\n.navbar-brand {\n float: left;\n padding: @navbar-padding-vertical @navbar-padding-horizontal;\n font-size: @font-size-large;\n line-height: @line-height-computed;\n height: @navbar-height;\n\n &:hover,\n &:focus {\n text-decoration: none;\n }\n\n > img {\n display: block;\n }\n\n @media (min-width: @grid-float-breakpoint) {\n .navbar > .container &,\n .navbar > .container-fluid & {\n margin-left: -@navbar-padding-horizontal;\n }\n }\n}\n\n\n// Navbar toggle\n//\n// Custom button for toggling the `.navbar-collapse`, powered by the collapse\n// JavaScript plugin.\n\n.navbar-toggle {\n position: relative;\n float: right;\n margin-right: @navbar-padding-horizontal;\n padding: 9px 10px;\n .navbar-vertical-align(34px);\n background-color: transparent;\n background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214\n border: 1px solid transparent;\n border-radius: @border-radius-base;\n\n // We remove the `outline` here, but later compensate by attaching `:hover`\n // styles to `:focus`.\n &:focus {\n outline: 0;\n }\n\n // Bars\n .icon-bar {\n display: block;\n width: 22px;\n height: 2px;\n border-radius: 1px;\n }\n .icon-bar + .icon-bar {\n margin-top: 4px;\n }\n\n @media (min-width: @grid-float-breakpoint) {\n display: none;\n }\n}\n\n\n// Navbar nav links\n//\n// Builds on top of the `.nav` components with its own modifier class to make\n// the nav the full height of the horizontal nav (above 768px).\n\n.navbar-nav {\n margin: (@navbar-padding-vertical / 2) -@navbar-padding-horizontal;\n\n > li > a {\n padding-top: 10px;\n padding-bottom: 10px;\n line-height: @line-height-computed;\n }\n\n @media (max-width: @grid-float-breakpoint-max) {\n // Dropdowns get custom display when collapsed\n .open .dropdown-menu {\n position: static;\n float: none;\n width: auto;\n margin-top: 0;\n background-color: transparent;\n border: 0;\n box-shadow: none;\n > li > a,\n .dropdown-header {\n padding: 5px 15px 5px 25px;\n }\n > li > a {\n line-height: @line-height-computed;\n &:hover,\n &:focus {\n background-image: none;\n }\n }\n }\n }\n\n // Uncollapse the nav\n @media (min-width: @grid-float-breakpoint) {\n float: left;\n margin: 0;\n\n > li {\n float: left;\n > a {\n padding-top: @navbar-padding-vertical;\n padding-bottom: @navbar-padding-vertical;\n }\n }\n }\n}\n\n\n// Navbar form\n//\n// Extension of the `.form-inline` with some extra flavor for optimum display in\n// our navbars.\n\n.navbar-form {\n margin-left: -@navbar-padding-horizontal;\n margin-right: -@navbar-padding-horizontal;\n padding: 10px @navbar-padding-horizontal;\n border-top: 1px solid transparent;\n border-bottom: 1px solid transparent;\n @shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1);\n .box-shadow(@shadow);\n\n // Mixin behavior for optimum display\n .form-inline();\n\n .form-group {\n @media (max-width: @grid-float-breakpoint-max) {\n margin-bottom: 5px;\n\n &:last-child {\n margin-bottom: 0;\n }\n }\n }\n\n // Vertically center in expanded, horizontal navbar\n .navbar-vertical-align(@input-height-base);\n\n // Undo 100% width for pull classes\n @media (min-width: @grid-float-breakpoint) {\n width: auto;\n border: 0;\n margin-left: 0;\n margin-right: 0;\n padding-top: 0;\n padding-bottom: 0;\n .box-shadow(none);\n }\n}\n\n\n// Dropdown menus\n\n// Menu position and menu carets\n.navbar-nav > li > .dropdown-menu {\n margin-top: 0;\n .border-top-radius(0);\n}\n// Menu position and menu caret support for dropups via extra dropup class\n.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {\n margin-bottom: 0;\n .border-top-radius(@navbar-border-radius);\n .border-bottom-radius(0);\n}\n\n\n// Buttons in navbars\n//\n// Vertically center a button within a navbar (when *not* in a form).\n\n.navbar-btn {\n .navbar-vertical-align(@input-height-base);\n\n &.btn-sm {\n .navbar-vertical-align(@input-height-small);\n }\n &.btn-xs {\n .navbar-vertical-align(22);\n }\n}\n\n\n// Text in navbars\n//\n// Add a class to make any element properly align itself vertically within the navbars.\n\n.navbar-text {\n .navbar-vertical-align(@line-height-computed);\n\n @media (min-width: @grid-float-breakpoint) {\n float: left;\n margin-left: @navbar-padding-horizontal;\n margin-right: @navbar-padding-horizontal;\n }\n}\n\n\n// Component alignment\n//\n// Repurpose the pull utilities as their own navbar utilities to avoid specificity\n// issues with parents and chaining. Only do this when the navbar is uncollapsed\n// though so that navbar contents properly stack and align in mobile.\n//\n// Declared after the navbar components to ensure more specificity on the margins.\n\n@media (min-width: @grid-float-breakpoint) {\n .navbar-left { .pull-left(); }\n .navbar-right {\n .pull-right();\n margin-right: -@navbar-padding-horizontal;\n\n ~ .navbar-right {\n margin-right: 0;\n }\n }\n}\n\n\n// Alternate navbars\n// --------------------------------------------------\n\n// Default navbar\n.navbar-default {\n background-color: @navbar-default-bg;\n border-color: @navbar-default-border;\n\n .navbar-brand {\n color: @navbar-default-brand-color;\n &:hover,\n &:focus {\n color: @navbar-default-brand-hover-color;\n background-color: @navbar-default-brand-hover-bg;\n }\n }\n\n .navbar-text {\n color: @navbar-default-color;\n }\n\n .navbar-nav {\n > li > a {\n color: @navbar-default-link-color;\n\n &:hover,\n &:focus {\n color: @navbar-default-link-hover-color;\n background-color: @navbar-default-link-hover-bg;\n }\n }\n > .active > a {\n &,\n &:hover,\n &:focus {\n color: @navbar-default-link-active-color;\n background-color: @navbar-default-link-active-bg;\n }\n }\n > .disabled > a {\n &,\n &:hover,\n &:focus {\n color: @navbar-default-link-disabled-color;\n background-color: @navbar-default-link-disabled-bg;\n }\n }\n }\n\n .navbar-toggle {\n border-color: @navbar-default-toggle-border-color;\n &:hover,\n &:focus {\n background-color: @navbar-default-toggle-hover-bg;\n }\n .icon-bar {\n background-color: @navbar-default-toggle-icon-bar-bg;\n }\n }\n\n .navbar-collapse,\n .navbar-form {\n border-color: @navbar-default-border;\n }\n\n // Dropdown menu items\n .navbar-nav {\n // Remove background color from open dropdown\n > .open > a {\n &,\n &:hover,\n &:focus {\n background-color: @navbar-default-link-active-bg;\n color: @navbar-default-link-active-color;\n }\n }\n\n @media (max-width: @grid-float-breakpoint-max) {\n // Dropdowns get custom display when collapsed\n .open .dropdown-menu {\n > li > a {\n color: @navbar-default-link-color;\n &:hover,\n &:focus {\n color: @navbar-default-link-hover-color;\n background-color: @navbar-default-link-hover-bg;\n }\n }\n > .active > a {\n &,\n &:hover,\n &:focus {\n color: @navbar-default-link-active-color;\n background-color: @navbar-default-link-active-bg;\n }\n }\n > .disabled > a {\n &,\n &:hover,\n &:focus {\n color: @navbar-default-link-disabled-color;\n background-color: @navbar-default-link-disabled-bg;\n }\n }\n }\n }\n }\n\n\n // Links in navbars\n //\n // Add a class to ensure links outside the navbar nav are colored correctly.\n\n .navbar-link {\n color: @navbar-default-link-color;\n &:hover {\n color: @navbar-default-link-hover-color;\n }\n }\n\n .btn-link {\n color: @navbar-default-link-color;\n &:hover,\n &:focus {\n color: @navbar-default-link-hover-color;\n }\n &[disabled],\n fieldset[disabled] & {\n &:hover,\n &:focus {\n color: @navbar-default-link-disabled-color;\n }\n }\n }\n}\n\n// Inverse navbar\n\n.navbar-inverse {\n background-color: @navbar-inverse-bg;\n border-color: @navbar-inverse-border;\n\n .navbar-brand {\n color: @navbar-inverse-brand-color;\n &:hover,\n &:focus {\n color: @navbar-inverse-brand-hover-color;\n background-color: @navbar-inverse-brand-hover-bg;\n }\n }\n\n .navbar-text {\n color: @navbar-inverse-color;\n }\n\n .navbar-nav {\n > li > a {\n color: @navbar-inverse-link-color;\n\n &:hover,\n &:focus {\n color: @navbar-inverse-link-hover-color;\n background-color: @navbar-inverse-link-hover-bg;\n }\n }\n > .active > a {\n &,\n &:hover,\n &:focus {\n color: @navbar-inverse-link-active-color;\n background-color: @navbar-inverse-link-active-bg;\n }\n }\n > .disabled > a {\n &,\n &:hover,\n &:focus {\n color: @navbar-inverse-link-disabled-color;\n background-color: @navbar-inverse-link-disabled-bg;\n }\n }\n }\n\n // Darken the responsive nav toggle\n .navbar-toggle {\n border-color: @navbar-inverse-toggle-border-color;\n &:hover,\n &:focus {\n background-color: @navbar-inverse-toggle-hover-bg;\n }\n .icon-bar {\n background-color: @navbar-inverse-toggle-icon-bar-bg;\n }\n }\n\n .navbar-collapse,\n .navbar-form {\n border-color: darken(@navbar-inverse-bg, 7%);\n }\n\n // Dropdowns\n .navbar-nav {\n > .open > a {\n &,\n &:hover,\n &:focus {\n background-color: @navbar-inverse-link-active-bg;\n color: @navbar-inverse-link-active-color;\n }\n }\n\n @media (max-width: @grid-float-breakpoint-max) {\n // Dropdowns get custom display\n .open .dropdown-menu {\n > .dropdown-header {\n border-color: @navbar-inverse-border;\n }\n .divider {\n background-color: @navbar-inverse-border;\n }\n > li > a {\n color: @navbar-inverse-link-color;\n &:hover,\n &:focus {\n color: @navbar-inverse-link-hover-color;\n background-color: @navbar-inverse-link-hover-bg;\n }\n }\n > .active > a {\n &,\n &:hover,\n &:focus {\n color: @navbar-inverse-link-active-color;\n background-color: @navbar-inverse-link-active-bg;\n }\n }\n > .disabled > a {\n &,\n &:hover,\n &:focus {\n color: @navbar-inverse-link-disabled-color;\n background-color: @navbar-inverse-link-disabled-bg;\n }\n }\n }\n }\n }\n\n .navbar-link {\n color: @navbar-inverse-link-color;\n &:hover {\n color: @navbar-inverse-link-hover-color;\n }\n }\n\n .btn-link {\n color: @navbar-inverse-link-color;\n &:hover,\n &:focus {\n color: @navbar-inverse-link-hover-color;\n }\n &[disabled],\n fieldset[disabled] & {\n &:hover,\n &:focus {\n color: @navbar-inverse-link-disabled-color;\n }\n }\n }\n}\n","// Navbar vertical align\n//\n// Vertically center elements in the navbar.\n// Example: an element has a height of 30px, so write out `.navbar-vertical-align(30px);` to calculate the appropriate top margin.\n\n.navbar-vertical-align(@element-height) {\n margin-top: ((@navbar-height - @element-height) / 2);\n margin-bottom: ((@navbar-height - @element-height) / 2);\n}\n","//\n// Utility classes\n// --------------------------------------------------\n\n\n// Floats\n// -------------------------\n\n.clearfix {\n .clearfix();\n}\n.center-block {\n .center-block();\n}\n.pull-right {\n float: right !important;\n}\n.pull-left {\n float: left !important;\n}\n\n\n// Toggling content\n// -------------------------\n\n// Note: Deprecated .hide in favor of .hidden or .sr-only (as appropriate) in v3.0.1\n.hide {\n display: none !important;\n}\n.show {\n display: block !important;\n}\n.invisible {\n visibility: hidden;\n}\n.text-hide {\n .text-hide();\n}\n\n\n// Hide from screenreaders and browsers\n//\n// Credit: HTML5 Boilerplate\n\n.hidden {\n display: none !important;\n}\n\n\n// For Affix plugin\n// -------------------------\n\n.affix {\n position: fixed;\n}\n","//\n// Breadcrumbs\n// --------------------------------------------------\n\n\n.breadcrumb {\n padding: @breadcrumb-padding-vertical @breadcrumb-padding-horizontal;\n margin-bottom: @line-height-computed;\n list-style: none;\n background-color: @breadcrumb-bg;\n border-radius: @border-radius-base;\n\n > li {\n display: inline-block;\n\n + li:before {\n content: \"@{breadcrumb-separator}\\00a0\"; // Unicode space added since inline-block means non-collapsing white-space\n padding: 0 5px;\n color: @breadcrumb-color;\n }\n }\n\n > .active {\n color: @breadcrumb-active-color;\n }\n}\n","//\n// Pagination (multiple pages)\n// --------------------------------------------------\n.pagination {\n display: inline-block;\n padding-left: 0;\n margin: @line-height-computed 0;\n border-radius: @border-radius-base;\n\n > li {\n display: inline; // Remove list-style and block-level defaults\n > a,\n > span {\n position: relative;\n float: left; // Collapse white-space\n padding: @padding-base-vertical @padding-base-horizontal;\n line-height: @line-height-base;\n text-decoration: none;\n color: @pagination-color;\n background-color: @pagination-bg;\n border: 1px solid @pagination-border;\n margin-left: -1px;\n }\n &:first-child {\n > a,\n > span {\n margin-left: 0;\n .border-left-radius(@border-radius-base);\n }\n }\n &:last-child {\n > a,\n > span {\n .border-right-radius(@border-radius-base);\n }\n }\n }\n\n > li > a,\n > li > span {\n &:hover,\n &:focus {\n z-index: 2;\n color: @pagination-hover-color;\n background-color: @pagination-hover-bg;\n border-color: @pagination-hover-border;\n }\n }\n\n > .active > a,\n > .active > span {\n &,\n &:hover,\n &:focus {\n z-index: 3;\n color: @pagination-active-color;\n background-color: @pagination-active-bg;\n border-color: @pagination-active-border;\n cursor: default;\n }\n }\n\n > .disabled {\n > span,\n > span:hover,\n > span:focus,\n > a,\n > a:hover,\n > a:focus {\n color: @pagination-disabled-color;\n background-color: @pagination-disabled-bg;\n border-color: @pagination-disabled-border;\n cursor: @cursor-disabled;\n }\n }\n}\n\n// Sizing\n// --------------------------------------------------\n\n// Large\n.pagination-lg {\n .pagination-size(@padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @border-radius-large);\n}\n\n// Small\n.pagination-sm {\n .pagination-size(@padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @border-radius-small);\n}\n","// Pagination\n\n.pagination-size(@padding-vertical; @padding-horizontal; @font-size; @line-height; @border-radius) {\n > li {\n > a,\n > span {\n padding: @padding-vertical @padding-horizontal;\n font-size: @font-size;\n line-height: @line-height;\n }\n &:first-child {\n > a,\n > span {\n .border-left-radius(@border-radius);\n }\n }\n &:last-child {\n > a,\n > span {\n .border-right-radius(@border-radius);\n }\n }\n }\n}\n","//\n// Pager pagination\n// --------------------------------------------------\n\n\n.pager {\n padding-left: 0;\n margin: @line-height-computed 0;\n list-style: none;\n text-align: center;\n &:extend(.clearfix all);\n li {\n display: inline;\n > a,\n > span {\n display: inline-block;\n padding: 5px 14px;\n background-color: @pager-bg;\n border: 1px solid @pager-border;\n border-radius: @pager-border-radius;\n }\n\n > a:hover,\n > a:focus {\n text-decoration: none;\n background-color: @pager-hover-bg;\n }\n }\n\n .next {\n > a,\n > span {\n float: right;\n }\n }\n\n .previous {\n > a,\n > span {\n float: left;\n }\n }\n\n .disabled {\n > a,\n > a:hover,\n > a:focus,\n > span {\n color: @pager-disabled-color;\n background-color: @pager-bg;\n cursor: @cursor-disabled;\n }\n }\n}\n","//\n// Labels\n// --------------------------------------------------\n\n.label {\n display: inline;\n padding: .2em .6em .3em;\n font-size: 75%;\n font-weight: bold;\n line-height: 1;\n color: @label-color;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: .25em;\n\n // Add hover effects, but only for links\n a& {\n &:hover,\n &:focus {\n color: @label-link-hover-color;\n text-decoration: none;\n cursor: pointer;\n }\n }\n\n // Empty labels collapse automatically (not available in IE8)\n &:empty {\n display: none;\n }\n\n // Quick fix for labels in buttons\n .btn & {\n position: relative;\n top: -1px;\n }\n}\n\n// Colors\n// Contextual variations (linked labels get darker on :hover)\n\n.label-default {\n .label-variant(@label-default-bg);\n}\n\n.label-primary {\n .label-variant(@label-primary-bg);\n}\n\n.label-success {\n .label-variant(@label-success-bg);\n}\n\n.label-info {\n .label-variant(@label-info-bg);\n}\n\n.label-warning {\n .label-variant(@label-warning-bg);\n}\n\n.label-danger {\n .label-variant(@label-danger-bg);\n}\n","// Labels\n\n.label-variant(@color) {\n background-color: @color;\n\n &[href] {\n &:hover,\n &:focus {\n background-color: darken(@color, 10%);\n }\n }\n}\n","//\n// Badges\n// --------------------------------------------------\n\n\n// Base class\n.badge {\n display: inline-block;\n min-width: 10px;\n padding: 3px 7px;\n font-size: @font-size-small;\n font-weight: @badge-font-weight;\n color: @badge-color;\n line-height: @badge-line-height;\n vertical-align: middle;\n white-space: nowrap;\n text-align: center;\n background-color: @badge-bg;\n border-radius: @badge-border-radius;\n\n // Empty badges collapse automatically (not available in IE8)\n &:empty {\n display: none;\n }\n\n // Quick fix for badges in buttons\n .btn & {\n position: relative;\n top: -1px;\n }\n\n .btn-xs &,\n .btn-group-xs > .btn & {\n top: 0;\n padding: 1px 5px;\n }\n\n // Hover state, but only for links\n a& {\n &:hover,\n &:focus {\n color: @badge-link-hover-color;\n text-decoration: none;\n cursor: pointer;\n }\n }\n\n // Account for badges in navs\n .list-group-item.active > &,\n .nav-pills > .active > a > & {\n color: @badge-active-color;\n background-color: @badge-active-bg;\n }\n\n .list-group-item > & {\n float: right;\n }\n\n .list-group-item > & + & {\n margin-right: 5px;\n }\n\n .nav-pills > li > a > & {\n margin-left: 3px;\n }\n}\n","//\n// Jumbotron\n// --------------------------------------------------\n\n\n.jumbotron {\n padding-top: @jumbotron-padding;\n padding-bottom: @jumbotron-padding;\n margin-bottom: @jumbotron-padding;\n color: @jumbotron-color;\n background-color: @jumbotron-bg;\n\n h1,\n .h1 {\n color: @jumbotron-heading-color;\n }\n\n p {\n margin-bottom: (@jumbotron-padding / 2);\n font-size: @jumbotron-font-size;\n font-weight: 200;\n }\n\n > hr {\n border-top-color: darken(@jumbotron-bg, 10%);\n }\n\n .container &,\n .container-fluid & {\n border-radius: @border-radius-large; // Only round corners at higher resolutions if contained in a container\n padding-left: (@grid-gutter-width / 2);\n padding-right: (@grid-gutter-width / 2);\n }\n\n .container {\n max-width: 100%;\n }\n\n @media screen and (min-width: @screen-sm-min) {\n padding-top: (@jumbotron-padding * 1.6);\n padding-bottom: (@jumbotron-padding * 1.6);\n\n .container &,\n .container-fluid & {\n padding-left: (@jumbotron-padding * 2);\n padding-right: (@jumbotron-padding * 2);\n }\n\n h1,\n .h1 {\n font-size: @jumbotron-heading-font-size;\n }\n }\n}\n","//\n// Thumbnails\n// --------------------------------------------------\n\n\n// Mixin and adjust the regular image class\n.thumbnail {\n display: block;\n padding: @thumbnail-padding;\n margin-bottom: @line-height-computed;\n line-height: @line-height-base;\n background-color: @thumbnail-bg;\n border: 1px solid @thumbnail-border;\n border-radius: @thumbnail-border-radius;\n .transition(border .2s ease-in-out);\n\n > img,\n a > img {\n &:extend(.img-responsive);\n margin-left: auto;\n margin-right: auto;\n }\n\n // Add a hover state for linked versions only\n a&:hover,\n a&:focus,\n a&.active {\n border-color: @link-color;\n }\n\n // Image captions\n .caption {\n padding: @thumbnail-caption-padding;\n color: @thumbnail-caption-color;\n }\n}\n","//\n// Alerts\n// --------------------------------------------------\n\n\n// Base styles\n// -------------------------\n\n.alert {\n padding: @alert-padding;\n margin-bottom: @line-height-computed;\n border: 1px solid transparent;\n border-radius: @alert-border-radius;\n\n // Headings for larger alerts\n h4 {\n margin-top: 0;\n // Specified for the h4 to prevent conflicts of changing @headings-color\n color: inherit;\n }\n\n // Provide class for links that match alerts\n .alert-link {\n font-weight: @alert-link-font-weight;\n }\n\n // Improve alignment and spacing of inner content\n > p,\n > ul {\n margin-bottom: 0;\n }\n\n > p + p {\n margin-top: 5px;\n }\n}\n\n// Dismissible alerts\n//\n// Expand the right padding and account for the close button's positioning.\n\n.alert-dismissable, // The misspelled .alert-dismissable was deprecated in 3.2.0.\n.alert-dismissible {\n padding-right: (@alert-padding + 20);\n\n // Adjust close link position\n .close {\n position: relative;\n top: -2px;\n right: -21px;\n color: inherit;\n }\n}\n\n// Alternate styles\n//\n// Generate contextual modifier classes for colorizing the alert.\n\n.alert-success {\n .alert-variant(@alert-success-bg; @alert-success-border; @alert-success-text);\n}\n\n.alert-info {\n .alert-variant(@alert-info-bg; @alert-info-border; @alert-info-text);\n}\n\n.alert-warning {\n .alert-variant(@alert-warning-bg; @alert-warning-border; @alert-warning-text);\n}\n\n.alert-danger {\n .alert-variant(@alert-danger-bg; @alert-danger-border; @alert-danger-text);\n}\n","// Alerts\n\n.alert-variant(@background; @border; @text-color) {\n background-color: @background;\n border-color: @border;\n color: @text-color;\n\n hr {\n border-top-color: darken(@border, 5%);\n }\n .alert-link {\n color: darken(@text-color, 10%);\n }\n}\n","//\n// Progress bars\n// --------------------------------------------------\n\n\n// Bar animations\n// -------------------------\n\n// WebKit\n@-webkit-keyframes progress-bar-stripes {\n from { background-position: 40px 0; }\n to { background-position: 0 0; }\n}\n\n// Spec and IE10+\n@keyframes progress-bar-stripes {\n from { background-position: 40px 0; }\n to { background-position: 0 0; }\n}\n\n\n// Bar itself\n// -------------------------\n\n// Outer container\n.progress {\n overflow: hidden;\n height: @line-height-computed;\n margin-bottom: @line-height-computed;\n background-color: @progress-bg;\n border-radius: @progress-border-radius;\n .box-shadow(inset 0 1px 2px rgba(0,0,0,.1));\n}\n\n// Bar of progress\n.progress-bar {\n float: left;\n width: 0%;\n height: 100%;\n font-size: @font-size-small;\n line-height: @line-height-computed;\n color: @progress-bar-color;\n text-align: center;\n background-color: @progress-bar-bg;\n .box-shadow(inset 0 -1px 0 rgba(0,0,0,.15));\n .transition(width .6s ease);\n}\n\n// Striped bars\n//\n// `.progress-striped .progress-bar` is deprecated as of v3.2.0 in favor of the\n// `.progress-bar-striped` class, which you just add to an existing\n// `.progress-bar`.\n.progress-striped .progress-bar,\n.progress-bar-striped {\n #gradient > .striped();\n background-size: 40px 40px;\n}\n\n// Call animation for the active one\n//\n// `.progress.active .progress-bar` is deprecated as of v3.2.0 in favor of the\n// `.progress-bar.active` approach.\n.progress.active .progress-bar,\n.progress-bar.active {\n .animation(progress-bar-stripes 2s linear infinite);\n}\n\n\n// Variations\n// -------------------------\n\n.progress-bar-success {\n .progress-bar-variant(@progress-bar-success-bg);\n}\n\n.progress-bar-info {\n .progress-bar-variant(@progress-bar-info-bg);\n}\n\n.progress-bar-warning {\n .progress-bar-variant(@progress-bar-warning-bg);\n}\n\n.progress-bar-danger {\n .progress-bar-variant(@progress-bar-danger-bg);\n}\n","// Gradients\n\n#gradient {\n\n // Horizontal gradient, from left to right\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .horizontal(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Opera 12\n background-image: linear-gradient(to right, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n background-repeat: repeat-x;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\",argb(@start-color),argb(@end-color))); // IE9 and down\n }\n\n // Vertical gradient, from top to bottom\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .vertical(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Opera 12\n background-image: linear-gradient(to bottom, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n background-repeat: repeat-x;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\",argb(@start-color),argb(@end-color))); // IE9 and down\n }\n\n .directional(@start-color: #555; @end-color: #333; @deg: 45deg) {\n background-repeat: repeat-x;\n background-image: -webkit-linear-gradient(@deg, @start-color, @end-color); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(@deg, @start-color, @end-color); // Opera 12\n background-image: linear-gradient(@deg, @start-color, @end-color); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n }\n .horizontal-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color);\n background-image: -o-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(to right, @start-color, @mid-color @color-stop, @end-color);\n background-repeat: no-repeat;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n }\n .vertical-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-image: -o-linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-repeat: no-repeat;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n }\n .radial(@inner-color: #555; @outer-color: #333) {\n background-image: -webkit-radial-gradient(circle, @inner-color, @outer-color);\n background-image: radial-gradient(circle, @inner-color, @outer-color);\n background-repeat: no-repeat;\n }\n .striped(@color: rgba(255,255,255,.15); @angle: 45deg) {\n background-image: -webkit-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n background-image: linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n }\n}\n","// Progress bars\n\n.progress-bar-variant(@color) {\n background-color: @color;\n\n // Deprecated parent class requirement as of v3.2.0\n .progress-striped & {\n #gradient > .striped();\n }\n}\n",".media {\n // Proper spacing between instances of .media\n margin-top: 15px;\n\n &:first-child {\n margin-top: 0;\n }\n}\n\n.media,\n.media-body {\n zoom: 1;\n overflow: hidden;\n}\n\n.media-body {\n width: 10000px;\n}\n\n.media-object {\n display: block;\n\n // Fix collapse in webkit from max-width: 100% and display: table-cell.\n &.img-thumbnail {\n max-width: none;\n }\n}\n\n.media-right,\n.media > .pull-right {\n padding-left: 10px;\n}\n\n.media-left,\n.media > .pull-left {\n padding-right: 10px;\n}\n\n.media-left,\n.media-right,\n.media-body {\n display: table-cell;\n vertical-align: top;\n}\n\n.media-middle {\n vertical-align: middle;\n}\n\n.media-bottom {\n vertical-align: bottom;\n}\n\n// Reset margins on headings for tighter default spacing\n.media-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n\n// Media list variation\n//\n// Undo default ul/ol styles\n.media-list {\n padding-left: 0;\n list-style: none;\n}\n","//\n// List groups\n// --------------------------------------------------\n\n\n// Base class\n//\n// Easily usable on