summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAzul <azul@riseup.net>2016-10-20 14:39:33 +0200
committerAzul <azul@riseup.net>2016-10-20 14:39:33 +0200
commitb97daaed9b513006ace7e8eb5232a2211e965e77 (patch)
treee27002e8368e92410e5d4af2a945260c2ea6e2d1
parentc6c4d9fd10b8ca8e24889112727e44c9bf68dd60 (diff)
parent6eb2dae802e5453e2a4361ab28f614cce9294f4c (diff)
Merge remote-tracking branch 'origin/develop'
We'll only use the master branch for development from now on.
-rw-r--r--.gitignore8
-rw-r--r--.gitlab-ci.yml32
-rw-r--r--.travis.yml11
-rw-r--r--Gemfile44
-rw-r--r--Gemfile.lock422
-rw-r--r--README.md59
-rw-r--r--app/assets/images/Avatar_Pic.pngbin0 -> 4298 bytes
-rw-r--r--app/assets/javascripts/application.js1
-rw-r--r--app/assets/javascripts/leap.js2
-rw-r--r--app/assets/javascripts/typeahead.bundle.js2451
-rw-r--r--app/assets/javascripts/users.js2
-rw-r--r--app/assets/stylesheets/application.scss5
-rw-r--r--app/assets/stylesheets/backport.scss24
-rw-r--r--app/assets/stylesheets/leap.scss23
-rw-r--r--app/assets/stylesheets/twitter.scss78
-rw-r--r--app/controllers/account_controller.rb19
-rw-r--r--app/controllers/account_settings_controller.rb0
-rw-r--r--app/controllers/api/certs_controller.rb (renamed from app/controllers/v1/certs_controller.rb)2
-rw-r--r--app/controllers/api/configs_controller.rb (renamed from app/controllers/v1/configs_controller.rb)14
-rw-r--r--app/controllers/api/identities_controller.rb (renamed from app/controllers/v1/identities_controller.rb)4
-rw-r--r--app/controllers/api/messages_controller.rb (renamed from app/controllers/v1/messages_controller.rb)2
-rw-r--r--app/controllers/api/services_controller.rb (renamed from app/controllers/v1/services_controller.rb)4
-rw-r--r--app/controllers/api/sessions_controller.rb (renamed from app/controllers/v1/sessions_controller.rb)3
-rw-r--r--app/controllers/api/smtp_certs_controller.rb (renamed from app/controllers/v1/smtp_certs_controller.rb)2
-rw-r--r--app/controllers/api/users_controller.rb (renamed from app/controllers/v1/users_controller.rb)19
-rw-r--r--app/controllers/api_controller.rb1
-rw-r--r--app/controllers/application_controller.rb13
-rw-r--r--app/controllers/controller_extension/fetch_user.rb2
-rw-r--r--app/controllers/controller_extension/json_file.rb23
-rw-r--r--app/controllers/errors_controller.rb2
-rw-r--r--app/controllers/home_controller.rb2
-rw-r--r--app/controllers/pages_controller.rb2
-rw-r--r--app/controllers/sessions_controller.rb3
-rw-r--r--app/controllers/static_config_controller.rb11
-rw-r--r--app/controllers/users_controller.rb28
-rw-r--r--app/helpers/application_helper.rb3
-rw-r--r--app/helpers/link_helper.rb4
-rw-r--r--app/helpers/navigation_helper.rb6
-rw-r--r--app/helpers/twitter_helper.rb88
-rw-r--r--app/models/account.rb10
-rw-r--r--app/models/api_monitor_user.rb11
-rw-r--r--app/models/api_user.rb13
-rw-r--r--app/models/identity.rb2
-rw-r--r--app/models/session.rb2
-rw-r--r--app/models/token.rb4
-rw-r--r--app/models/user.rb14
-rw-r--r--app/views/account/new.html.haml (renamed from app/views/users/new.html.haml)10
-rw-r--r--app/views/api/sessions/new.json.erb (renamed from app/views/v1/sessions/new.json.erb)0
-rw-r--r--app/views/common/_action_buttons.html.haml10
-rw-r--r--app/views/common/_download_button.html.haml8
-rw-r--r--app/views/common/_home_page_buttons.html.haml4
-rw-r--r--app/views/home/_content.html.haml43
-rw-r--r--app/views/home/_provider_message.html.haml2
-rw-r--r--app/views/layouts/_content.html.haml2
-rw-r--r--app/views/layouts/application.html.haml8
-rw-r--r--app/views/sessions/_warnings.html.haml (renamed from app/views/users/_warnings.html.haml)0
-rw-r--r--app/views/sessions/new.html.haml6
-rw-r--r--app/views/twitter/_index.html.erb34
-rw-r--r--app/views/users/_change_password.html.haml2
-rw-r--r--app/views/users/_change_pgp_key.html.haml2
-rw-r--r--app/views/users/_change_service_level.html.haml17
-rw-r--r--app/views/users/_contact_email.html.haml2
-rw-r--r--app/views/users/show.html.haml2
-rwxr-xr-xbin/bundle3
-rwxr-xr-xbin/rails4
-rwxr-xr-xbin/rake4
-rwxr-xr-xbin/setup29
-rw-r--r--config/application.rb60
-rw-r--r--config/boot.rb5
-rw-r--r--config/ci/gitlab/couchdb.yml3
-rw-r--r--config/ci/travis/README (renamed from test/config/README)0
-rw-r--r--config/ci/travis/couchdb.admin.yml (renamed from test/config/couchdb.admin.yml)3
-rw-r--r--config/ci/travis/couchdb.yml (renamed from test/config/couchdb.yml)1
-rwxr-xr-xconfig/ci/travis/setup_couch.sh (renamed from test/travis/setup_couch.sh)0
-rw-r--r--config/defaults.yml1
-rw-r--r--config/environment.rb6
-rw-r--r--config/environments/development.rb48
-rw-r--r--config/environments/production.rb70
-rw-r--r--config/environments/test.rb33
-rw-r--r--config/initializers/assets.rb1
-rw-r--r--config/initializers/client_side_validations.rb12
-rw-r--r--config/initializers/customization.rb6
-rw-r--r--config/initializers/error_constants.rb5
-rw-r--r--config/initializers/simple_form_bootstrap.rb12
-rw-r--r--config/initializers/validations.rb4
-rw-r--r--config/locales/en/generic.en.yml2
-rw-r--r--config/routes.rb16
-rw-r--r--doc/DEPLOY.md9
-rw-r--r--doc/DEVELOP.md41
-rw-r--r--doc/TROUBLESHOOT.md14
-rw-r--r--doc/TWITTER_FEED.md53
-rw-r--r--engines/billing/app/controllers/billing_admin_controller.rb1
-rw-r--r--engines/billing/app/controllers/billing_base_controller.rb2
-rw-r--r--engines/billing/app/controllers/payments_controller.rb2
-rw-r--r--engines/billing/app/controllers/subscriptions_controller.rb3
-rw-r--r--engines/billing/config/routes.rb33
-rw-r--r--engines/support/app/models/account_extension/tickets.rb13
-rw-r--r--engines/support/app/models/ticket.rb6
-rw-r--r--engines/support/app/views/tickets/_edit_form.html.haml8
-rw-r--r--engines/support/config/initializers/account_lifecycle.rb2
-rw-r--r--engines/support/config/locales/en.yml2
-rw-r--r--engines/support/config/routes.rb4
-rw-r--r--engines/support/lib/account_extension/tickets.rb15
-rw-r--r--engines/support/test/integration/create_ticket_test.rb2
-rw-r--r--engines/support/test/unit/ticket_test.rb6
-rw-r--r--features/1/anonymous.feature34
-rw-r--r--features/1/authentication.feature24
-rw-r--r--features/1/config.feature58
-rw-r--r--features/1/service.feature33
-rw-r--r--features/1/unauthenticated.feature31
-rw-r--r--features/anonymous.feature8
-rw-r--r--features/authentication.feature4
-rw-r--r--features/config.feature16
-rw-r--r--features/service.feature4
-rw-r--r--features/step_definitions/config_steps.rb9
-rw-r--r--features/support/hooks.rb4
-rw-r--r--features/unauthenticated.feature10
-rw-r--r--lib/email.rb (renamed from app/models/email.rb)0
-rw-r--r--lib/extensions/couchrest.rb4
-rw-r--r--lib/gemfile_tools.rb4
-rw-r--r--lib/local_email.rb (renamed from app/models/local_email.rb)1
-rw-r--r--lib/login_format_validation.rb (renamed from app/models/login_format_validation.rb)0
-rw-r--r--lib/tasks/i18n.rake2
-rw-r--r--lib/tasks/test.rake15
-rw-r--r--lib/temporary_user.rb (renamed from app/models/temporary_user.rb)32
-rwxr-xr-xscript/generate_bearer_token86
-rwxr-xr-xscript/invalidate_bearer_token47
-rw-r--r--test/functional/account_controller_test.rb26
-rw-r--r--test/functional/api/certs_controller_test.rb (renamed from test/functional/v1/certs_controller_test.rb)22
-rw-r--r--test/functional/api/identities_controller_test.rb (renamed from test/functional/v1/identities_controller_test.rb)8
-rw-r--r--test/functional/api/messages_controller_test.rb (renamed from test/functional/v1/messages_controller_test.rb)15
-rw-r--r--test/functional/api/services_controller_test.rb (renamed from test/functional/v1/services_controller_test.rb)8
-rw-r--r--test/functional/api/sessions_controller_test.rb (renamed from test/functional/v1/sessions_controller_test.rb)15
-rw-r--r--test/functional/api/smtp_certs_controller_test.rb (renamed from test/functional/v1/smtp_certs_controller_test.rb)10
-rw-r--r--test/functional/api/token_auth_test.rb (renamed from test/functional/token_auth_test.rb)12
-rw-r--r--test/functional/api/users_controller_test.rb (renamed from test/functional/v1/users_controller_test.rb)52
-rw-r--r--test/functional/home_controller_test.rb16
-rw-r--r--test/functional/static_config_controller_test.rb6
-rw-r--r--test/functional/users_controller_test.rb24
-rw-r--r--test/integration/api/cert_test.rb11
-rw-r--r--test/integration/api/signup_test.rb2
-rw-r--r--test/integration/api/smtp_cert_test.rb26
-rw-r--r--test/integration/api/srp_test.rb18
-rw-r--r--test/integration/api/token_auth_test.rb (renamed from test/integration/api/token_test.rb)4
-rw-r--r--test/integration/api/update_account_test.rb2
-rw-r--r--test/integration/browser/account_livecycle_test.rb114
-rw-r--r--test/integration/browser/account_livecycle_test.rb.orig (renamed from test/integration/browser/account_test.rb)33
-rw-r--r--test/integration/browser/admin_test.rb18
-rw-r--r--test/integration/browser/password_validation_test.rb8
-rw-r--r--test/integration/browser/security_test.rb52
-rw-r--r--test/integration/locale_path_test.rb7
-rw-r--r--test/integration/navigation_test.rb9
-rw-r--r--test/leap_web_users_test.rb7
-rw-r--r--test/performance/browsing_test.rb12
-rw-r--r--test/support/api_controller_test.rb29
-rw-r--r--test/support/api_integration_test.rb14
-rw-r--r--test/support/assert_responses.rb13
-rw-r--r--test/support/browser_integration_test.rb40
-rw-r--r--test/support/record_assertions.rb10
-rw-r--r--test/test_helper.rb6
-rw-r--r--test/travis/ruby-version1
-rw-r--r--test/unit/email_test.rb1
-rw-r--r--test/unit/identity_test.rb9
-rw-r--r--test/unit/invite_code_validator_test.rb14
-rw-r--r--test/unit/temporary_user_test.rb (renamed from test/unit/tmp_user_test.rb)30
-rw-r--r--test/unit/user_test.rb7
166 files changed, 4412 insertions, 896 deletions
diff --git a/.gitignore b/.gitignore
index 3aa8fc7..f898d47 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,13 @@
+# If you find yourself ignoring temporary files generated by your text editor
+# or operating system, you probably want to add a global ignore instead:
+# git config --global core.excludesfile '~/.gitignore_global'
+
# Ignore bundler config
/.bundle
-bin
# Ignore the default SQLite database.
/db/*.sqlite3
+/db/*.sqlite3-journal
# Ignore all logfiles and tempfiles.
/log/*.log
@@ -12,7 +16,6 @@ bin
/pkg
/*/pkg
/log
-.*.swp
*/Gemfile.lock
test/dummy/log/*
test/dummy/tmp/*
@@ -31,6 +34,7 @@ public/ca.crt
public/config/*
public/provider.json
config/config.yml
+config/secrets.yml
public/1/*
vendor/bundle/*
public/img
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..45b5fd6
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,32 @@
+# This file is a template, and might need editing before it works on your project.
+# Official language image. Look for the different tagged releases at:
+# https://hub.docker.com/r/library/ruby/tags/
+image: "ruby:2.1"
+
+# Pick zero or more services to be used on all builds.
+# Only needed when using a docker container to run your tests in.
+# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service
+services:
+ - couchdb:1.6.1
+
+# Cache gems in between builds
+cache:
+ paths:
+ - vendor/
+
+# This is a basic example for a gem or script which doesn't use
+# services such as redis or postgres
+before_script:
+ - ruby -v
+ - curl -s couchdb:5984
+ - git submodule update --init
+ - cp config/ci/gitlab/couchdb.yml config/couchdb.admin.yml
+ - cp config/ci/gitlab/couchdb.yml config
+ - gem install bundler --no-ri --no-rdoc
+ - bundle install -j $(nproc) --path vendor --without development debug
+ - bundle exec rake RAILS_ENV=test db:rotate
+ - bundle exec rake RAILS_ENV=test db:migrate
+
+rails:
+ script:
+ - bundle exec rake test
diff --git a/.travis.yml b/.travis.yml
index 4e7aad0..fbc4e06 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,15 +1,12 @@
+sudo: false
services:
- couchdb
notifications:
email: false
-before_install:
- - "gem install bundler --version 1.11.2"
before_script:
- - "rm .ruby-version"
- - "mv test/travis/ruby-version .ruby-version"
- - "test/travis/setup_couch.sh"
- - "mv test/config/couchdb.admin.yml config/couchdb.admin.yml"
- - "mv test/config/couchdb.yml config/couchdb.yml"
+ - "config/ci/travis/setup_couch.sh"
+ - "mv config/ci/travis/couchdb.admin.yml config/couchdb.admin.yml"
+ - "mv config/ci/travis/couchdb.yml config/couchdb.yml"
- "bundle exec rake RAILS_ENV=test db:rotate"
- "bundle exec rake RAILS_ENV=test db:migrate"
after_script:
diff --git a/Gemfile b/Gemfile
index be05269..3b5435e 100644
--- a/Gemfile
+++ b/Gemfile
@@ -4,12 +4,14 @@ require File.expand_path('../lib/gemfile_tools.rb', __FILE__)
## CORE
# rake 11.x throws lots of warnings about rails 3.2 code
-gem "rake", "~> 10.4"
-gem "rails", "~> 3.2.22.4"
-gem "couchrest", "~> 1.1.3"
-gem "couchrest_model", "~> 2.0.0"
+gem "rake"
+gem "rails", "~> 4.2.7"
+# TODO: drop this and the respond_with usage
+gem 'responders', '~> 2.0'
+gem "couchrest", "~> 2.0.0.rc3"
+gem "couchrest_model", "~> 2.1.0.beta2"
if ARGV.grep(/assets:precompile/).empty?
- gem "couchrest_session_store", "= 0.3.1"
+ gem "couchrest_session_store", "~> 0.4.2"
end
## AUTHENTICATION
@@ -30,8 +32,7 @@ gem 'rails-i18n' # locale files for built-in validation messages and times
gem 'common_languages', :path => 'vendor/gems/common_languages'
## VIEWS
-gem 'kaminari', "0.13.0" # for pagination. trying 0.13.0 as there seem to be
- # issues with 0.14.0 when using couchrest
+gem 'kaminari'
gem 'rdiscount' # for rendering .md templates
## ASSETS
@@ -39,19 +40,12 @@ gem "jquery-rails"
gem "simple_form"
gem 'client_side_validations'
gem 'client_side_validations-simple_form'
-gem "haml-rails", "= 0.4.0" # The last version of haml-rails to support Rails 3.
-gem "bootstrap-sass", "= 2.3.2.2" # The last 2.x version. Bootstrap-sass versions
- # tracks the version of Bootstrap. We currently require
- # Bootstrap v2 because client side validations is incompatible
- # with Bootstrap v3. When upgrading to Rails 4, see
- # https://github.com/twbs/bootstrap-sass
-gem "sass-rails", "~> 3.2.5" # Only version supported by bootstrap-sass 2.3.2.2
-gem 'quiet_assets' # stops logging all the asset requests
+gem "haml-rails"
+gem "bootstrap-sass"
+gem "sass-rails"
group :production do
- gem "uglifier", "~> 1.2.7" # javascript compression https://github.com/lautis/uglifier
- # this must not be included in development mode, or js
- # will get included twice.
- gem 'therubyracer', "~> 0.12.2", :platforms => :ruby
+ gem "uglifier"
+ gem 'therubyracer', :platforms => :ruby
# ^^ See https://github.com/sstephenson/execjs#readme
# for list of supported runtimes.
end
@@ -68,7 +62,7 @@ group :test do
gem 'phantomjs-binaries' # binaries specific to the os
# moching and stubbing
- gem 'mocha', '~> 0.13.0', :require => false
+ gem 'mocha', :require => false
gem 'minitest-stub-const' # why?
# generating test data
@@ -83,7 +77,6 @@ group :test do
end
group :test, :development do
- gem 'thin'
gem 'i18n-missing_translations'
gem 'pry'
end
@@ -93,22 +86,19 @@ group :production do
end
group :development do
- gem "better_errors", '1.1.0'
+ gem "better_errors"
gem "binding_of_caller"
end
group :test, :debug do
# bundler on jessie doesn't support `:platforms => :ruby_21`
- if RUBY_VERSION < "2.0"
- gem 'debugger'
- else
- gem 'byebug'
- end
+ gem 'byebug'
end
##
## OPTIONAL GEMS AND ENGINES
##
+gem 'twitter'
enabled_engines.each do |name, gem_info|
gem gem_info[:name], :path => gem_info[:path], :groups => gem_info[:env]
diff --git a/Gemfile.lock b/Gemfile.lock
index 0b692ba..ca0cb63 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -26,236 +26,302 @@ GEM
remote: https://rubygems.org/
specs:
SyslogLogger (2.0)
- actionmailer (3.2.22.4)
- actionpack (= 3.2.22.4)
- mail (~> 2.5.4)
- actionpack (3.2.22.4)
- activemodel (= 3.2.22.4)
- activesupport (= 3.2.22.4)
- builder (~> 3.0.0)
+ actionmailer (4.2.7.1)
+ actionpack (= 4.2.7.1)
+ actionview (= 4.2.7.1)
+ activejob (= 4.2.7.1)
+ mail (~> 2.5, >= 2.5.4)
+ rails-dom-testing (~> 1.0, >= 1.0.5)
+ actionpack (4.2.7.1)
+ actionview (= 4.2.7.1)
+ activesupport (= 4.2.7.1)
+ rack (~> 1.6)
+ rack-test (~> 0.6.2)
+ rails-dom-testing (~> 1.0, >= 1.0.5)
+ rails-html-sanitizer (~> 1.0, >= 1.0.2)
+ actionview (4.2.7.1)
+ activesupport (= 4.2.7.1)
+ builder (~> 3.1)
erubis (~> 2.7.0)
- journey (~> 1.0.4)
- rack (~> 1.4.5)
- rack-cache (~> 1.2)
- rack-test (~> 0.6.1)
- sprockets (~> 2.2.1)
- activemodel (3.2.22.4)
- activesupport (= 3.2.22.4)
- builder (~> 3.0.0)
- activerecord (3.2.22.4)
- activemodel (= 3.2.22.4)
- activesupport (= 3.2.22.4)
- arel (~> 3.0.2)
- tzinfo (~> 0.3.29)
- activeresource (3.2.22.4)
- activemodel (= 3.2.22.4)
- activesupport (= 3.2.22.4)
- activesupport (3.2.22.4)
- i18n (~> 0.6, >= 0.6.4)
- multi_json (~> 1.0)
- addressable (2.3.6)
- arel (3.0.3)
- better_errors (1.1.0)
+ rails-dom-testing (~> 1.0, >= 1.0.5)
+ rails-html-sanitizer (~> 1.0, >= 1.0.2)
+ activejob (4.2.7.1)
+ activesupport (= 4.2.7.1)
+ globalid (>= 0.3.0)
+ activemodel (4.2.7.1)
+ activesupport (= 4.2.7.1)
+ builder (~> 3.1)
+ activerecord (4.2.7.1)
+ activemodel (= 4.2.7.1)
+ activesupport (= 4.2.7.1)
+ arel (~> 6.0)
+ activesupport (4.2.7.1)
+ i18n (~> 0.7)
+ json (~> 1.7, >= 1.7.7)
+ minitest (~> 5.1)
+ thread_safe (~> 0.3, >= 0.3.4)
+ tzinfo (~> 1.1)
+ addressable (2.4.0)
+ arel (6.0.3)
+ autoprefixer-rails (6.4.0.2)
+ execjs
+ better_errors (2.1.1)
coderay (>= 1.0.0)
erubis (>= 2.6.6)
+ rack (>= 0.9.0)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
- bootstrap-sass (2.3.2.2)
- sass (~> 3.2)
- braintree (2.48.1)
+ bootstrap-sass (3.3.7)
+ autoprefixer-rails (>= 5.2.1)
+ sass (>= 3.3.4)
+ braintree (2.65.0)
builder (>= 2.0.0)
- builder (3.0.4)
- byebug (8.2.1)
- capybara (2.4.4)
+ buftok (0.2.0)
+ builder (3.2.2)
+ byebug (9.0.5)
+ capybara (2.7.1)
+ addressable
mime-types (>= 1.16)
nokogiri (>= 1.3.3)
rack (>= 1.0.0)
rack-test (>= 0.5.4)
xpath (~> 2.0)
- client_side_validations (3.2.6)
- client_side_validations-simple_form (2.1.0)
- client_side_validations (~> 3.2.5)
- simple_form (~> 2.1.0)
+ client_side_validations (4.2.5)
+ jquery-rails (>= 3.1.2, < 5.0.0)
+ js_regex (~> 1.0, >= 1.0.9)
+ rails (>= 4.0.0, < 4.3.0)
+ client_side_validations-simple_form (3.2.4)
+ client_side_validations (~> 4.2.3)
+ simple_form (~> 3.2)
cliver (0.3.2)
- coderay (1.1.0)
- couchrest (1.1.3)
- mime-types (~> 1.15)
- multi_json (~> 1.0)
- rest-client (~> 1.6.1)
- couchrest_model (2.0.1)
- activemodel (>= 3.0)
- couchrest (~> 1.1.3)
+ coderay (1.1.1)
+ concurrent-ruby (1.0.2)
+ couchrest (2.0.0)
+ httpclient (~> 2.8)
mime-types (>= 1.15)
+ multi_json (~> 1.7)
+ couchrest_model (2.1.0.rc1)
+ activemodel (~> 4.0)
+ couchrest (= 2.0.0)
tzinfo (>= 0.3.22)
- couchrest_session_store (0.3.1)
- actionpack (~> 3.0)
- couchrest
- couchrest_model
- cucumber (1.3.17)
+ couchrest_session_store (0.4.2)
+ actionpack (~> 4.0)
+ couchrest (~> 2.0.0.rc3)
+ couchrest_model (~> 2.1.0.beta2)
+ cucumber (2.4.0)
builder (>= 2.1.2)
+ cucumber-core (~> 1.5.0)
+ cucumber-wire (~> 0.0.1)
diff-lcs (>= 1.1.3)
- gherkin (~> 2.12)
+ gherkin (~> 4.0)
multi_json (>= 1.7.5, < 2.0)
- multi_test (>= 0.1.1)
- cucumber-rails (1.4.2)
+ multi_test (>= 0.1.2)
+ cucumber-core (1.5.0)
+ gherkin (~> 4.0)
+ cucumber-rails (1.4.4)
capybara (>= 1.1.2, < 3)
- cucumber (>= 1.3.8, < 2)
- mime-types (>= 1.16, < 3)
+ cucumber (>= 1.3.8, < 3)
+ mime-types (>= 1.16, < 4)
nokogiri (~> 1.5)
- rails (>= 3, < 5)
- daemons (1.1.9)
+ railties (>= 3, < 5.1)
+ cucumber-wire (0.0.1)
debug_inspector (0.0.2)
diff-lcs (1.2.5)
+ domain_name (0.5.20160615)
+ unf (>= 0.0.5, < 1.0.0)
+ equalizer (0.0.10)
erubis (2.7.0)
- eventmachine (1.0.3)
- execjs (2.2.2)
- factory_girl (4.5.0)
+ execjs (2.7.0)
+ factory_girl (4.7.0)
activesupport (>= 3.0.0)
- factory_girl_rails (4.5.0)
- factory_girl (~> 4.5.0)
+ factory_girl_rails (4.7.0)
+ factory_girl (~> 4.7.0)
railties (>= 3.0.0)
- fake_braintree (0.7.0)
+ fake_braintree (0.8.0)
activesupport
braintree (~> 2.32)
- capybara (>= 2.0.3)
+ capybara (>= 2.2.0)
sinatra
- faker (1.4.3)
+ faker (1.6.6)
i18n (~> 0.5)
- ffi (1.9.6)
- gherkin (2.12.2)
- multi_json (~> 1.3)
- haml (4.0.6)
+ faraday (0.9.2)
+ multipart-post (>= 1.2, < 3)
+ ffi (1.9.14)
+ gherkin (4.0.0)
+ globalid (0.3.7)
+ activesupport (>= 4.1.0)
+ haml (4.0.7)
tilt
- haml-rails (0.4)
- actionpack (>= 3.1, < 4.1)
- activesupport (>= 3.1, < 4.1)
- haml (>= 3.1, < 4.1)
- railties (>= 3.1, < 4.1)
- hike (1.2.3)
- http_accept_language (2.0.2)
+ haml-rails (0.9.0)
+ actionpack (>= 4.0.1)
+ activesupport (>= 4.0.1)
+ haml (>= 4.0.6, < 5.0)
+ html2haml (>= 1.0.1)
+ railties (>= 4.0.1)
+ html2haml (2.0.0)
+ erubis (~> 2.7.0)
+ haml (~> 4.0.0)
+ nokogiri (~> 1.6.0)
+ ruby_parser (~> 3.5)
+ http (1.0.4)
+ addressable (~> 2.3)
+ http-cookie (~> 1.0)
+ http-form_data (~> 1.0.1)
+ http_parser.rb (~> 0.6.0)
+ http-cookie (1.0.2)
+ domain_name (~> 0.5)
+ http-form_data (1.0.1)
+ http_accept_language (2.0.5)
+ http_parser.rb (0.6.0)
+ httpclient (2.8.1)
i18n (0.7.0)
i18n-missing_translations (0.0.1)
- journey (1.0.4)
- jquery-rails (3.1.2)
- railties (>= 3.0, < 5.0)
+ jquery-rails (4.1.1)
+ rails-dom-testing (>= 1, < 3)
+ railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
+ js_regex (1.0.17)
+ regexp_parser (= 0.3.6)
json (1.8.3)
- kaminari (0.13.0)
+ kaminari (0.17.0)
actionpack (>= 3.0.0)
activesupport (>= 3.0.0)
- railties (>= 3.0.0)
launchy (2.4.3)
addressable (~> 2.3)
- libv8 (3.16.14.11)
- mail (2.5.4)
- mime-types (~> 1.16)
- treetop (~> 1.4.8)
+ libv8 (3.16.14.15)
+ loofah (2.0.3)
+ nokogiri (>= 1.5.9)
+ mail (2.6.4)
+ mime-types (>= 1.16, < 4)
+ memoizable (0.4.2)
+ thread_safe (~> 0.3, >= 0.3.1)
metaclass (0.0.4)
method_source (0.8.2)
- mime-types (1.25.1)
- mini_portile (0.6.1)
- minitest-stub-const (0.2)
- mocha (0.13.3)
+ mime-types (3.1)
+ mime-types-data (~> 3.2015)
+ mime-types-data (3.2016.0521)
+ mini_portile2 (2.1.0)
+ minitest (5.9.0)
+ minitest-stub-const (0.5)
+ mocha (1.1.0)
metaclass (~> 0.0.1)
multi_json (1.12.1)
- multi_test (0.1.1)
- nokogiri (1.6.5)
- mini_portile (~> 0.6.0)
- phantomjs-binaries (1.9.2.4)
+ multi_test (0.1.2)
+ multipart-post (2.0.0)
+ naught (1.1.0)
+ nokogiri (1.6.8)
+ mini_portile2 (~> 2.1.0)
+ pkg-config (~> 1.1.7)
+ phantomjs-binaries (2.1.1.0)
sys-uname (= 0.9.0)
- poltergeist (1.5.1)
+ pkg-config (1.1.7)
+ poltergeist (1.10.0)
capybara (~> 2.1)
cliver (~> 0.3.1)
- multi_json (~> 1.0)
websocket-driver (>= 0.2.0)
- polyglot (0.3.5)
- pry (0.10.1)
+ pry (0.10.4)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
slop (~> 3.4)
- quiet_assets (1.0.3)
- railties (>= 3.1, < 5.0)
- rack (1.4.7)
- rack-cache (1.6.1)
- rack (>= 0.4)
+ rack (1.6.4)
rack-protection (1.5.3)
rack
- rack-ssl (1.3.4)
- rack
rack-test (0.6.3)
rack (>= 1.0)
- rails (3.2.22.4)
- actionmailer (= 3.2.22.4)
- actionpack (= 3.2.22.4)
- activerecord (= 3.2.22.4)
- activeresource (= 3.2.22.4)
- activesupport (= 3.2.22.4)
- bundler (~> 1.0)
- railties (= 3.2.22.4)
- rails-i18n (3.0.1)
- i18n (~> 0.5)
- rails (>= 3.0.0, < 4.0.0)
+ rails (4.2.7.1)
+ actionmailer (= 4.2.7.1)
+ actionpack (= 4.2.7.1)
+ actionview (= 4.2.7.1)
+ activejob (= 4.2.7.1)
+ activemodel (= 4.2.7.1)
+ activerecord (= 4.2.7.1)
+ activesupport (= 4.2.7.1)
+ bundler (>= 1.3.0, < 2.0)
+ railties (= 4.2.7.1)
+ sprockets-rails
+ rails-deprecated_sanitizer (1.0.3)
+ activesupport (>= 4.2.0.alpha)
+ rails-dom-testing (1.0.7)
+ activesupport (>= 4.2.0.beta, < 5.0)
+ nokogiri (~> 1.6.0)
+ rails-deprecated_sanitizer (>= 1.0.1)
+ rails-html-sanitizer (1.0.3)
+ loofah (~> 2.0)
+ rails-i18n (4.0.9)
+ i18n (~> 0.7)
+ railties (~> 4.0)
rails_warden (0.5.8)
warden (>= 1.0.0)
- railties (3.2.22.4)
- actionpack (= 3.2.22.4)
- activesupport (= 3.2.22.4)
- rack-ssl (~> 1.3.2)
+ railties (4.2.7.1)
+ actionpack (= 4.2.7.1)
+ activesupport (= 4.2.7.1)
rake (>= 0.8.7)
- rdoc (~> 3.4)
- thor (>= 0.14.6, < 2.0)
- rake (10.5.0)
- rdiscount (2.1.7.1)
- rdoc (3.12.2)
- json (~> 1.4)
+ thor (>= 0.18.1, < 2.0)
+ rake (11.2.2)
+ rdiscount (2.2.0.1)
ref (2.0.0)
- rest-client (1.6.9)
- mime-types (~> 1.16)
+ regexp_parser (0.3.6)
+ responders (2.2.0)
+ railties (>= 4.2.0, < 5.1)
ruby-srp (0.2.1)
- sass (3.4.9)
- sass-rails (3.2.6)
- railties (~> 3.2.0)
- sass (>= 3.1.10)
- tilt (~> 1.3)
- simple_form (2.1.2)
- actionpack (~> 3.0)
- activemodel (~> 3.0)
- sinatra (1.4.5)
- rack (~> 1.4)
+ ruby_parser (3.8.2)
+ sexp_processor (~> 4.1)
+ sass (3.4.22)
+ sass-rails (5.0.6)
+ railties (>= 4.0.0, < 6)
+ sass (~> 3.1)
+ sprockets (>= 2.8, < 4.0)
+ sprockets-rails (>= 2.0, < 4.0)
+ tilt (>= 1.1, < 3)
+ sexp_processor (4.7.0)
+ simple_form (3.2.1)
+ actionpack (> 4, < 5.1)
+ activemodel (> 4, < 5.1)
+ simple_oauth (0.3.1)
+ sinatra (1.4.7)
+ rack (~> 1.5)
rack-protection (~> 1.4)
- tilt (~> 1.3, >= 1.3.4)
+ tilt (>= 1.3, < 3)
slop (3.6.0)
- sprockets (2.2.3)
- hike (~> 1.2)
- multi_json (~> 1.0)
- rack (~> 1.0)
- tilt (~> 1.1, != 1.3.0)
+ sprockets (3.7.0)
+ concurrent-ruby (~> 1.0)
+ rack (> 1, < 3)
+ sprockets-rails (3.1.1)
+ actionpack (>= 4.0)
+ activesupport (>= 4.0)
+ sprockets (>= 3.0.0)
sys-uname (0.9.0)
ffi (>= 1.0.0)
therubyracer (0.12.2)
libv8 (~> 3.16.14.0)
ref
- thin (1.6.3)
- daemons (~> 1.0, >= 1.0.9)
- eventmachine (~> 1.0)
- rack (~> 1.0)
thor (0.19.1)
- tilt (1.4.1)
- treetop (1.4.15)
- polyglot
- polyglot (>= 0.3.1)
- tzinfo (0.3.51)
- uglifier (1.2.7)
- execjs (>= 0.3.0)
- multi_json (~> 1.3)
- valid_email (0.0.7)
+ thread_safe (0.3.5)
+ tilt (2.0.5)
+ twitter (5.16.0)
+ addressable (~> 2.3)
+ buftok (~> 0.2.0)
+ equalizer (= 0.0.10)
+ faraday (~> 0.9.0)
+ http (~> 1.0)
+ http_parser.rb (~> 0.6.0)
+ json (~> 1.8)
+ memoizable (~> 0.4.0)
+ naught (~> 1.0)
+ simple_oauth (~> 0.3.0)
+ tzinfo (1.2.2)
+ thread_safe (~> 0.1)
+ uglifier (3.0.1)
+ execjs (>= 0.3.0, < 3)
+ unf (0.2.0.beta2)
+ valid_email (0.0.13)
activemodel
- mail
- warden (1.2.3)
+ mail (~> 2.6.1)
+ warden (1.2.6)
rack (>= 1.0)
- websocket-driver (0.5.1)
+ websocket-driver (0.6.4)
websocket-extensions (>= 0.1.0)
- websocket-extensions (0.1.1)
+ websocket-extensions (0.1.2)
xpath (2.0.0)
nokogiri (~> 1.3)
@@ -264,48 +330,48 @@ PLATFORMS
DEPENDENCIES
SyslogLogger (~> 2.0)
- better_errors (= 1.1.0)
+ better_errors
binding_of_caller
- bootstrap-sass (= 2.3.2.2)
+ bootstrap-sass
byebug
capybara
certificate_authority!
client_side_validations
client_side_validations-simple_form
common_languages!
- couchrest (~> 1.1.3)
- couchrest_model (~> 2.0.0)
- couchrest_session_store (= 0.3.1)
+ couchrest (~> 2.0.0.rc3)
+ couchrest_model (~> 2.1.0.beta2)
+ couchrest_session_store (~> 0.4.2)
cucumber-rails
factory_girl_rails
fake_braintree
faker
- haml-rails (= 0.4.0)
+ haml-rails
http_accept_language
i18n-missing_translations
jquery-rails
- kaminari (= 0.13.0)
+ kaminari
launchy
leap_web_billing!
leap_web_help!
minitest-stub-const
- mocha (~> 0.13.0)
+ mocha
phantomjs-binaries
poltergeist
pry
- quiet_assets
- rails (~> 3.2.22.4)
+ rails (~> 4.2.7)
rails-i18n
rails_warden
- rake (~> 10.4)
+ rake
rdiscount
+ responders (~> 2.0)
ruby-srp (~> 0.2.1)
- sass-rails (~> 3.2.5)
+ sass-rails
simple_form
- therubyracer (~> 0.12.2)
- thin
- uglifier (~> 1.2.7)
+ therubyracer
+ twitter
+ uglifier
valid_email
BUNDLED WITH
- 1.11.2
+ 1.12.5
diff --git a/README.md b/README.md
index b6f3d1b..923b239 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,7 @@ The LEAP Web App provides the following functions:
* Webfinger access to user’s public keys
* Email aliases and forwarding
* Localized and Customizable documentation
+* Display of status updates from Twitter (access to tweets via Twitter API)
Written in: Ruby, Rails.
@@ -36,6 +37,7 @@ For more information, see these files in the ``doc`` directory:
* DEPLOY -- for notes on deployment.
* DEVELOP -- for developer notes.
* CUSTOM -- how to customize.
+* TWITTER_FEED -- how to use it.
External docs:
@@ -46,20 +48,6 @@ External docs:
* Overview of the main code repositories
* Ideas for discrete, unclaimed development projects that would greatly benefit the LEAP ecosystem.
-Known problems
----------------------------
-
-* Client certificates are generated without a CSR. The problem is that
- this makes the web application extremely vulnerable to denial of
- service attacks. This is not an issue unless the provider enables the
- possibility of anonymously fetching a client certificate without
- authenticating first.
-
-* By its very nature, the user database is vulnerable to enumeration
- attacks. These are very hard to prevent, because our protocol is
- designed to allow query of a user database via proxy in order to
- provide network perspective.
-
Installation
---------------------------
@@ -69,17 +57,37 @@ these instructions:
### Install system requirements
+You'll need git, ruby (2.1.5), couchdb and bundler installed.
+On a recent debian based distribution run
+
sudo apt install git ruby couchdb bundler
-Your actual requirements might differ if you are running an older OS that defaults to ruby 1.9.
+For other operation systems please lookup the install instructions of these
+tools.
### Download source
+We host our own git repository. In order to create a local clone run
+
git clone --recursive git://leap.se/leap_web
+ cd leap_web
+
+The repo is mirrored on github and we accept pull requests there:
+
+ https://github.com/leapcode/leap_web
+
+### Pick branch (development only)
+
+We use the master branch for the stable version deployed to production.
+Development usually happens on the develop branch. So for development you
+want to run
+
+ git checkout origin/develop -b develop
+
+This will create a local branch called develop based on our develop branch.
### Install required ruby libraries
- cd leap_web
bundle --binstubs
Typically, you run ``bundle`` as a normal user and it will ask you for a
@@ -88,13 +96,13 @@ have sudo, run ``bundle`` as root.
### Installation for development purposes
-Please see `doc/DEVELOP.md` for further required steps when installing
+Please see `doc/DEVELOP.md` for details about installing
leap_web for development purposes.
-Configuration
+Configuration for Production
----------------------------
-The configuration file `config/defaults.yml` providers good defaults for
+The configuration file `config/defaults.yml` provides good defaults for
most values. You can override these defaults by creating a file
`config/config.yml`.
@@ -167,3 +175,16 @@ To run an individual test:
or
ruby -Itest certs/test/unit/client_certificate_test.rb
+Known problems
+---------------------------
+
+* Client certificates are generated without a CSR. The problem is that
+ this makes the web application extremely vulnerable to denial of
+ service attacks. This is not an issue unless the provider enables the
+ possibility of anonymously fetching a client certificate without
+ authenticating first.
+
+* By its very nature, the user database is vulnerable to enumeration
+ attacks. These are very hard to prevent, because our protocol is
+ designed to allow query of a user database via proxy in order to
+ provide network perspective.
diff --git a/app/assets/images/Avatar_Pic.png b/app/assets/images/Avatar_Pic.png
new file mode 100644
index 0000000..b5eebc8
--- /dev/null
+++ b/app/assets/images/Avatar_Pic.png
Binary files differ
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 9af373d..7888161 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -16,6 +16,7 @@
//= require bootstrap
//= require rails.validations
//= require rails.validations.simple_form
+//= require typeahead.bundle
//= require leap
//= require platform
//= require tickets
diff --git a/app/assets/javascripts/leap.js b/app/assets/javascripts/leap.js
index 94e602d..c8fbcf5 100644
--- a/app/assets/javascripts/leap.js
+++ b/app/assets/javascripts/leap.js
@@ -5,3 +5,5 @@
function alert_message(msg) {
$('#messages').append('<div class="alert alert-error"><a class="close" data-dismiss="alert">×</a><span>'+msg+'</span></div>');
}
+
+ClientSideValidations.formBuilders['SimpleForm::FormBuilder'].wrappers.bootstrap = ClientSideValidations.formBuilders['SimpleForm::FormBuilder'].wrappers["default"];
diff --git a/app/assets/javascripts/typeahead.bundle.js b/app/assets/javascripts/typeahead.bundle.js
new file mode 100644
index 0000000..bb0c8ae
--- /dev/null
+++ b/app/assets/javascripts/typeahead.bundle.js
@@ -0,0 +1,2451 @@
+/*!
+ * typeahead.js 0.11.1
+ * https://github.com/twitter/typeahead.js
+ * Copyright 2013-2015 Twitter, Inc. and other contributors; Licensed MIT
+ */
+
+(function(root, factory) {
+ if (typeof define === "function" && define.amd) {
+ define("bloodhound", [ "jquery" ], function(a0) {
+ return root["Bloodhound"] = factory(a0);
+ });
+ } else if (typeof exports === "object") {
+ module.exports = factory(require("jquery"));
+ } else {
+ root["Bloodhound"] = factory(jQuery);
+ }
+})(this, function($) {
+ var _ = function() {
+ "use strict";
+ return {
+ isMsie: function() {
+ return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false;
+ },
+ isBlankString: function(str) {
+ return !str || /^\s*$/.test(str);
+ },
+ escapeRegExChars: function(str) {
+ return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
+ },
+ isString: function(obj) {
+ return typeof obj === "string";
+ },
+ isNumber: function(obj) {
+ return typeof obj === "number";
+ },
+ isArray: $.isArray,
+ isFunction: $.isFunction,
+ isObject: $.isPlainObject,
+ isUndefined: function(obj) {
+ return typeof obj === "undefined";
+ },
+ isElement: function(obj) {
+ return !!(obj && obj.nodeType === 1);
+ },
+ isJQuery: function(obj) {
+ return obj instanceof $;
+ },
+ toStr: function toStr(s) {
+ return _.isUndefined(s) || s === null ? "" : s + "";
+ },
+ bind: $.proxy,
+ each: function(collection, cb) {
+ $.each(collection, reverseArgs);
+ function reverseArgs(index, value) {
+ return cb(value, index);
+ }
+ },
+ map: $.map,
+ filter: $.grep,
+ every: function(obj, test) {
+ var result = true;
+ if (!obj) {
+ return result;
+ }
+ $.each(obj, function(key, val) {
+ if (!(result = test.call(null, val, key, obj))) {
+ return false;
+ }
+ });
+ return !!result;
+ },
+ some: function(obj, test) {
+ var result = false;
+ if (!obj) {
+ return result;
+ }
+ $.each(obj, function(key, val) {
+ if (result = test.call(null, val, key, obj)) {
+ return false;
+ }
+ });
+ return !!result;
+ },
+ mixin: $.extend,
+ identity: function(x) {
+ return x;
+ },
+ clone: function(obj) {
+ return $.extend(true, {}, obj);
+ },
+ getIdGenerator: function() {
+ var counter = 0;
+ return function() {
+ return counter++;
+ };
+ },
+ templatify: function templatify(obj) {
+ return $.isFunction(obj) ? obj : template;
+ function template() {
+ return String(obj);
+ }
+ },
+ defer: function(fn) {
+ setTimeout(fn, 0);
+ },
+ debounce: function(func, wait, immediate) {
+ var timeout, result;
+ return function() {
+ var context = this, args = arguments, later, callNow;
+ later = function() {
+ timeout = null;
+ if (!immediate) {
+ result = func.apply(context, args);
+ }
+ };
+ callNow = immediate && !timeout;
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ if (callNow) {
+ result = func.apply(context, args);
+ }
+ return result;
+ };
+ },
+ throttle: function(func, wait) {
+ var context, args, timeout, result, previous, later;
+ previous = 0;
+ later = function() {
+ previous = new Date();
+ timeout = null;
+ result = func.apply(context, args);
+ };
+ return function() {
+ var now = new Date(), remaining = wait - (now - previous);
+ context = this;
+ args = arguments;
+ if (remaining <= 0) {
+ clearTimeout(timeout);
+ timeout = null;
+ previous = now;
+ result = func.apply(context, args);
+ } else if (!timeout) {
+ timeout = setTimeout(later, remaining);
+ }
+ return result;
+ };
+ },
+ stringify: function(val) {
+ return _.isString(val) ? val : JSON.stringify(val);
+ },
+ noop: function() {}
+ };
+ }();
+ var VERSION = "0.11.1";
+ var tokenizers = function() {
+ "use strict";
+ return {
+ nonword: nonword,
+ whitespace: whitespace,
+ obj: {
+ nonword: getObjTokenizer(nonword),
+ whitespace: getObjTokenizer(whitespace)
+ }
+ };
+ function whitespace(str) {
+ str = _.toStr(str);
+ return str ? str.split(/\s+/) : [];
+ }
+ function nonword(str) {
+ str = _.toStr(str);
+ return str ? str.split(/\W+/) : [];
+ }
+ function getObjTokenizer(tokenizer) {
+ return function setKey(keys) {
+ keys = _.isArray(keys) ? keys : [].slice.call(arguments, 0);
+ return function tokenize(o) {
+ var tokens = [];
+ _.each(keys, function(k) {
+ tokens = tokens.concat(tokenizer(_.toStr(o[k])));
+ });
+ return tokens;
+ };
+ };
+ }
+ }();
+ var LruCache = function() {
+ "use strict";
+ function LruCache(maxSize) {
+ this.maxSize = _.isNumber(maxSize) ? maxSize : 100;
+ this.reset();
+ if (this.maxSize <= 0) {
+ this.set = this.get = $.noop;
+ }
+ }
+ _.mixin(LruCache.prototype, {
+ set: function set(key, val) {
+ var tailItem = this.list.tail, node;
+ if (this.size >= this.maxSize) {
+ this.list.remove(tailItem);
+ delete this.hash[tailItem.key];
+ this.size--;
+ }
+ if (node = this.hash[key]) {
+ node.val = val;
+ this.list.moveToFront(node);
+ } else {
+ node = new Node(key, val);
+ this.list.add(node);
+ this.hash[key] = node;
+ this.size++;
+ }
+ },
+ get: function get(key) {
+ var node = this.hash[key];
+ if (node) {
+ this.list.moveToFront(node);
+ return node.val;
+ }
+ },
+ reset: function reset() {
+ this.size = 0;
+ this.hash = {};
+ this.list = new List();
+ }
+ });
+ function List() {
+ this.head = this.tail = null;
+ }
+ _.mixin(List.prototype, {
+ add: function add(node) {
+ if (this.head) {
+ node.next = this.head;
+ this.head.prev = node;
+ }
+ this.head = node;
+ this.tail = this.tail || node;
+ },
+ remove: function remove(node) {
+ node.prev ? node.prev.next = node.next : this.head = node.next;
+ node.next ? node.next.prev = node.prev : this.tail = node.prev;
+ },
+ moveToFront: function(node) {
+ this.remove(node);
+ this.add(node);
+ }
+ });
+ function Node(key, val) {
+ this.key = key;
+ this.val = val;
+ this.prev = this.next = null;
+ }
+ return LruCache;
+ }();
+ var PersistentStorage = function() {
+ "use strict";
+ var LOCAL_STORAGE;
+ try {
+ LOCAL_STORAGE = window.localStorage;
+ LOCAL_STORAGE.setItem("~~~", "!");
+ LOCAL_STORAGE.removeItem("~~~");
+ } catch (err) {
+ LOCAL_STORAGE = null;
+ }
+ function PersistentStorage(namespace, override) {
+ this.prefix = [ "__", namespace, "__" ].join("");
+ this.ttlKey = "__ttl__";
+ this.keyMatcher = new RegExp("^" + _.escapeRegExChars(this.prefix));
+ this.ls = override || LOCAL_STORAGE;
+ !this.ls && this._noop();
+ }
+ _.mixin(PersistentStorage.prototype, {
+ _prefix: function(key) {
+ return this.prefix + key;
+ },
+ _ttlKey: function(key) {
+ return this._prefix(key) + this.ttlKey;
+ },
+ _noop: function() {
+ this.get = this.set = this.remove = this.clear = this.isExpired = _.noop;
+ },
+ _safeSet: function(key, val) {
+ try {
+ this.ls.setItem(key, val);
+ } catch (err) {
+ if (err.name === "QuotaExceededError") {
+ this.clear();
+ this._noop();
+ }
+ }
+ },
+ get: function(key) {
+ if (this.isExpired(key)) {
+ this.remove(key);
+ }
+ return decode(this.ls.getItem(this._prefix(key)));
+ },
+ set: function(key, val, ttl) {
+ if (_.isNumber(ttl)) {
+ this._safeSet(this._ttlKey(key), encode(now() + ttl));
+ } else {
+ this.ls.removeItem(this._ttlKey(key));
+ }
+ return this._safeSet(this._prefix(key), encode(val));
+ },
+ remove: function(key) {
+ this.ls.removeItem(this._ttlKey(key));
+ this.ls.removeItem(this._prefix(key));
+ return this;
+ },
+ clear: function() {
+ var i, keys = gatherMatchingKeys(this.keyMatcher);
+ for (i = keys.length; i--; ) {
+ this.remove(keys[i]);
+ }
+ return this;
+ },
+ isExpired: function(key) {
+ var ttl = decode(this.ls.getItem(this._ttlKey(key)));
+ return _.isNumber(ttl) && now() > ttl ? true : false;
+ }
+ });
+ return PersistentStorage;
+ function now() {
+ return new Date().getTime();
+ }
+ function encode(val) {
+ return JSON.stringify(_.isUndefined(val) ? null : val);
+ }
+ function decode(val) {
+ return $.parseJSON(val);
+ }
+ function gatherMatchingKeys(keyMatcher) {
+ var i, key, keys = [], len = LOCAL_STORAGE.length;
+ for (i = 0; i < len; i++) {
+ if ((key = LOCAL_STORAGE.key(i)).match(keyMatcher)) {
+ keys.push(key.replace(keyMatcher, ""));
+ }
+ }
+ return keys;
+ }
+ }();
+ var Transport = function() {
+ "use strict";
+ var pendingRequestsCount = 0, pendingRequests = {}, maxPendingRequests = 6, sharedCache = new LruCache(10);
+ function Transport(o) {
+ o = o || {};
+ this.cancelled = false;
+ this.lastReq = null;
+ this._send = o.transport;
+ this._get = o.limiter ? o.limiter(this._get) : this._get;
+ this._cache = o.cache === false ? new LruCache(0) : sharedCache;
+ }
+ Transport.setMaxPendingRequests = function setMaxPendingRequests(num) {
+ maxPendingRequests = num;
+ };
+ Transport.resetCache = function resetCache() {
+ sharedCache.reset();
+ };
+ _.mixin(Transport.prototype, {
+ _fingerprint: function fingerprint(o) {
+ o = o || {};
+ return o.url + o.type + $.param(o.data || {});
+ },
+ _get: function(o, cb) {
+ var that = this, fingerprint, jqXhr;
+ fingerprint = this._fingerprint(o);
+ if (this.cancelled || fingerprint !== this.lastReq) {
+ return;
+ }
+ if (jqXhr = pendingRequests[fingerprint]) {
+ jqXhr.done(done).fail(fail);
+ } else if (pendingRequestsCount < maxPendingRequests) {
+ pendingRequestsCount++;
+ pendingRequests[fingerprint] = this._send(o).done(done).fail(fail).always(always);
+ } else {
+ this.onDeckRequestArgs = [].slice.call(arguments, 0);
+ }
+ function done(resp) {
+ cb(null, resp);
+ that._cache.set(fingerprint, resp);
+ }
+ function fail() {
+ cb(true);
+ }
+ function always() {
+ pendingRequestsCount--;
+ delete pendingRequests[fingerprint];
+ if (that.onDeckRequestArgs) {
+ that._get.apply(that, that.onDeckRequestArgs);
+ that.onDeckRequestArgs = null;
+ }
+ }
+ },
+ get: function(o, cb) {
+ var resp, fingerprint;
+ cb = cb || $.noop;
+ o = _.isString(o) ? {
+ url: o
+ } : o || {};
+ fingerprint = this._fingerprint(o);
+ this.cancelled = false;
+ this.lastReq = fingerprint;
+ if (resp = this._cache.get(fingerprint)) {
+ cb(null, resp);
+ } else {
+ this._get(o, cb);
+ }
+ },
+ cancel: function() {
+ this.cancelled = true;
+ }
+ });
+ return Transport;
+ }();
+ var SearchIndex = window.SearchIndex = function() {
+ "use strict";
+ var CHILDREN = "c", IDS = "i";
+ function SearchIndex(o) {
+ o = o || {};
+ if (!o.datumTokenizer || !o.queryTokenizer) {
+ $.error("datumTokenizer and queryTokenizer are both required");
+ }
+ this.identify = o.identify || _.stringify;
+ this.datumTokenizer = o.datumTokenizer;
+ this.queryTokenizer = o.queryTokenizer;
+ this.reset();
+ }
+ _.mixin(SearchIndex.prototype, {
+ bootstrap: function bootstrap(o) {
+ this.datums = o.datums;
+ this.trie = o.trie;
+ },
+ add: function(data) {
+ var that = this;
+ data = _.isArray(data) ? data : [ data ];
+ _.each(data, function(datum) {
+ var id, tokens;
+ that.datums[id = that.identify(datum)] = datum;
+ tokens = normalizeTokens(that.datumTokenizer(datum));
+ _.each(tokens, function(token) {
+ var node, chars, ch;
+ node = that.trie;
+ chars = token.split("");
+ while (ch = chars.shift()) {
+ node = node[CHILDREN][ch] || (node[CHILDREN][ch] = newNode());
+ node[IDS].push(id);
+ }
+ });
+ });
+ },
+ get: function get(ids) {
+ var that = this;
+ return _.map(ids, function(id) {
+ return that.datums[id];
+ });
+ },
+ search: function search(query) {
+ var that = this, tokens, matches;
+ tokens = normalizeTokens(this.queryTokenizer(query));
+ _.each(tokens, function(token) {
+ var node, chars, ch, ids;
+ if (matches && matches.length === 0) {
+ return false;
+ }
+ node = that.trie;
+ chars = token.split("");
+ while (node && (ch = chars.shift())) {
+ node = node[CHILDREN][ch];
+ }
+ if (node && chars.length === 0) {
+ ids = node[IDS].slice(0);
+ matches = matches ? getIntersection(matches, ids) : ids;
+ } else {
+ matches = [];
+ return false;
+ }
+ });
+ return matches ? _.map(unique(matches), function(id) {
+ return that.datums[id];
+ }) : [];
+ },
+ all: function all() {
+ var values = [];
+ for (var key in this.datums) {
+ values.push(this.datums[key]);
+ }
+ return values;
+ },
+ reset: function reset() {
+ this.datums = {};
+ this.trie = newNode();
+ },
+ serialize: function serialize() {
+ return {
+ datums: this.datums,
+ trie: this.trie
+ };
+ }
+ });
+ return SearchIndex;
+ function normalizeTokens(tokens) {
+ tokens = _.filter(tokens, function(token) {
+ return !!token;
+ });
+ tokens = _.map(tokens, function(token) {
+ return token.toLowerCase();
+ });
+ return tokens;
+ }
+ function newNode() {
+ var node = {};
+ node[IDS] = [];
+ node[CHILDREN] = {};
+ return node;
+ }
+ function unique(array) {
+ var seen = {}, uniques = [];
+ for (var i = 0, len = array.length; i < len; i++) {
+ if (!seen[array[i]]) {
+ seen[array[i]] = true;
+ uniques.push(array[i]);
+ }
+ }
+ return uniques;
+ }
+ function getIntersection(arrayA, arrayB) {
+ var ai = 0, bi = 0, intersection = [];
+ arrayA = arrayA.sort();
+ arrayB = arrayB.sort();
+ var lenArrayA = arrayA.length, lenArrayB = arrayB.length;
+ while (ai < lenArrayA && bi < lenArrayB) {
+ if (arrayA[ai] < arrayB[bi]) {
+ ai++;
+ } else if (arrayA[ai] > arrayB[bi]) {
+ bi++;
+ } else {
+ intersection.push(arrayA[ai]);
+ ai++;
+ bi++;
+ }
+ }
+ return intersection;
+ }
+ }();
+ var Prefetch = function() {
+ "use strict";
+ var keys;
+ keys = {
+ data: "data",
+ protocol: "protocol",
+ thumbprint: "thumbprint"
+ };
+ function Prefetch(o) {
+ this.url = o.url;
+ this.ttl = o.ttl;
+ this.cache = o.cache;
+ this.prepare = o.prepare;
+ this.transform = o.transform;
+ this.transport = o.transport;
+ this.thumbprint = o.thumbprint;
+ this.storage = new PersistentStorage(o.cacheKey);
+ }
+ _.mixin(Prefetch.prototype, {
+ _settings: function settings() {
+ return {
+ url: this.url,
+ type: "GET",
+ dataType: "json"
+ };
+ },
+ store: function store(data) {
+ if (!this.cache) {
+ return;
+ }
+ this.storage.set(keys.data, data, this.ttl);
+ this.storage.set(keys.protocol, location.protocol, this.ttl);
+ this.storage.set(keys.thumbprint, this.thumbprint, this.ttl);
+ },
+ fromCache: function fromCache() {
+ var stored = {}, isExpired;
+ if (!this.cache) {
+ return null;
+ }
+ stored.data = this.storage.get(keys.data);
+ stored.protocol = this.storage.get(keys.protocol);
+ stored.thumbprint = this.storage.get(keys.thumbprint);
+ isExpired = stored.thumbprint !== this.thumbprint || stored.protocol !== location.protocol;
+ return stored.data && !isExpired ? stored.data : null;
+ },
+ fromNetwork: function(cb) {
+ var that = this, settings;
+ if (!cb) {
+ return;
+ }
+ settings = this.prepare(this._settings());
+ this.transport(settings).fail(onError).done(onResponse);
+ function onError() {
+ cb(true);
+ }
+ function onResponse(resp) {
+ cb(null, that.transform(resp));
+ }
+ },
+ clear: function clear() {
+ this.storage.clear();
+ return this;
+ }
+ });
+ return Prefetch;
+ }();
+ var Remote = function() {
+ "use strict";
+ function Remote(o) {
+ this.url = o.url;
+ this.prepare = o.prepare;
+ this.transform = o.transform;
+ this.transport = new Transport({
+ cache: o.cache,
+ limiter: o.limiter,
+ transport: o.transport
+ });
+ }
+ _.mixin(Remote.prototype, {
+ _settings: function settings() {
+ return {
+ url: this.url,
+ type: "GET",
+ dataType: "json"
+ };
+ },
+ get: function get(query, cb) {
+ var that = this, settings;
+ if (!cb) {
+ return;
+ }
+ query = query || "";
+ settings = this.prepare(query, this._settings());
+ return this.transport.get(settings, onResponse);
+ function onResponse(err, resp) {
+ err ? cb([]) : cb(that.transform(resp));
+ }
+ },
+ cancelLastRequest: function cancelLastRequest() {
+ this.transport.cancel();
+ }
+ });
+ return Remote;
+ }();
+ var oParser = function() {
+ "use strict";
+ return function parse(o) {
+ var defaults, sorter;
+ defaults = {
+ initialize: true,
+ identify: _.stringify,
+ datumTokenizer: null,
+ queryTokenizer: null,
+ sufficient: 5,
+ sorter: null,
+ local: [],
+ prefetch: null,
+ remote: null
+ };
+ o = _.mixin(defaults, o || {});
+ !o.datumTokenizer && $.error("datumTokenizer is required");
+ !o.queryTokenizer && $.error("queryTokenizer is required");
+ sorter = o.sorter;
+ o.sorter = sorter ? function(x) {
+ return x.sort(sorter);
+ } : _.identity;
+ o.local = _.isFunction(o.local) ? o.local() : o.local;
+ o.prefetch = parsePrefetch(o.prefetch);
+ o.remote = parseRemote(o.remote);
+ return o;
+ };
+ function parsePrefetch(o) {
+ var defaults;
+ if (!o) {
+ return null;
+ }
+ defaults = {
+ url: null,
+ ttl: 24 * 60 * 60 * 1e3,
+ cache: true,
+ cacheKey: null,
+ thumbprint: "",
+ prepare: _.identity,
+ transform: _.identity,
+ transport: null
+ };
+ o = _.isString(o) ? {
+ url: o
+ } : o;
+ o = _.mixin(defaults, o);
+ !o.url && $.error("prefetch requires url to be set");
+ o.transform = o.filter || o.transform;
+ o.cacheKey = o.cacheKey || o.url;
+ o.thumbprint = VERSION + o.thumbprint;
+ o.transport = o.transport ? callbackToDeferred(o.transport) : $.ajax;
+ return o;
+ }
+ function parseRemote(o) {
+ var defaults;
+ if (!o) {
+ return;
+ }
+ defaults = {
+ url: null,
+ cache: true,
+ prepare: null,
+ replace: null,
+ wildcard: null,
+ limiter: null,
+ rateLimitBy: "debounce",
+ rateLimitWait: 300,
+ transform: _.identity,
+ transport: null
+ };
+ o = _.isString(o) ? {
+ url: o
+ } : o;
+ o = _.mixin(defaults, o);
+ !o.url && $.error("remote requires url to be set");
+ o.transform = o.filter || o.transform;
+ o.prepare = toRemotePrepare(o);
+ o.limiter = toLimiter(o);
+ o.transport = o.transport ? callbackToDeferred(o.transport) : $.ajax;
+ delete o.replace;
+ delete o.wildcard;
+ delete o.rateLimitBy;
+ delete o.rateLimitWait;
+ return o;
+ }
+ function toRemotePrepare(o) {
+ var prepare, replace, wildcard;
+ prepare = o.prepare;
+ replace = o.replace;
+ wildcard = o.wildcard;
+ if (prepare) {
+ return prepare;
+ }
+ if (replace) {
+ prepare = prepareByReplace;
+ } else if (o.wildcard) {
+ prepare = prepareByWildcard;
+ } else {
+ prepare = idenityPrepare;
+ }
+ return prepare;
+ function prepareByReplace(query, settings) {
+ settings.url = replace(settings.url, query);
+ return settings;
+ }
+ function prepareByWildcard(query, settings) {
+ settings.url = settings.url.replace(wildcard, encodeURIComponent(query));
+ return settings;
+ }
+ function idenityPrepare(query, settings) {
+ return settings;
+ }
+ }
+ function toLimiter(o) {
+ var limiter, method, wait;
+ limiter = o.limiter;
+ method = o.rateLimitBy;
+ wait = o.rateLimitWait;
+ if (!limiter) {
+ limiter = /^throttle$/i.test(method) ? throttle(wait) : debounce(wait);
+ }
+ return limiter;
+ function debounce(wait) {
+ return function debounce(fn) {
+ return _.debounce(fn, wait);
+ };
+ }
+ function throttle(wait) {
+ return function throttle(fn) {
+ return _.throttle(fn, wait);
+ };
+ }
+ }
+ function callbackToDeferred(fn) {
+ return function wrapper(o) {
+ var deferred = $.Deferred();
+ fn(o, onSuccess, onError);
+ return deferred;
+ function onSuccess(resp) {
+ _.defer(function() {
+ deferred.resolve(resp);
+ });
+ }
+ function onError(err) {
+ _.defer(function() {
+ deferred.reject(err);
+ });
+ }
+ };
+ }
+ }();
+ var Bloodhound = function() {
+ "use strict";
+ var old;
+ old = window && window.Bloodhound;
+ function Bloodhound(o) {
+ o = oParser(o);
+ this.sorter = o.sorter;
+ this.identify = o.identify;
+ this.sufficient = o.sufficient;
+ this.local = o.local;
+ this.remote = o.remote ? new Remote(o.remote) : null;
+ this.prefetch = o.prefetch ? new Prefetch(o.prefetch) : null;
+ this.index = new SearchIndex({
+ identify: this.identify,
+ datumTokenizer: o.datumTokenizer,
+ queryTokenizer: o.queryTokenizer
+ });
+ o.initialize !== false && this.initialize();
+ }
+ Bloodhound.noConflict = function noConflict() {
+ window && (window.Bloodhound = old);
+ return Bloodhound;
+ };
+ Bloodhound.tokenizers = tokenizers;
+ _.mixin(Bloodhound.prototype, {
+ __ttAdapter: function ttAdapter() {
+ var that = this;
+ return this.remote ? withAsync : withoutAsync;
+ function withAsync(query, sync, async) {
+ return that.search(query, sync, async);
+ }
+ function withoutAsync(query, sync) {
+ return that.search(query, sync);
+ }
+ },
+ _loadPrefetch: function loadPrefetch() {
+ var that = this, deferred, serialized;
+ deferred = $.Deferred();
+ if (!this.prefetch) {
+ deferred.resolve();
+ } else if (serialized = this.prefetch.fromCache()) {
+ this.index.bootstrap(serialized);
+ deferred.resolve();
+ } else {
+ this.prefetch.fromNetwork(done);
+ }
+ return deferred.promise();
+ function done(err, data) {
+ if (err) {
+ return deferred.reject();
+ }
+ that.add(data);
+ that.prefetch.store(that.index.serialize());
+ deferred.resolve();
+ }
+ },
+ _initialize: function initialize() {
+ var that = this, deferred;
+ this.clear();
+ (this.initPromise = this._loadPrefetch()).done(addLocalToIndex);
+ return this.initPromise;
+ function addLocalToIndex() {
+ that.add(that.local);
+ }
+ },
+ initialize: function initialize(force) {
+ return !this.initPromise || force ? this._initialize() : this.initPromise;
+ },
+ add: function add(data) {
+ this.index.add(data);
+ return this;
+ },
+ get: function get(ids) {
+ ids = _.isArray(ids) ? ids : [].slice.call(arguments);
+ return this.index.get(ids);
+ },
+ search: function search(query, sync, async) {
+ var that = this, local;
+ local = this.sorter(this.index.search(query));
+ sync(this.remote ? local.slice() : local);
+ if (this.remote && local.length < this.sufficient) {
+ this.remote.get(query, processRemote);
+ } else if (this.remote) {
+ this.remote.cancelLastRequest();
+ }
+ return this;
+ function processRemote(remote) {
+ var nonDuplicates = [];
+ _.each(remote, function(r) {
+ !_.some(local, function(l) {
+ return that.identify(r) === that.identify(l);
+ }) && nonDuplicates.push(r);
+ });
+ async && async(nonDuplicates);
+ }
+ },
+ all: function all() {
+ return this.index.all();
+ },
+ clear: function clear() {
+ this.index.reset();
+ return this;
+ },
+ clearPrefetchCache: function clearPrefetchCache() {
+ this.prefetch && this.prefetch.clear();
+ return this;
+ },
+ clearRemoteCache: function clearRemoteCache() {
+ Transport.resetCache();
+ return this;
+ },
+ ttAdapter: function ttAdapter() {
+ return this.__ttAdapter();
+ }
+ });
+ return Bloodhound;
+ }();
+ return Bloodhound;
+});
+
+(function(root, factory) {
+ if (typeof define === "function" && define.amd) {
+ define("typeahead.js", [ "jquery" ], function(a0) {
+ return factory(a0);
+ });
+ } else if (typeof exports === "object") {
+ module.exports = factory(require("jquery"));
+ } else {
+ factory(jQuery);
+ }
+})(this, function($) {
+ var _ = function() {
+ "use strict";
+ return {
+ isMsie: function() {
+ return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false;
+ },
+ isBlankString: function(str) {
+ return !str || /^\s*$/.test(str);
+ },
+ escapeRegExChars: function(str) {
+ return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
+ },
+ isString: function(obj) {
+ return typeof obj === "string";
+ },
+ isNumber: function(obj) {
+ return typeof obj === "number";
+ },
+ isArray: $.isArray,
+ isFunction: $.isFunction,
+ isObject: $.isPlainObject,
+ isUndefined: function(obj) {
+ return typeof obj === "undefined";
+ },
+ isElement: function(obj) {
+ return !!(obj && obj.nodeType === 1);
+ },
+ isJQuery: function(obj) {
+ return obj instanceof $;
+ },
+ toStr: function toStr(s) {
+ return _.isUndefined(s) || s === null ? "" : s + "";
+ },
+ bind: $.proxy,
+ each: function(collection, cb) {
+ $.each(collection, reverseArgs);
+ function reverseArgs(index, value) {
+ return cb(value, index);
+ }
+ },
+ map: $.map,
+ filter: $.grep,
+ every: function(obj, test) {
+ var result = true;
+ if (!obj) {
+ return result;
+ }
+ $.each(obj, function(key, val) {
+ if (!(result = test.call(null, val, key, obj))) {
+ return false;
+ }
+ });
+ return !!result;
+ },
+ some: function(obj, test) {
+ var result = false;
+ if (!obj) {
+ return result;
+ }
+ $.each(obj, function(key, val) {
+ if (result = test.call(null, val, key, obj)) {
+ return false;
+ }
+ });
+ return !!result;
+ },
+ mixin: $.extend,
+ identity: function(x) {
+ return x;
+ },
+ clone: function(obj) {
+ return $.extend(true, {}, obj);
+ },
+ getIdGenerator: function() {
+ var counter = 0;
+ return function() {
+ return counter++;
+ };
+ },
+ templatify: function templatify(obj) {
+ return $.isFunction(obj) ? obj : template;
+ function template() {
+ return String(obj);
+ }
+ },
+ defer: function(fn) {
+ setTimeout(fn, 0);
+ },
+ debounce: function(func, wait, immediate) {
+ var timeout, result;
+ return function() {
+ var context = this, args = arguments, later, callNow;
+ later = function() {
+ timeout = null;
+ if (!immediate) {
+ result = func.apply(context, args);
+ }
+ };
+ callNow = immediate && !timeout;
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ if (callNow) {
+ result = func.apply(context, args);
+ }
+ return result;
+ };
+ },
+ throttle: function(func, wait) {
+ var context, args, timeout, result, previous, later;
+ previous = 0;
+ later = function() {
+ previous = new Date();
+ timeout = null;
+ result = func.apply(context, args);
+ };
+ return function() {
+ var now = new Date(), remaining = wait - (now - previous);
+ context = this;
+ args = arguments;
+ if (remaining <= 0) {
+ clearTimeout(timeout);
+ timeout = null;
+ previous = now;
+ result = func.apply(context, args);
+ } else if (!timeout) {
+ timeout = setTimeout(later, remaining);
+ }
+ return result;
+ };
+ },
+ stringify: function(val) {
+ return _.isString(val) ? val : JSON.stringify(val);
+ },
+ noop: function() {}
+ };
+ }();
+ var WWW = function() {
+ "use strict";
+ var defaultClassNames = {
+ wrapper: "twitter-typeahead",
+ input: "tt-input",
+ hint: "tt-hint",
+ menu: "tt-menu",
+ dataset: "tt-dataset",
+ suggestion: "tt-suggestion",
+ selectable: "tt-selectable",
+ empty: "tt-empty",
+ open: "tt-open",
+ cursor: "tt-cursor",
+ highlight: "tt-highlight"
+ };
+ return build;
+ function build(o) {
+ var www, classes;
+ classes = _.mixin({}, defaultClassNames, o);
+ www = {
+ css: buildCss(),
+ classes: classes,
+ html: buildHtml(classes),
+ selectors: buildSelectors(classes)
+ };
+ return {
+ css: www.css,
+ html: www.html,
+ classes: www.classes,
+ selectors: www.selectors,
+ mixin: function(o) {
+ _.mixin(o, www);
+ }
+ };
+ }
+ function buildHtml(c) {
+ return {
+ wrapper: '<span class="' + c.wrapper + '"></span>',
+ menu: '<div class="' + c.menu + '"></div>'
+ };
+ }
+ function buildSelectors(classes) {
+ var selectors = {};
+ _.each(classes, function(v, k) {
+ selectors[k] = "." + v;
+ });
+ return selectors;
+ }
+ function buildCss() {
+ var css = {
+ wrapper: {
+ position: "relative",
+ display: "inline-block"
+ },
+ hint: {
+ position: "absolute",
+ top: "0",
+ left: "0",
+ borderColor: "transparent",
+ boxShadow: "none",
+ opacity: "1"
+ },
+ input: {
+ position: "relative",
+ verticalAlign: "top",
+ backgroundColor: "transparent"
+ },
+ inputWithNoHint: {
+ position: "relative",
+ verticalAlign: "top"
+ },
+ menu: {
+ position: "absolute",
+ top: "100%",
+ left: "0",
+ zIndex: "100",
+ display: "none"
+ },
+ ltr: {
+ left: "0",
+ right: "auto"
+ },
+ rtl: {
+ left: "auto",
+ right: " 0"
+ }
+ };
+ if (_.isMsie()) {
+ _.mixin(css.input, {
+ backgroundImage: "url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)"
+ });
+ }
+ return css;
+ }
+ }();
+ var EventBus = function() {
+ "use strict";
+ var namespace, deprecationMap;
+ namespace = "typeahead:";
+ deprecationMap = {
+ render: "rendered",
+ cursorchange: "cursorchanged",
+ select: "selected",
+ autocomplete: "autocompleted"
+ };
+ function EventBus(o) {
+ if (!o || !o.el) {
+ $.error("EventBus initialized without el");
+ }
+ this.$el = $(o.el);
+ }
+ _.mixin(EventBus.prototype, {
+ _trigger: function(type, args) {
+ var $e;
+ $e = $.Event(namespace + type);
+ (args = args || []).unshift($e);
+ this.$el.trigger.apply(this.$el, args);
+ return $e;
+ },
+ before: function(type) {
+ var args, $e;
+ args = [].slice.call(arguments, 1);
+ $e = this._trigger("before" + type, args);
+ return $e.isDefaultPrevented();
+ },
+ trigger: function(type) {
+ var deprecatedType;
+ this._trigger(type, [].slice.call(arguments, 1));
+ if (deprecatedType = deprecationMap[type]) {
+ this._trigger(deprecatedType, [].slice.call(arguments, 1));
+ }
+ }
+ });
+ return EventBus;
+ }();
+ var EventEmitter = function() {
+ "use strict";
+ var splitter = /\s+/, nextTick = getNextTick();
+ return {
+ onSync: onSync,
+ onAsync: onAsync,
+ off: off,
+ trigger: trigger
+ };
+ function on(method, types, cb, context) {
+ var type;
+ if (!cb) {
+ return this;
+ }
+ types = types.split(splitter);
+ cb = context ? bindContext(cb, context) : cb;
+ this._callbacks = this._callbacks || {};
+ while (type = types.shift()) {
+ this._callbacks[type] = this._callbacks[type] || {
+ sync: [],
+ async: []
+ };
+ this._callbacks[type][method].push(cb);
+ }
+ return this;
+ }
+ function onAsync(types, cb, context) {
+ return on.call(this, "async", types, cb, context);
+ }
+ function onSync(types, cb, context) {
+ return on.call(this, "sync", types, cb, context);
+ }
+ function off(types) {
+ var type;
+ if (!this._callbacks) {
+ return this;
+ }
+ types = types.split(splitter);
+ while (type = types.shift()) {
+ delete this._callbacks[type];
+ }
+ return this;
+ }
+ function trigger(types) {
+ var type, callbacks, args, syncFlush, asyncFlush;
+ if (!this._callbacks) {
+ return this;
+ }
+ types = types.split(splitter);
+ args = [].slice.call(arguments, 1);
+ while ((type = types.shift()) && (callbacks = this._callbacks[type])) {
+ syncFlush = getFlush(callbacks.sync, this, [ type ].concat(args));
+ asyncFlush = getFlush(callbacks.async, this, [ type ].concat(args));
+ syncFlush() && nextTick(asyncFlush);
+ }
+ return this;
+ }
+ function getFlush(callbacks, context, args) {
+ return flush;
+ function flush() {
+ var cancelled;
+ for (var i = 0, len = callbacks.length; !cancelled && i < len; i += 1) {
+ cancelled = callbacks[i].apply(context, args) === false;
+ }
+ return !cancelled;
+ }
+ }
+ function getNextTick() {
+ var nextTickFn;
+ if (window.setImmediate) {
+ nextTickFn = function nextTickSetImmediate(fn) {
+ setImmediate(function() {
+ fn();
+ });
+ };
+ } else {
+ nextTickFn = function nextTickSetTimeout(fn) {
+ setTimeout(function() {
+ fn();
+ }, 0);
+ };
+ }
+ return nextTickFn;
+ }
+ function bindContext(fn, context) {
+ return fn.bind ? fn.bind(context) : function() {
+ fn.apply(context, [].slice.call(arguments, 0));
+ };
+ }
+ }();
+ var highlight = function(doc) {
+ "use strict";
+ var defaults = {
+ node: null,
+ pattern: null,
+ tagName: "strong",
+ className: null,
+ wordsOnly: false,
+ caseSensitive: false
+ };
+ return function hightlight(o) {
+ var regex;
+ o = _.mixin({}, defaults, o);
+ if (!o.node || !o.pattern) {
+ return;
+ }
+ o.pattern = _.isArray(o.pattern) ? o.pattern : [ o.pattern ];
+ regex = getRegex(o.pattern, o.caseSensitive, o.wordsOnly);
+ traverse(o.node, hightlightTextNode);
+ function hightlightTextNode(textNode) {
+ var match, patternNode, wrapperNode;
+ if (match = regex.exec(textNode.data)) {
+ wrapperNode = doc.createElement(o.tagName);
+ o.className && (wrapperNode.className = o.className);
+ patternNode = textNode.splitText(match.index);
+ patternNode.splitText(match[0].length);
+ wrapperNode.appendChild(patternNode.cloneNode(true));
+ textNode.parentNode.replaceChild(wrapperNode, patternNode);
+ }
+ return !!match;
+ }
+ function traverse(el, hightlightTextNode) {
+ var childNode, TEXT_NODE_TYPE = 3;
+ for (var i = 0; i < el.childNodes.length; i++) {
+ childNode = el.childNodes[i];
+ if (childNode.nodeType === TEXT_NODE_TYPE) {
+ i += hightlightTextNode(childNode) ? 1 : 0;
+ } else {
+ traverse(childNode, hightlightTextNode);
+ }
+ }
+ }
+ };
+ function getRegex(patterns, caseSensitive, wordsOnly) {
+ var escapedPatterns = [], regexStr;
+ for (var i = 0, len = patterns.length; i < len; i++) {
+ escapedPatterns.push(_.escapeRegExChars(patterns[i]));
+ }
+ regexStr = wordsOnly ? "\\b(" + escapedPatterns.join("|") + ")\\b" : "(" + escapedPatterns.join("|") + ")";
+ return caseSensitive ? new RegExp(regexStr) : new RegExp(regexStr, "i");
+ }
+ }(window.document);
+ var Input = function() {
+ "use strict";
+ var specialKeyCodeMap;
+ specialKeyCodeMap = {
+ 9: "tab",
+ 27: "esc",
+ 37: "left",
+ 39: "right",
+ 13: "enter",
+ 38: "up",
+ 40: "down"
+ };
+ function Input(o, www) {
+ o = o || {};
+ if (!o.input) {
+ $.error("input is missing");
+ }
+ www.mixin(this);
+ this.$hint = $(o.hint);
+ this.$input = $(o.input);
+ this.query = this.$input.val();
+ this.queryWhenFocused = this.hasFocus() ? this.query : null;
+ this.$overflowHelper = buildOverflowHelper(this.$input);
+ this._checkLanguageDirection();
+ if (this.$hint.length === 0) {
+ this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = _.noop;
+ }
+ }
+ Input.normalizeQuery = function(str) {
+ return _.toStr(str).replace(/^\s*/g, "").replace(/\s{2,}/g, " ");
+ };
+ _.mixin(Input.prototype, EventEmitter, {
+ _onBlur: function onBlur() {
+ this.resetInputValue();
+ this.trigger("blurred");
+ },
+ _onFocus: function onFocus() {
+ this.queryWhenFocused = this.query;
+ this.trigger("focused");
+ },
+ _onKeydown: function onKeydown($e) {
+ var keyName = specialKeyCodeMap[$e.which || $e.keyCode];
+ this._managePreventDefault(keyName, $e);
+ if (keyName && this._shouldTrigger(keyName, $e)) {
+ this.trigger(keyName + "Keyed", $e);
+ }
+ },
+ _onInput: function onInput() {
+ this._setQuery(this.getInputValue());
+ this.clearHintIfInvalid();
+ this._checkLanguageDirection();
+ },
+ _managePreventDefault: function managePreventDefault(keyName, $e) {
+ var preventDefault;
+ switch (keyName) {
+ case "up":
+ case "down":
+ preventDefault = !withModifier($e);
+ break;
+
+ default:
+ preventDefault = false;
+ }
+ preventDefault && $e.preventDefault();
+ },
+ _shouldTrigger: function shouldTrigger(keyName, $e) {
+ var trigger;
+ switch (keyName) {
+ case "tab":
+ trigger = !withModifier($e);
+ break;
+
+ default:
+ trigger = true;
+ }
+ return trigger;
+ },
+ _checkLanguageDirection: function checkLanguageDirection() {
+ var dir = (this.$input.css("direction") || "ltr").toLowerCase();
+ if (this.dir !== dir) {
+ this.dir = dir;
+ this.$hint.attr("dir", dir);
+ this.trigger("langDirChanged", dir);
+ }
+ },
+ _setQuery: function setQuery(val, silent) {
+ var areEquivalent, hasDifferentWhitespace;
+ areEquivalent = areQueriesEquivalent(val, this.query);
+ hasDifferentWhitespace = areEquivalent ? this.query.length !== val.length : false;
+ this.query = val;
+ if (!silent && !areEquivalent) {
+ this.trigger("queryChanged", this.query);
+ } else if (!silent && hasDifferentWhitespace) {
+ this.trigger("whitespaceChanged", this.query);
+ }
+ },
+ bind: function() {
+ var that = this, onBlur, onFocus, onKeydown, onInput;
+ onBlur = _.bind(this._onBlur, this);
+ onFocus = _.bind(this._onFocus, this);
+ onKeydown = _.bind(this._onKeydown, this);
+ onInput = _.bind(this._onInput, this);
+ this.$input.on("blur.tt", onBlur).on("focus.tt", onFocus).on("keydown.tt", onKeydown);
+ if (!_.isMsie() || _.isMsie() > 9) {
+ this.$input.on("input.tt", onInput);
+ } else {
+ this.$input.on("keydown.tt keypress.tt cut.tt paste.tt", function($e) {
+ if (specialKeyCodeMap[$e.which || $e.keyCode]) {
+ return;
+ }
+ _.defer(_.bind(that._onInput, that, $e));
+ });
+ }
+ return this;
+ },
+ focus: function focus() {
+ this.$input.focus();
+ },
+ blur: function blur() {
+ this.$input.blur();
+ },
+ getLangDir: function getLangDir() {
+ return this.dir;
+ },
+ getQuery: function getQuery() {
+ return this.query || "";
+ },
+ setQuery: function setQuery(val, silent) {
+ this.setInputValue(val);
+ this._setQuery(val, silent);
+ },
+ hasQueryChangedSinceLastFocus: function hasQueryChangedSinceLastFocus() {
+ return this.query !== this.queryWhenFocused;
+ },
+ getInputValue: function getInputValue() {
+ return this.$input.val();
+ },
+ setInputValue: function setInputValue(value) {
+ this.$input.val(value);
+ this.clearHintIfInvalid();
+ this._checkLanguageDirection();
+ },
+ resetInputValue: function resetInputValue() {
+ this.setInputValue(this.query);
+ },
+ getHint: function getHint() {
+ return this.$hint.val();
+ },
+ setHint: function setHint(value) {
+ this.$hint.val(value);
+ },
+ clearHint: function clearHint() {
+ this.setHint("");
+ },
+ clearHintIfInvalid: function clearHintIfInvalid() {
+ var val, hint, valIsPrefixOfHint, isValid;
+ val = this.getInputValue();
+ hint = this.getHint();
+ valIsPrefixOfHint = val !== hint && hint.indexOf(val) === 0;
+ isValid = val !== "" && valIsPrefixOfHint && !this.hasOverflow();
+ !isValid && this.clearHint();
+ },
+ hasFocus: function hasFocus() {
+ return this.$input.is(":focus");
+ },
+ hasOverflow: function hasOverflow() {
+ var constraint = this.$input.width() - 2;
+ this.$overflowHelper.text(this.getInputValue());
+ return this.$overflowHelper.width() >= constraint;
+ },
+ isCursorAtEnd: function() {
+ var valueLength, selectionStart, range;
+ valueLength = this.$input.val().length;
+ selectionStart = this.$input[0].selectionStart;
+ if (_.isNumber(selectionStart)) {
+ return selectionStart === valueLength;
+ } else if (document.selection) {
+ range = document.selection.createRange();
+ range.moveStart("character", -valueLength);
+ return valueLength === range.text.length;
+ }
+ return true;
+ },
+ destroy: function destroy() {
+ this.$hint.off(".tt");
+ this.$input.off(".tt");
+ this.$overflowHelper.remove();
+ this.$hint = this.$input = this.$overflowHelper = $("<div>");
+ }
+ });
+ return Input;
+ function buildOverflowHelper($input) {
+ return $('<pre aria-hidden="true"></pre>').css({
+ position: "absolute",
+ visibility: "hidden",
+ whiteSpace: "pre",
+ fontFamily: $input.css("font-family"),
+ fontSize: $input.css("font-size"),
+ fontStyle: $input.css("font-style"),
+ fontVariant: $input.css("font-variant"),
+ fontWeight: $input.css("font-weight"),
+ wordSpacing: $input.css("word-spacing"),
+ letterSpacing: $input.css("letter-spacing"),
+ textIndent: $input.css("text-indent"),
+ textRendering: $input.css("text-rendering"),
+ textTransform: $input.css("text-transform")
+ }).insertAfter($input);
+ }
+ function areQueriesEquivalent(a, b) {
+ return Input.normalizeQuery(a) === Input.normalizeQuery(b);
+ }
+ function withModifier($e) {
+ return $e.altKey || $e.ctrlKey || $e.metaKey || $e.shiftKey;
+ }
+ }();
+ var Dataset = function() {
+ "use strict";
+ var keys, nameGenerator;
+ keys = {
+ val: "tt-selectable-display",
+ obj: "tt-selectable-object"
+ };
+ nameGenerator = _.getIdGenerator();
+ function Dataset(o, www) {
+ o = o || {};
+ o.templates = o.templates || {};
+ o.templates.notFound = o.templates.notFound || o.templates.empty;
+ if (!o.source) {
+ $.error("missing source");
+ }
+ if (!o.node) {
+ $.error("missing node");
+ }
+ if (o.name && !isValidName(o.name)) {
+ $.error("invalid dataset name: " + o.name);
+ }
+ www.mixin(this);
+ this.highlight = !!o.highlight;
+ this.name = o.name || nameGenerator();
+ this.limit = o.limit || 5;
+ this.displayFn = getDisplayFn(o.display || o.displayKey);
+ this.templates = getTemplates(o.templates, this.displayFn);
+ this.source = o.source.__ttAdapter ? o.source.__ttAdapter() : o.source;
+ this.async = _.isUndefined(o.async) ? this.source.length > 2 : !!o.async;
+ this._resetLastSuggestion();
+ this.$el = $(o.node).addClass(this.classes.dataset).addClass(this.classes.dataset + "-" + this.name);
+ }
+ Dataset.extractData = function extractData(el) {
+ var $el = $(el);
+ if ($el.data(keys.obj)) {
+ return {
+ val: $el.data(keys.val) || "",
+ obj: $el.data(keys.obj) || null
+ };
+ }
+ return null;
+ };
+ _.mixin(Dataset.prototype, EventEmitter, {
+ _overwrite: function overwrite(query, suggestions) {
+ suggestions = suggestions || [];
+ if (suggestions.length) {
+ this._renderSuggestions(query, suggestions);
+ } else if (this.async && this.templates.pending) {
+ this._renderPending(query);
+ } else if (!this.async && this.templates.notFound) {
+ this._renderNotFound(query);
+ } else {
+ this._empty();
+ }
+ this.trigger("rendered", this.name, suggestions, false);
+ },
+ _append: function append(query, suggestions) {
+ suggestions = suggestions || [];
+ if (suggestions.length && this.$lastSuggestion.length) {
+ this._appendSuggestions(query, suggestions);
+ } else if (suggestions.length) {
+ this._renderSuggestions(query, suggestions);
+ } else if (!this.$lastSuggestion.length && this.templates.notFound) {
+ this._renderNotFound(query);
+ }
+ this.trigger("rendered", this.name, suggestions, true);
+ },
+ _renderSuggestions: function renderSuggestions(query, suggestions) {
+ var $fragment;
+ $fragment = this._getSuggestionsFragment(query, suggestions);
+ this.$lastSuggestion = $fragment.children().last();
+ this.$el.html($fragment).prepend(this._getHeader(query, suggestions)).append(this._getFooter(query, suggestions));
+ },
+ _appendSuggestions: function appendSuggestions(query, suggestions) {
+ var $fragment, $lastSuggestion;
+ $fragment = this._getSuggestionsFragment(query, suggestions);
+ $lastSuggestion = $fragment.children().last();
+ this.$lastSuggestion.after($fragment);
+ this.$lastSuggestion = $lastSuggestion;
+ },
+ _renderPending: function renderPending(query) {
+ var template = this.templates.pending;
+ this._resetLastSuggestion();
+ template && this.$el.html(template({
+ query: query,
+ dataset: this.name
+ }));
+ },
+ _renderNotFound: function renderNotFound(query) {
+ var template = this.templates.notFound;
+ this._resetLastSuggestion();
+ template && this.$el.html(template({
+ query: query,
+ dataset: this.name
+ }));
+ },
+ _empty: function empty() {
+ this.$el.empty();
+ this._resetLastSuggestion();
+ },
+ _getSuggestionsFragment: function getSuggestionsFragment(query, suggestions) {
+ var that = this, fragment;
+ fragment = document.createDocumentFragment();
+ _.each(suggestions, function getSuggestionNode(suggestion) {
+ var $el, context;
+ context = that._injectQuery(query, suggestion);
+ $el = $(that.templates.suggestion(context)).data(keys.obj, suggestion).data(keys.val, that.displayFn(suggestion)).addClass(that.classes.suggestion + " " + that.classes.selectable);
+ fragment.appendChild($el[0]);
+ });
+ this.highlight && highlight({
+ className: this.classes.highlight,
+ node: fragment,
+ pattern: query
+ });
+ return $(fragment);
+ },
+ _getFooter: function getFooter(query, suggestions) {
+ return this.templates.footer ? this.templates.footer({
+ query: query,
+ suggestions: suggestions,
+ dataset: this.name
+ }) : null;
+ },
+ _getHeader: function getHeader(query, suggestions) {
+ return this.templates.header ? this.templates.header({
+ query: query,
+ suggestions: suggestions,
+ dataset: this.name
+ }) : null;
+ },
+ _resetLastSuggestion: function resetLastSuggestion() {
+ this.$lastSuggestion = $();
+ },
+ _injectQuery: function injectQuery(query, obj) {
+ return _.isObject(obj) ? _.mixin({
+ _query: query
+ }, obj) : obj;
+ },
+ update: function update(query) {
+ var that = this, canceled = false, syncCalled = false, rendered = 0;
+ this.cancel();
+ this.cancel = function cancel() {
+ canceled = true;
+ that.cancel = $.noop;
+ that.async && that.trigger("asyncCanceled", query);
+ };
+ this.source(query, sync, async);
+ !syncCalled && sync([]);
+ function sync(suggestions) {
+ if (syncCalled) {
+ return;
+ }
+ syncCalled = true;
+ suggestions = (suggestions || []).slice(0, that.limit);
+ rendered = suggestions.length;
+ that._overwrite(query, suggestions);
+ if (rendered < that.limit && that.async) {
+ that.trigger("asyncRequested", query);
+ }
+ }
+ function async(suggestions) {
+ suggestions = suggestions || [];
+ if (!canceled && rendered < that.limit) {
+ that.cancel = $.noop;
+ rendered += suggestions.length;
+ that._append(query, suggestions.slice(0, that.limit - rendered));
+ that.async && that.trigger("asyncReceived", query);
+ }
+ }
+ },
+ cancel: $.noop,
+ clear: function clear() {
+ this._empty();
+ this.cancel();
+ this.trigger("cleared");
+ },
+ isEmpty: function isEmpty() {
+ return this.$el.is(":empty");
+ },
+ destroy: function destroy() {
+ this.$el = $("<div>");
+ }
+ });
+ return Dataset;
+ function getDisplayFn(display) {
+ display = display || _.stringify;
+ return _.isFunction(display) ? display : displayFn;
+ function displayFn(obj) {
+ return obj[display];
+ }
+ }
+ function getTemplates(templates, displayFn) {
+ return {
+ notFound: templates.notFound && _.templatify(templates.notFound),
+ pending: templates.pending && _.templatify(templates.pending),
+ header: templates.header && _.templatify(templates.header),
+ footer: templates.footer && _.templatify(templates.footer),
+ suggestion: templates.suggestion || suggestionTemplate
+ };
+ function suggestionTemplate(context) {
+ return $("<div>").text(displayFn(context));
+ }
+ }
+ function isValidName(str) {
+ return /^[_a-zA-Z0-9-]+$/.test(str);
+ }
+ }();
+ var Menu = function() {
+ "use strict";
+ function Menu(o, www) {
+ var that = this;
+ o = o || {};
+ if (!o.node) {
+ $.error("node is required");
+ }
+ www.mixin(this);
+ this.$node = $(o.node);
+ this.query = null;
+ this.datasets = _.map(o.datasets, initializeDataset);
+ function initializeDataset(oDataset) {
+ var node = that.$node.find(oDataset.node).first();
+ oDataset.node = node.length ? node : $("<div>").appendTo(that.$node);
+ return new Dataset(oDataset, www);
+ }
+ }
+ _.mixin(Menu.prototype, EventEmitter, {
+ _onSelectableClick: function onSelectableClick($e) {
+ this.trigger("selectableClicked", $($e.currentTarget));
+ },
+ _onRendered: function onRendered(type, dataset, suggestions, async) {
+ this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty());
+ this.trigger("datasetRendered", dataset, suggestions, async);
+ },
+ _onCleared: function onCleared() {
+ this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty());
+ this.trigger("datasetCleared");
+ },
+ _propagate: function propagate() {
+ this.trigger.apply(this, arguments);
+ },
+ _allDatasetsEmpty: function allDatasetsEmpty() {
+ return _.every(this.datasets, isDatasetEmpty);
+ function isDatasetEmpty(dataset) {
+ return dataset.isEmpty();
+ }
+ },
+ _getSelectables: function getSelectables() {
+ return this.$node.find(this.selectors.selectable);
+ },
+ _removeCursor: function _removeCursor() {
+ var $selectable = this.getActiveSelectable();
+ $selectable && $selectable.removeClass(this.classes.cursor);
+ },
+ _ensureVisible: function ensureVisible($el) {
+ var elTop, elBottom, nodeScrollTop, nodeHeight;
+ elTop = $el.position().top;
+ elBottom = elTop + $el.outerHeight(true);
+ nodeScrollTop = this.$node.scrollTop();
+ nodeHeight = this.$node.height() + parseInt(this.$node.css("paddingTop"), 10) + parseInt(this.$node.css("paddingBottom"), 10);
+ if (elTop < 0) {
+ this.$node.scrollTop(nodeScrollTop + elTop);
+ } else if (nodeHeight < elBottom) {
+ this.$node.scrollTop(nodeScrollTop + (elBottom - nodeHeight));
+ }
+ },
+ bind: function() {
+ var that = this, onSelectableClick;
+ onSelectableClick = _.bind(this._onSelectableClick, this);
+ this.$node.on("click.tt", this.selectors.selectable, onSelectableClick);
+ _.each(this.datasets, function(dataset) {
+ dataset.onSync("asyncRequested", that._propagate, that).onSync("asyncCanceled", that._propagate, that).onSync("asyncReceived", that._propagate, that).onSync("rendered", that._onRendered, that).onSync("cleared", that._onCleared, that);
+ });
+ return this;
+ },
+ isOpen: function isOpen() {
+ return this.$node.hasClass(this.classes.open);
+ },
+ open: function open() {
+ this.$node.addClass(this.classes.open);
+ },
+ close: function close() {
+ this.$node.removeClass(this.classes.open);
+ this._removeCursor();
+ },
+ setLanguageDirection: function setLanguageDirection(dir) {
+ this.$node.attr("dir", dir);
+ },
+ selectableRelativeToCursor: function selectableRelativeToCursor(delta) {
+ var $selectables, $oldCursor, oldIndex, newIndex;
+ $oldCursor = this.getActiveSelectable();
+ $selectables = this._getSelectables();
+ oldIndex = $oldCursor ? $selectables.index($oldCursor) : -1;
+ newIndex = oldIndex + delta;
+ newIndex = (newIndex + 1) % ($selectables.length + 1) - 1;
+ newIndex = newIndex < -1 ? $selectables.length - 1 : newIndex;
+ return newIndex === -1 ? null : $selectables.eq(newIndex);
+ },
+ setCursor: function setCursor($selectable) {
+ this._removeCursor();
+ if ($selectable = $selectable && $selectable.first()) {
+ $selectable.addClass(this.classes.cursor);
+ this._ensureVisible($selectable);
+ }
+ },
+ getSelectableData: function getSelectableData($el) {
+ return $el && $el.length ? Dataset.extractData($el) : null;
+ },
+ getActiveSelectable: function getActiveSelectable() {
+ var $selectable = this._getSelectables().filter(this.selectors.cursor).first();
+ return $selectable.length ? $selectable : null;
+ },
+ getTopSelectable: function getTopSelectable() {
+ var $selectable = this._getSelectables().first();
+ return $selectable.length ? $selectable : null;
+ },
+ update: function update(query) {
+ var isValidUpdate = query !== this.query;
+ if (isValidUpdate) {
+ this.query = query;
+ _.each(this.datasets, updateDataset);
+ }
+ return isValidUpdate;
+ function updateDataset(dataset) {
+ dataset.update(query);
+ }
+ },
+ empty: function empty() {
+ _.each(this.datasets, clearDataset);
+ this.query = null;
+ this.$node.addClass(this.classes.empty);
+ function clearDataset(dataset) {
+ dataset.clear();
+ }
+ },
+ destroy: function destroy() {
+ this.$node.off(".tt");
+ this.$node = $("<div>");
+ _.each(this.datasets, destroyDataset);
+ function destroyDataset(dataset) {
+ dataset.destroy();
+ }
+ }
+ });
+ return Menu;
+ }();
+ var DefaultMenu = function() {
+ "use strict";
+ var s = Menu.prototype;
+ function DefaultMenu() {
+ Menu.apply(this, [].slice.call(arguments, 0));
+ }
+ _.mixin(DefaultMenu.prototype, Menu.prototype, {
+ open: function open() {
+ !this._allDatasetsEmpty() && this._show();
+ return s.open.apply(this, [].slice.call(arguments, 0));
+ },
+ close: function close() {
+ this._hide();
+ return s.close.apply(this, [].slice.call(arguments, 0));
+ },
+ _onRendered: function onRendered() {
+ if (this._allDatasetsEmpty()) {
+ this._hide();
+ } else {
+ this.isOpen() && this._show();
+ }
+ return s._onRendered.apply(this, [].slice.call(arguments, 0));
+ },
+ _onCleared: function onCleared() {
+ if (this._allDatasetsEmpty()) {
+ this._hide();
+ } else {
+ this.isOpen() && this._show();
+ }
+ return s._onCleared.apply(this, [].slice.call(arguments, 0));
+ },
+ setLanguageDirection: function setLanguageDirection(dir) {
+ this.$node.css(dir === "ltr" ? this.css.ltr : this.css.rtl);
+ return s.setLanguageDirection.apply(this, [].slice.call(arguments, 0));
+ },
+ _hide: function hide() {
+ this.$node.hide();
+ },
+ _show: function show() {
+ this.$node.css("display", "block");
+ }
+ });
+ return DefaultMenu;
+ }();
+ var Typeahead = function() {
+ "use strict";
+ function Typeahead(o, www) {
+ var onFocused, onBlurred, onEnterKeyed, onTabKeyed, onEscKeyed, onUpKeyed, onDownKeyed, onLeftKeyed, onRightKeyed, onQueryChanged, onWhitespaceChanged;
+ o = o || {};
+ if (!o.input) {
+ $.error("missing input");
+ }
+ if (!o.menu) {
+ $.error("missing menu");
+ }
+ if (!o.eventBus) {
+ $.error("missing event bus");
+ }
+ www.mixin(this);
+ this.eventBus = o.eventBus;
+ this.minLength = _.isNumber(o.minLength) ? o.minLength : 1;
+ this.input = o.input;
+ this.menu = o.menu;
+ this.enabled = true;
+ this.active = false;
+ this.input.hasFocus() && this.activate();
+ this.dir = this.input.getLangDir();
+ this._hacks();
+ this.menu.bind().onSync("selectableClicked", this._onSelectableClicked, this).onSync("asyncRequested", this._onAsyncRequested, this).onSync("asyncCanceled", this._onAsyncCanceled, this).onSync("asyncReceived", this._onAsyncReceived, this).onSync("datasetRendered", this._onDatasetRendered, this).onSync("datasetCleared", this._onDatasetCleared, this);
+ onFocused = c(this, "activate", "open", "_onFocused");
+ onBlurred = c(this, "deactivate", "_onBlurred");
+ onEnterKeyed = c(this, "isActive", "isOpen", "_onEnterKeyed");
+ onTabKeyed = c(this, "isActive", "isOpen", "_onTabKeyed");
+ onEscKeyed = c(this, "isActive", "_onEscKeyed");
+ onUpKeyed = c(this, "isActive", "open", "_onUpKeyed");
+ onDownKeyed = c(this, "isActive", "open", "_onDownKeyed");
+ onLeftKeyed = c(this, "isActive", "isOpen", "_onLeftKeyed");
+ onRightKeyed = c(this, "isActive", "isOpen", "_onRightKeyed");
+ onQueryChanged = c(this, "_openIfActive", "_onQueryChanged");
+ onWhitespaceChanged = c(this, "_openIfActive", "_onWhitespaceChanged");
+ this.input.bind().onSync("focused", onFocused, this).onSync("blurred", onBlurred, this).onSync("enterKeyed", onEnterKeyed, this).onSync("tabKeyed", onTabKeyed, this).onSync("escKeyed", onEscKeyed, this).onSync("upKeyed", onUpKeyed, this).onSync("downKeyed", onDownKeyed, this).onSync("leftKeyed", onLeftKeyed, this).onSync("rightKeyed", onRightKeyed, this).onSync("queryChanged", onQueryChanged, this).onSync("whitespaceChanged", onWhitespaceChanged, this).onSync("langDirChanged", this._onLangDirChanged, this);
+ }
+ _.mixin(Typeahead.prototype, {
+ _hacks: function hacks() {
+ var $input, $menu;
+ $input = this.input.$input || $("<div>");
+ $menu = this.menu.$node || $("<div>");
+ $input.on("blur.tt", function($e) {
+ var active, isActive, hasActive;
+ active = document.activeElement;
+ isActive = $menu.is(active);
+ hasActive = $menu.has(active).length > 0;
+ if (_.isMsie() && (isActive || hasActive)) {
+ $e.preventDefault();
+ $e.stopImmediatePropagation();
+ _.defer(function() {
+ $input.focus();
+ });
+ }
+ });
+ $menu.on("mousedown.tt", function($e) {
+ $e.preventDefault();
+ });
+ },
+ _onSelectableClicked: function onSelectableClicked(type, $el) {
+ this.select($el);
+ },
+ _onDatasetCleared: function onDatasetCleared() {
+ this._updateHint();
+ },
+ _onDatasetRendered: function onDatasetRendered(type, dataset, suggestions, async) {
+ this._updateHint();
+ this.eventBus.trigger("render", suggestions, async, dataset);
+ },
+ _onAsyncRequested: function onAsyncRequested(type, dataset, query) {
+ this.eventBus.trigger("asyncrequest", query, dataset);
+ },
+ _onAsyncCanceled: function onAsyncCanceled(type, dataset, query) {
+ this.eventBus.trigger("asynccancel", query, dataset);
+ },
+ _onAsyncReceived: function onAsyncReceived(type, dataset, query) {
+ this.eventBus.trigger("asyncreceive", query, dataset);
+ },
+ _onFocused: function onFocused() {
+ this._minLengthMet() && this.menu.update(this.input.getQuery());
+ },
+ _onBlurred: function onBlurred() {
+ if (this.input.hasQueryChangedSinceLastFocus()) {
+ this.eventBus.trigger("change", this.input.getQuery());
+ }
+ },
+ _onEnterKeyed: function onEnterKeyed(type, $e) {
+ var $selectable;
+ if ($selectable = this.menu.getActiveSelectable()) {
+ this.select($selectable) && $e.preventDefault();
+ }
+ },
+ _onTabKeyed: function onTabKeyed(type, $e) {
+ var $selectable;
+ if ($selectable = this.menu.getActiveSelectable()) {
+ this.select($selectable) && $e.preventDefault();
+ } else if ($selectable = this.menu.getTopSelectable()) {
+ this.autocomplete($selectable) && $e.preventDefault();
+ }
+ },
+ _onEscKeyed: function onEscKeyed() {
+ this.close();
+ },
+ _onUpKeyed: function onUpKeyed() {
+ this.moveCursor(-1);
+ },
+ _onDownKeyed: function onDownKeyed() {
+ this.moveCursor(+1);
+ },
+ _onLeftKeyed: function onLeftKeyed() {
+ if (this.dir === "rtl" && this.input.isCursorAtEnd()) {
+ this.autocomplete(this.menu.getTopSelectable());
+ }
+ },
+ _onRightKeyed: function onRightKeyed() {
+ if (this.dir === "ltr" && this.input.isCursorAtEnd()) {
+ this.autocomplete(this.menu.getTopSelectable());
+ }
+ },
+ _onQueryChanged: function onQueryChanged(e, query) {
+ this._minLengthMet(query) ? this.menu.update(query) : this.menu.empty();
+ },
+ _onWhitespaceChanged: function onWhitespaceChanged() {
+ this._updateHint();
+ },
+ _onLangDirChanged: function onLangDirChanged(e, dir) {
+ if (this.dir !== dir) {
+ this.dir = dir;
+ this.menu.setLanguageDirection(dir);
+ }
+ },
+ _openIfActive: function openIfActive() {
+ this.isActive() && this.open();
+ },
+ _minLengthMet: function minLengthMet(query) {
+ query = _.isString(query) ? query : this.input.getQuery() || "";
+ return query.length >= this.minLength;
+ },
+ _updateHint: function updateHint() {
+ var $selectable, data, val, query, escapedQuery, frontMatchRegEx, match;
+ $selectable = this.menu.getTopSelectable();
+ data = this.menu.getSelectableData($selectable);
+ val = this.input.getInputValue();
+ if (data && !_.isBlankString(val) && !this.input.hasOverflow()) {
+ query = Input.normalizeQuery(val);
+ escapedQuery = _.escapeRegExChars(query);
+ frontMatchRegEx = new RegExp("^(?:" + escapedQuery + ")(.+$)", "i");
+ match = frontMatchRegEx.exec(data.val);
+ match && this.input.setHint(val + match[1]);
+ } else {
+ this.input.clearHint();
+ }
+ },
+ isEnabled: function isEnabled() {
+ return this.enabled;
+ },
+ enable: function enable() {
+ this.enabled = true;
+ },
+ disable: function disable() {
+ this.enabled = false;
+ },
+ isActive: function isActive() {
+ return this.active;
+ },
+ activate: function activate() {
+ if (this.isActive()) {
+ return true;
+ } else if (!this.isEnabled() || this.eventBus.before("active")) {
+ return false;
+ } else {
+ this.active = true;
+ this.eventBus.trigger("active");
+ return true;
+ }
+ },
+ deactivate: function deactivate() {
+ if (!this.isActive()) {
+ return true;
+ } else if (this.eventBus.before("idle")) {
+ return false;
+ } else {
+ this.active = false;
+ this.close();
+ this.eventBus.trigger("idle");
+ return true;
+ }
+ },
+ isOpen: function isOpen() {
+ return this.menu.isOpen();
+ },
+ open: function open() {
+ if (!this.isOpen() && !this.eventBus.before("open")) {
+ this.menu.open();
+ this._updateHint();
+ this.eventBus.trigger("open");
+ }
+ return this.isOpen();
+ },
+ close: function close() {
+ if (this.isOpen() && !this.eventBus.before("close")) {
+ this.menu.close();
+ this.input.clearHint();
+ this.input.resetInputValue();
+ this.eventBus.trigger("close");
+ }
+ return !this.isOpen();
+ },
+ setVal: function setVal(val) {
+ this.input.setQuery(_.toStr(val));
+ },
+ getVal: function getVal() {
+ return this.input.getQuery();
+ },
+ select: function select($selectable) {
+ var data = this.menu.getSelectableData($selectable);
+ if (data && !this.eventBus.before("select", data.obj)) {
+ this.input.setQuery(data.val, true);
+ this.eventBus.trigger("select", data.obj);
+ this.close();
+ return true;
+ }
+ return false;
+ },
+ autocomplete: function autocomplete($selectable) {
+ var query, data, isValid;
+ query = this.input.getQuery();
+ data = this.menu.getSelectableData($selectable);
+ isValid = data && query !== data.val;
+ if (isValid && !this.eventBus.before("autocomplete", data.obj)) {
+ this.input.setQuery(data.val);
+ this.eventBus.trigger("autocomplete", data.obj);
+ return true;
+ }
+ return false;
+ },
+ moveCursor: function moveCursor(delta) {
+ var query, $candidate, data, payload, cancelMove;
+ query = this.input.getQuery();
+ $candidate = this.menu.selectableRelativeToCursor(delta);
+ data = this.menu.getSelectableData($candidate);
+ payload = data ? data.obj : null;
+ cancelMove = this._minLengthMet() && this.menu.update(query);
+ if (!cancelMove && !this.eventBus.before("cursorchange", payload)) {
+ this.menu.setCursor($candidate);
+ if (data) {
+ this.input.setInputValue(data.val);
+ } else {
+ this.input.resetInputValue();
+ this._updateHint();
+ }
+ this.eventBus.trigger("cursorchange", payload);
+ return true;
+ }
+ return false;
+ },
+ destroy: function destroy() {
+ this.input.destroy();
+ this.menu.destroy();
+ }
+ });
+ return Typeahead;
+ function c(ctx) {
+ var methods = [].slice.call(arguments, 1);
+ return function() {
+ var args = [].slice.call(arguments);
+ _.each(methods, function(method) {
+ return ctx[method].apply(ctx, args);
+ });
+ };
+ }
+ }();
+ (function() {
+ "use strict";
+ var old, keys, methods;
+ old = $.fn.typeahead;
+ keys = {
+ www: "tt-www",
+ attrs: "tt-attrs",
+ typeahead: "tt-typeahead"
+ };
+ methods = {
+ initialize: function initialize(o, datasets) {
+ var www;
+ datasets = _.isArray(datasets) ? datasets : [].slice.call(arguments, 1);
+ o = o || {};
+ www = WWW(o.classNames);
+ return this.each(attach);
+ function attach() {
+ var $input, $wrapper, $hint, $menu, defaultHint, defaultMenu, eventBus, input, menu, typeahead, MenuConstructor;
+ _.each(datasets, function(d) {
+ d.highlight = !!o.highlight;
+ });
+ $input = $(this);
+ $wrapper = $(www.html.wrapper);
+ $hint = $elOrNull(o.hint);
+ $menu = $elOrNull(o.menu);
+ defaultHint = o.hint !== false && !$hint;
+ defaultMenu = o.menu !== false && !$menu;
+ defaultHint && ($hint = buildHintFromInput($input, www));
+ defaultMenu && ($menu = $(www.html.menu).css(www.css.menu));
+ $hint && $hint.val("");
+ $input = prepInput($input, www);
+ if (defaultHint || defaultMenu) {
+ $wrapper.css(www.css.wrapper);
+ $input.css(defaultHint ? www.css.input : www.css.inputWithNoHint);
+ $input.wrap($wrapper).parent().prepend(defaultHint ? $hint : null).append(defaultMenu ? $menu : null);
+ }
+ MenuConstructor = defaultMenu ? DefaultMenu : Menu;
+ eventBus = new EventBus({
+ el: $input
+ });
+ input = new Input({
+ hint: $hint,
+ input: $input
+ }, www);
+ menu = new MenuConstructor({
+ node: $menu,
+ datasets: datasets
+ }, www);
+ typeahead = new Typeahead({
+ input: input,
+ menu: menu,
+ eventBus: eventBus,
+ minLength: o.minLength
+ }, www);
+ $input.data(keys.www, www);
+ $input.data(keys.typeahead, typeahead);
+ }
+ },
+ isEnabled: function isEnabled() {
+ var enabled;
+ ttEach(this.first(), function(t) {
+ enabled = t.isEnabled();
+ });
+ return enabled;
+ },
+ enable: function enable() {
+ ttEach(this, function(t) {
+ t.enable();
+ });
+ return this;
+ },
+ disable: function disable() {
+ ttEach(this, function(t) {
+ t.disable();
+ });
+ return this;
+ },
+ isActive: function isActive() {
+ var active;
+ ttEach(this.first(), function(t) {
+ active = t.isActive();
+ });
+ return active;
+ },
+ activate: function activate() {
+ ttEach(this, function(t) {
+ t.activate();
+ });
+ return this;
+ },
+ deactivate: function deactivate() {
+ ttEach(this, function(t) {
+ t.deactivate();
+ });
+ return this;
+ },
+ isOpen: function isOpen() {
+ var open;
+ ttEach(this.first(), function(t) {
+ open = t.isOpen();
+ });
+ return open;
+ },
+ open: function open() {
+ ttEach(this, function(t) {
+ t.open();
+ });
+ return this;
+ },
+ close: function close() {
+ ttEach(this, function(t) {
+ t.close();
+ });
+ return this;
+ },
+ select: function select(el) {
+ var success = false, $el = $(el);
+ ttEach(this.first(), function(t) {
+ success = t.select($el);
+ });
+ return success;
+ },
+ autocomplete: function autocomplete(el) {
+ var success = false, $el = $(el);
+ ttEach(this.first(), function(t) {
+ success = t.autocomplete($el);
+ });
+ return success;
+ },
+ moveCursor: function moveCursoe(delta) {
+ var success = false;
+ ttEach(this.first(), function(t) {
+ success = t.moveCursor(delta);
+ });
+ return success;
+ },
+ val: function val(newVal) {
+ var query;
+ if (!arguments.length) {
+ ttEach(this.first(), function(t) {
+ query = t.getVal();
+ });
+ return query;
+ } else {
+ ttEach(this, function(t) {
+ t.setVal(newVal);
+ });
+ return this;
+ }
+ },
+ destroy: function destroy() {
+ ttEach(this, function(typeahead, $input) {
+ revert($input);
+ typeahead.destroy();
+ });
+ return this;
+ }
+ };
+ $.fn.typeahead = function(method) {
+ if (methods[method]) {
+ return methods[method].apply(this, [].slice.call(arguments, 1));
+ } else {
+ return methods.initialize.apply(this, arguments);
+ }
+ };
+ $.fn.typeahead.noConflict = function noConflict() {
+ $.fn.typeahead = old;
+ return this;
+ };
+ function ttEach($els, fn) {
+ $els.each(function() {
+ var $input = $(this), typeahead;
+ (typeahead = $input.data(keys.typeahead)) && fn(typeahead, $input);
+ });
+ }
+ function buildHintFromInput($input, www) {
+ return $input.clone().addClass(www.classes.hint).removeData().css(www.css.hint).css(getBackgroundStyles($input)).prop("readonly", true).removeAttr("id name placeholder required").attr({
+ autocomplete: "off",
+ spellcheck: "false",
+ tabindex: -1
+ });
+ }
+ function prepInput($input, www) {
+ $input.data(keys.attrs, {
+ dir: $input.attr("dir"),
+ autocomplete: $input.attr("autocomplete"),
+ spellcheck: $input.attr("spellcheck"),
+ style: $input.attr("style")
+ });
+ $input.addClass(www.classes.input).attr({
+ autocomplete: "off",
+ spellcheck: false
+ });
+ try {
+ !$input.attr("dir") && $input.attr("dir", "auto");
+ } catch (e) {}
+ return $input;
+ }
+ function getBackgroundStyles($el) {
+ return {
+ backgroundAttachment: $el.css("background-attachment"),
+ backgroundClip: $el.css("background-clip"),
+ backgroundColor: $el.css("background-color"),
+ backgroundImage: $el.css("background-image"),
+ backgroundOrigin: $el.css("background-origin"),
+ backgroundPosition: $el.css("background-position"),
+ backgroundRepeat: $el.css("background-repeat"),
+ backgroundSize: $el.css("background-size")
+ };
+ }
+ function revert($input) {
+ var www, $wrapper;
+ www = $input.data(keys.www);
+ $wrapper = $input.parent().filter(www.selectors.wrapper);
+ _.each($input.data(keys.attrs), function(val, key) {
+ _.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val);
+ });
+ $input.removeData(keys.typeahead).removeData(keys.www).removeData(keys.attr).removeClass(www.classes.input);
+ if ($wrapper.length) {
+ $input.detach().insertAfter($wrapper);
+ $wrapper.remove();
+ }
+ }
+ function $elOrNull(obj) {
+ var isValid, $el;
+ isValid = _.isJQuery(obj) || _.isElement(obj);
+ $el = isValid ? $(obj).first() : [];
+ return $el.length ? $el : null;
+ }
+ })();
+}); \ No newline at end of file
diff --git a/app/assets/javascripts/users.js b/app/assets/javascripts/users.js
index 70d78fd..3dae526 100644
--- a/app/assets/javascripts/users.js
+++ b/app/assets/javascripts/users.js
@@ -6,7 +6,7 @@
"error_tag":"span",
"wrapper_error_class":"error",
"wrapper_tag":"div",
- "wrapper_class":"control-group"
+ "wrapper_class":"form-group"
}
//
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 9cd3a55..f42044b 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -9,15 +9,14 @@
//
// import bootstrap.
//
+@import "bootstrap-sprockets";
@import "bootstrap";
-@import "bootstrap-responsive";
-// backport bootstrap 3.2 features
-@import "backport";
//
// LEAP web app specific overrides
//
@import "leap";
+@import "twitter";
// And finally bootswatch style itself
// @import "bootswatch/cerulean/bootswatch";
diff --git a/app/assets/stylesheets/backport.scss b/app/assets/stylesheets/backport.scss
deleted file mode 100644
index cadb035..0000000
--- a/app/assets/stylesheets/backport.scss
+++ /dev/null
@@ -1,24 +0,0 @@
-//
-// Backporting styles from bootstrap 3.2
-//
-
-
-// List options
-
-// Unstyled keeps list items block level, just removes default browser padding and list-style
-.list-unstyled {
- padding-left: 0;
- list-style: none;
-}
-
-// Inline turns list items into inline-block
-.list-inline {
- @extend .list-unstyled;
- margin-left: -5px;
-
- > li {
- display: inline-block;
- padding-left: 5px;
- padding-right: 5px;
- }
-}
diff --git a/app/assets/stylesheets/leap.scss b/app/assets/stylesheets/leap.scss
index abbfc88..8c4d702 100644
--- a/app/assets/stylesheets/leap.scss
+++ b/app/assets/stylesheets/leap.scss
@@ -57,7 +57,6 @@
[class*="-icon-"] {
display: inline-block;
- @include ie7-restore-right-whitespace();
vertical-align: middle;
background-repeat: no-repeat;
margin-top: 1px;
@@ -92,8 +91,8 @@
//
input.large {
- font-size: $baseFontSize * 1.25;
- line-height: $baseLineHeight * 1.5;
+ font-size: $font-size-base * 1.25;
+ line-height: $line-height-base * 1.5;
}
.p {
@@ -120,9 +119,9 @@ input, textarea {
// like a label, but with no background
.label-clear {
- background-color: $white;
+ background-color: $body-bg;
text-shadow: none;
- color: $black;
+ color: $gray-base;
}
// force a black icon, even if bootstrap thinks differently
@@ -188,7 +187,7 @@ input, textarea {
font-weight: bold;
}
a {
- color: $textColor;
+ color: $text-color;
}
}
@@ -199,7 +198,7 @@ input, textarea {
}
.download {
a.btn {
- width: 15em;
+ width: 18em;
font-weight: bold;
small {
font-weight: normal;
@@ -228,9 +227,9 @@ input, textarea {
box-shadow: 0 2px 4px rgba(0,0,0,.1);
li.active {
a, a:hover {
- background-color: $linkColor;
- color: $white;
- border-color: darken($linkColor, 0%);
+ background-color: $link-color;
+ color: $body-bg;
+ border-color: darken($link-color, 0%);
cursor: pointer;
}
}
@@ -265,7 +264,7 @@ $footer-height: 100px;
$footer-border-width: 1px;
$footer-gutter: 20px; // vertical gap above footer
$footer-combined: $footer-height + $footer-border-width + $footer-gutter;
-$footer-color: $grayLighter !default;
+$footer-color: $gray-lighter !default;
html, body {
height: 100%;
@@ -313,7 +312,7 @@ html, body {
// border-top: $footer-border-width solid darken($footer-color, 10%);
background-color: $footer-color;
a {
- color: $black;
+ color: $gray-base;
margin: 0 5px;
}
}
diff --git a/app/assets/stylesheets/twitter.scss b/app/assets/stylesheets/twitter.scss
new file mode 100644
index 0000000..4419918
--- /dev/null
+++ b/app/assets/stylesheets/twitter.scss
@@ -0,0 +1,78 @@
+.twitter {
+ position: relative;
+}
+
+.twitter_header {
+ font-size: 16px;
+ text-align: left;
+ margin-bottom: 55px;
+ padding: 10px 8px;
+}
+
+.twitter_id {
+ position: absolute;
+}
+
+.twitter_image_frame {
+ display: block;
+ width: 40px;
+ height: 40px;
+ overflow: hidden;
+ position: absolute;
+ left: 0;
+ top: 0;
+ // background-image: url(/Avatar_Pic.png);
+ }
+
+.twitter_image_frame > img {
+ display: block;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 30;
+ width: 100%;
+ margin: auto;
+ }
+
+.twitter_name {
+ padding-left: 55px;
+ line-height: 20px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-size: 16px;
+}
+
+// Body = displays tweets
+.twitter_list {
+ box-sizing: border-box;
+}
+
+.tweet {
+ border-top-style: solid;
+ border-color: lightgrey;
+ padding: 10px 8px;
+}
+
+.tweet_text {
+box-sizing: border-box;
+}
+
+.tweet_text_date {
+ text-align: right;
+ padding-top: 4px;
+ font-size: 12px ;
+}
+
+.twitter_footer {
+ border-top-style: solid;
+ border-color: lightgrey;
+ padding: 10px 8px;
+ font-style: italic;
+ font-size: 12px;
+}
+
+.more_tweets_link {
+ text-align: right;
+ font-size: 12px;
+}
diff --git a/app/controllers/account_controller.rb b/app/controllers/account_controller.rb
new file mode 100644
index 0000000..42e8983
--- /dev/null
+++ b/app/controllers/account_controller.rb
@@ -0,0 +1,19 @@
+class AccountController < ApplicationController
+
+ before_filter :require_registration_allowed
+ before_filter :redirect_if_logged_in
+
+ respond_to :html
+
+ def new
+ @user = User.new
+ end
+
+ protected
+
+ def require_registration_allowed
+ unless APP_CONFIG[:allow_registration]
+ redirect_to home_path
+ end
+ end
+end
diff --git a/app/controllers/account_settings_controller.rb b/app/controllers/account_settings_controller.rb
deleted file mode 100644
index e69de29..0000000
--- a/app/controllers/account_settings_controller.rb
+++ /dev/null
diff --git a/app/controllers/v1/certs_controller.rb b/app/controllers/api/certs_controller.rb
index ffa6e35..46a84d3 100644
--- a/app/controllers/v1/certs_controller.rb
+++ b/app/controllers/api/certs_controller.rb
@@ -1,4 +1,4 @@
-class V1::CertsController < ApiController
+class Api::CertsController < ApiController
before_filter :require_login, :unless => :anonymous_access_allowed?
before_filter :require_enabled
diff --git a/app/controllers/v1/configs_controller.rb b/app/controllers/api/configs_controller.rb
index 4a6f455..0f9b8a6 100644
--- a/app/controllers/v1/configs_controller.rb
+++ b/app/controllers/api/configs_controller.rb
@@ -1,17 +1,15 @@
-class V1::ConfigsController < ApiController
+class Api::ConfigsController < ApiController
include ControllerExtension::JsonFile
before_filter :require_login, :unless => :anonymous_access_allowed?
before_filter :sanitize_id, only: :show
- before_filter :lookup_file, only: :show
- before_filter :fetch_file, only: :show
def index
render json: {services: service_paths}
end
def show
- send_file
+ send_file lookup_file
end
protected
@@ -23,7 +21,11 @@ class V1::ConfigsController < ApiController
}
def service_paths
- Hash[SERVICE_IDS.map{|k,v| [k,"/1/configs/#{v}.json"] } ]
+ Hash[SERVICE_IDS.map{|k,v| [k,"/#{api_version}/configs/#{v}.json"] } ]
+ end
+
+ def api_version
+ ["1", "2"].include?(params[:version]) ? params[:version] : "2"
end
def sanitize_id
@@ -34,6 +36,6 @@ class V1::ConfigsController < ApiController
def lookup_file
path = APP_CONFIG[:config_file_paths][@id]
not_found if path.blank?
- @filename = Rails.root.join path
+ Rails.root.join path
end
end
diff --git a/app/controllers/v1/identities_controller.rb b/app/controllers/api/identities_controller.rb
index 4efd1f5..de4910a 100644
--- a/app/controllers/v1/identities_controller.rb
+++ b/app/controllers/api/identities_controller.rb
@@ -1,8 +1,10 @@
-module V1
+module Api
class IdentitiesController < ApiController
before_filter :token_authenticate
before_filter :require_monitor
+ respond_to :json
+
def show
@identity = Identity.find_by_address(params[:id])
if @identity
diff --git a/app/controllers/v1/messages_controller.rb b/app/controllers/api/messages_controller.rb
index c0ca0c7..a69a40a 100644
--- a/app/controllers/v1/messages_controller.rb
+++ b/app/controllers/api/messages_controller.rb
@@ -1,4 +1,4 @@
-module V1
+module Api
class MessagesController < ApiController
before_filter :require_login
diff --git a/app/controllers/v1/services_controller.rb b/app/controllers/api/services_controller.rb
index 523eb44..58e129c 100644
--- a/app/controllers/v1/services_controller.rb
+++ b/app/controllers/api/services_controller.rb
@@ -1,7 +1,9 @@
-class V1::ServicesController < ApiController
+class Api::ServicesController < ApiController
before_filter :require_login, :unless => :anonymous_access_allowed?
+ respond_to :json
+
def show
respond_with current_user.effective_service_level
end
diff --git a/app/controllers/v1/sessions_controller.rb b/app/controllers/api/sessions_controller.rb
index a343d9b..178f86e 100644
--- a/app/controllers/v1/sessions_controller.rb
+++ b/app/controllers/api/sessions_controller.rb
@@ -1,7 +1,8 @@
-module V1
+module Api
class SessionsController < ApiController
before_filter :require_login, only: :destroy
+ respond_to :json
def new
@session = Session.new
diff --git a/app/controllers/v1/smtp_certs_controller.rb b/app/controllers/api/smtp_certs_controller.rb
index 5760645..d9eab7d 100644
--- a/app/controllers/v1/smtp_certs_controller.rb
+++ b/app/controllers/api/smtp_certs_controller.rb
@@ -1,4 +1,4 @@
-class V1::SmtpCertsController < ApiController
+class Api::SmtpCertsController < ApiController
before_filter :require_login
before_filter :require_email_account
diff --git a/app/controllers/v1/users_controller.rb b/app/controllers/api/users_controller.rb
index 6640d10..709e076 100644
--- a/app/controllers/v1/users_controller.rb
+++ b/app/controllers/api/users_controller.rb
@@ -1,4 +1,4 @@
-module V1
+module Api
class UsersController < ApiController
include ControllerExtension::FetchUser
@@ -28,12 +28,20 @@ module V1
@user = User.find(params[:id])
end
if @user
- respond_with @user
+ respond_with user_response
else
not_found
end
end
+ def user_response
+ @user.to_hash.tap do |user_hash|
+ if @user == current_user
+ user_hash['is_admin'] = @user.is_admin?
+ end
+ end
+ end
+
def create
if current_user.is_monitor?
create_test_account
@@ -50,8 +58,7 @@ module V1
end
def destroy
- destroy_identity = current_user.is_monitor? || params[:identities] == "destroy"
- @user.account.destroy(destroy_identity)
+ @user.account.destroy(release_handles)
if @user == current_user
logout
end
@@ -60,6 +67,10 @@ module V1
private
+ def release_handles
+ current_user.is_monitor? || params[:identities] == "destroy"
+ end
+
# tester auth can only create test users.
def create_test_account
if User::is_test?(params[:user][:login])
diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb
index 70b3cac..95c8f57 100644
--- a/app/controllers/api_controller.rb
+++ b/app/controllers/api_controller.rb
@@ -1,7 +1,6 @@
class ApiController < ApplicationController
skip_before_filter :verify_authenticity_token
- respond_to :json
protected
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 079dc18..8d08a2c 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -4,13 +4,24 @@ class ApplicationController < ActionController::Base
before_filter :no_cache_header
before_filter :no_frame_header
before_filter :language_header
+
+ # UPGRADE: this won't be needed in Rails 5 anymore as it's the default
+ # behavior if a template is present but a different format would be
+ # rendered and that template is not present
+ before_filter :verify_request_format!, if: :mime_types_specified
+
rescue_from StandardError, :with => :default_error_handler
- rescue_from RestClient::Exception, :with => :default_error_handler
+ rescue_from CouchRest::Exception, :with => :default_error_handler
ActiveSupport.run_load_hooks(:application_controller, self)
protected
+ def mime_types_specified
+ mimes = collect_mimes_from_class_level
+ mimes.present?
+ end
+
def default_error_handler(exc)
respond_to do |format|
format.json { render_json_error(exc) }
diff --git a/app/controllers/controller_extension/fetch_user.rb b/app/controllers/controller_extension/fetch_user.rb
index 97f92fa..632291d 100644
--- a/app/controllers/controller_extension/fetch_user.rb
+++ b/app/controllers/controller_extension/fetch_user.rb
@@ -22,7 +22,7 @@ module ControllerExtension::FetchUser
@user = User.find(params[:user_id] || params[:id])
if current_user.is_admin? || current_user.is_monitor?
if @user.nil?
- not_found(t(:no_such_thing, :thing => 'user'), users_url)
+ not_found(t(:no_such_user), users_url)
elsif current_user.is_monitor?
access_denied unless @user.is_test?
end
diff --git a/app/controllers/controller_extension/json_file.rb b/app/controllers/controller_extension/json_file.rb
index 6be919a..df9cf55 100644
--- a/app/controllers/controller_extension/json_file.rb
+++ b/app/controllers/controller_extension/json_file.rb
@@ -4,20 +4,25 @@ module ControllerExtension::JsonFile
protected
- def send_file
- if stale?(:last_modified => @file.mtime)
- response.content_type = 'application/json'
- render :text => @file.read
+ def send_file(filename)
+ file = fetch_file(filename)
+ if file.present?
+ send_file_or_cache_hit(file)
+ else
+ not_found
end
end
- def fetch_file
- if File.exists?(@filename)
- @file = File.new(@filename)
- else
- not_found
+ def send_file_or_cache_hit(file)
+ if stale?(:last_modified => file.mtime)
+ response.content_type = 'application/json'
+ render :text => file.read
end
end
+ def fetch_file(filename)
+ File.new(filename) if File.exist?(filename)
+ end
+
end
diff --git a/app/controllers/errors_controller.rb b/app/controllers/errors_controller.rb
index d869ab5..80c270f 100644
--- a/app/controllers/errors_controller.rb
+++ b/app/controllers/errors_controller.rb
@@ -1,5 +1,7 @@
# We render http errors ourselves so we can customize them
class ErrorsController < ApplicationController
+ respond_to :html
+
# 404
def not_found
render status: 404
diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb
index 1d62178..86c36e9 100644
--- a/app/controllers/home_controller.rb
+++ b/app/controllers/home_controller.rb
@@ -1,6 +1,8 @@
class HomeController < ApplicationController
layout 'home'
+ respond_to :html
+
def index
if logged_in?
redirect_to current_user
diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb
index e0f39e3..b9c601a 100644
--- a/app/controllers/pages_controller.rb
+++ b/app/controllers/pages_controller.rb
@@ -2,7 +2,9 @@
# Render static pages
#
+
class PagesController < ApplicationController
+ respond_to :html
def show
@show_navigation = false
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 34d4f53..18e5216 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -1,6 +1,7 @@
class SessionsController < ApplicationController
before_filter :redirect_if_logged_in, :only => [:new]
+ respond_to :html, :json
def new
@session = Session.new
@@ -16,7 +17,7 @@ class SessionsController < ApplicationController
end
#
- # Warden will catch all 401s and run this instead:
+ # Warden will catch all 401s and triggers this action:
#
def unauthenticated
login_required
diff --git a/app/controllers/static_config_controller.rb b/app/controllers/static_config_controller.rb
index c78e006..46e7cd2 100644
--- a/app/controllers/static_config_controller.rb
+++ b/app/controllers/static_config_controller.rb
@@ -5,13 +5,9 @@ class StaticConfigController < ActionController::Base
include ControllerExtension::JsonFile
before_filter :set_minimum_client_version
- before_filter :set_filename
- before_filter :fetch_file
-
- PROVIDER_JSON = Rails.root.join('config', 'provider', 'provider.json')
def provider
- send_file
+ send_file provider_json
end
protected
@@ -23,7 +19,8 @@ class StaticConfigController < ActionController::Base
APP_CONFIG[:minimum_client_version].to_s
end
- def set_filename
- @filename = PROVIDER_JSON
+ def provider_json
+ Rails.root.join APP_CONFIG[:config_file_paths]['provider']
end
+
end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 1404b0e..0a0f551 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -5,11 +5,9 @@
class UsersController < ApplicationController
include ControllerExtension::FetchUser
- before_filter :require_login, :except => [:new]
- before_filter :redirect_if_logged_in, :only => [:new]
+ before_filter :require_login
before_filter :require_admin, :only => [:index, :deactivate, :enable]
- before_filter :fetch_user, :only => [:show, :edit, :update, :destroy, :deactivate, :enable]
- before_filter :require_registration_allowed, only: :new
+ before_filter :fetch_user, :except => [:index]
respond_to :html
@@ -27,25 +25,12 @@ class UsersController < ApplicationController
@users = @users.limit(100)
end
- def new
- @user = User.new
- end
-
def show
end
def edit
end
- ## added so updating service level works, but not sure we will actually want this. also not sure that this is place to prevent user from updating own effective service level, but here as placeholder:
- def update
- @user.update_attributes(params[:user]) unless (!admin? and params[:user][:effective_service_level])
- if @user.valid?
- flash[:notice] = I18n.t(:changes_saved)
- end
- respond_with @user, :location => edit_user_path(@user)
- end
-
def deactivate
@user.account.disable
flash[:notice] = I18n.t("actions.user_disabled_message", username: @user.username)
@@ -73,10 +58,11 @@ class UsersController < ApplicationController
protected
- def require_registration_allowed
- unless APP_CONFIG[:allow_registration]
- redirect_to home_path
+ def user_params
+ if admin?
+ params.require(:user).permit(:effective_service_level)
+ else
+ params.require(:user).permit(:password, :password_confirmation)
end
end
-
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 6de5e1b..920186d 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -19,7 +19,8 @@ module ApplicationHelper
# http://twitter.github.io/bootstrap/base-css.html#icons
#
def icon(name, color=nil)
- "<i class=\"icon-#{name} #{color_class(color)}\"></i> ".html_safe
+ content_tag :span, '',
+ class: "glyphicon glyphicon-#{name} #{color_class(color)}"
end
def big_icon(name, color=nil)
diff --git a/app/helpers/link_helper.rb b/app/helpers/link_helper.rb
index ddb063e..b74e1d7 100644
--- a/app/helpers/link_helper.rb
+++ b/app/helpers/link_helper.rb
@@ -39,6 +39,10 @@ module LinkHelper
def btn(*args, &block)
html_options = extract_html_options!(args, &block)
type = Array(html_options.delete(:type))
+ btn_opts = [:default, :primary, :success, :info, :warning, :danger, :link]
+ if (type & btn_opts).blank?
+ type << :default
+ end
type.map! {|t| "btn-#{t}"}
html_options[:class] = concat_classes(html_options[:class], 'btn', type)
args[0] = t(args[0]) if args[0].is_a?(Symbol)
diff --git a/app/helpers/navigation_helper.rb b/app/helpers/navigation_helper.rb
index 2639246..1df840c 100644
--- a/app/helpers/navigation_helper.rb
+++ b/app/helpers/navigation_helper.rb
@@ -66,9 +66,9 @@ module NavigationHelper
end
def extract_icon!(options)
- icon = options.delete(:icon)
- if icon.present?
- content_tag(:i, '', class: "icon-#{icon}")
+ name = options.delete(:icon)
+ if name.present?
+ icon name
else
""
end
diff --git a/app/helpers/twitter_helper.rb b/app/helpers/twitter_helper.rb
new file mode 100644
index 0000000..7ce28be
--- /dev/null
+++ b/app/helpers/twitter_helper.rb
@@ -0,0 +1,88 @@
+module TwitterHelper
+ def twitter_enabled
+ if Rails.application.secrets.twitter
+ Rails.application.secrets.twitter['enabled'] == true
+ end
+ end
+
+ def twitter_client
+ Twitter::REST::Client.new do |config|
+ config.bearer_token = Rails.application.secrets.twitter['bearer_token']
+ end
+ end
+
+ def twitter_handle
+ Rails.application.secrets.twitter['twitter_handle']
+ end
+
+ def twitter_user_info
+ $twitter_user_info ||= []
+ end
+
+ def update_twitter_info
+ twitter_user_info[0] = Time.now
+ twitter_user_info[1] = twitter_client.user(twitter_handle).name
+ twitter_user_info[2] = twitter_client.user_timeline(twitter_handle, {:count => 200}).select{ |tweet| tweet.text.start_with?('RT','@')==false}
+ if twitter_user_info[2] == nil
+ error_handling
+ twitter_user_info[3] = "The twitter handle does not exist or the account's tweets are protected. Please change the privacy settings accordingly or contact your provider-admin."
+ end
+ rescue Twitter::Error::BadRequest
+ error_handling
+ twitter_user_info[3] = "The request for displaying tweets is invalid or cannot be otherwise served."
+ rescue Twitter::Error::Unauthorized
+ error_handling
+ twitter_user_info[3] = "Your bearer-token is invalid or the account's tweets are protected and cannot be displayed. Please change the privacy settings of the corresponding account, check your bearer-token in the secrets-file or contact your provider-admin to have the tweets shown."
+ rescue Twitter::Error::Forbidden
+ error_handling
+ twitter_user_info[3] = "The request for displaying tweets is understood, but it has been refused or access is not allowed."
+ rescue Twitter::Error::NotAcceptable
+ error_handling
+ twitter_user_info[3] = "An invalid format is specified in the request for displaying tweets."
+ rescue Twitter::Error::TooManyRequests
+ error_handling
+ twitter_user_info[3] = "The rate-limit for accessing the tweets is reached. You should be able to display tweets in a couple of minutes."
+ rescue Twitter::Error::NotFound
+ error_handling
+ twitter_user_info[3] = "The twitter hanlde does not exist."
+ rescue Twitter::Error
+ error_handling
+ twitter_user_info[3] = "An error occured while fetching the tweets."
+ end
+
+ def error_handling
+ twitter_user_info[2] = []
+ twitter_user_info
+ end
+
+ def cached_info
+ if twitter_user_info[0] == nil
+ update_twitter_info
+ else
+ if Time.now > twitter_user_info[0] + 15.minutes
+ update_twitter_info
+ end
+ end
+ twitter_user_info
+ end
+
+ def twitter_name
+ cached_info[1]
+ end
+
+ def num_of_tweets
+ 3
+ end
+
+ def tweets
+ cached_info[2].take(num_of_tweets)
+ end
+
+ def error_message
+ cached_info[3]
+ end
+
+ def all_tweets_count
+ twitter_user_info[2].count
+ end
+end
diff --git a/app/models/account.rb b/app/models/account.rb
index 7310250..d722caa 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -69,15 +69,13 @@ class Account
@user.refresh_identity
end
- def destroy(destroy_identity=false)
+ def destroy(release_handles=false)
return unless @user
if !@user.is_tmp?
- if destroy_identity == false
- @user.identities.each do |id|
+ @user.identities.each do |id|
+ if release_handles == false
id.orphan!
- end
- else
- @user.identities.each do |id|
+ else
id.destroy
end
end
diff --git a/app/models/api_monitor_user.rb b/app/models/api_monitor_user.rb
new file mode 100644
index 0000000..d0fe411
--- /dev/null
+++ b/app/models/api_monitor_user.rb
@@ -0,0 +1,11 @@
+#
+# A user that has limited admin access, to be used
+# for running monitor tests against a live production
+# installation.
+#
+class ApiMonitorUser < ApiUser
+ def is_monitor?
+ true
+ end
+end
+
diff --git a/app/models/api_user.rb b/app/models/api_user.rb
index 2efe1cb..c70cccb 100644
--- a/app/models/api_user.rb
+++ b/app/models/api_user.rb
@@ -3,21 +3,10 @@ class ApiUser < AnonymousUser
end
#
-# A user that has limited admin access, to be used
-# for running monitor tests against a live production
-# installation.
-#
-class ApiMonitorUser < ApiUser
- def is_monitor?
- true
- end
-end
-
-#
# Not yet supported:
#
#class ApiAdminUser < ApiUser
# def is_admin?
# true
# end
-#end \ No newline at end of file
+#end
diff --git a/app/models/identity.rb b/app/models/identity.rb
index f987e4e..92f8f7a 100644
--- a/app/models/identity.rb
+++ b/app/models/identity.rb
@@ -1,3 +1,5 @@
+require 'login_format_validation'
+require 'local_email'
#
# Identity states:
#
diff --git a/app/models/session.rb b/app/models/session.rb
index 0d7e10e..21e4dc6 100644
--- a/app/models/session.rb
+++ b/app/models/session.rb
@@ -1,3 +1,5 @@
+require 'login_format_validation'
+
class Session < SRP::Session
include ActiveModel::Validations
include LoginFormatValidation
diff --git a/app/models/token.rb b/app/models/token.rb
index b398fcb..8ac32b8 100644
--- a/app/models/token.rb
+++ b/app/models/token.rb
@@ -59,8 +59,8 @@ class Token < CouchRest::Model::Base
# So let's make sure we don't crash if they disappeared
def destroy_with_rescue
destroy_without_rescue
- rescue RestClient::ResourceNotFound # do nothing it's gone already
- rescue RestClient::Conflict # do nothing - it's been updated - #7670
+ rescue CouchRest::NotFound
+ rescue CouchRest::Conflict # do nothing - it's been updated - #7670
end
alias_method_chain :destroy, :rescue
diff --git a/app/models/user.rb b/app/models/user.rb
index cb093cf..9cebbca 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1,8 +1,10 @@
+require 'login_format_validation'
+require 'local_email'
+require 'temporary_user'
+
class User < CouchRest::Model::Base
include LoginFormatValidation
- use_database :users
-
property :login, String, :accessible => true
property :password_verifier, String, :accessible => true
property :password_salt, String, :accessible => true
@@ -71,12 +73,16 @@ class User < CouchRest::Model::Base
end
def to_json(options={})
+ to_hash.to_json(options)
+ end
+
+ def to_hash()
{
:login => self.login,
:ok => self.valid?,
:id => self.id,
- :enabled => self.enabled?
- }.to_json(options)
+ :enabled => self.enabled?,
+ }
end
def salt
diff --git a/app/views/users/new.html.haml b/app/views/account/new.html.haml
index bc0b1af..d40259e 100644
--- a/app/views/users/new.html.haml
+++ b/app/views/account/new.html.haml
@@ -1,18 +1,18 @@
-#
-# This form is handled entirely by javascript
-# Please take care when changing element ids.
--#
--# The form is hidden when no js is available
+-#
+-# The form is hidden when no js is available
-# to prevent submission in the clear.
-#
- form_options = {url: '/not-used', html: {id: 'new_user', class: user_form_class('form-horizontal'), style: 'display:none'}, validate: true}
-.span1
-.span9
+.col-md-1
+.col-md-9
%h2=t :signup
.lead=t :signup_info
- = render :partial => 'warnings'
+ = render "sessions/warnings"
= simple_form_for(@user, form_options) do |f|
= f.input :login, :label => t(:username), :required => false, :input_html => { :id => :srp_username }
= f.input :password, :label => t(:password), :required => false, :validate => true, :input_html => { :id => :srp_password }
diff --git a/app/views/v1/sessions/new.json.erb b/app/views/api/sessions/new.json.erb
index 36154b8..36154b8 100644
--- a/app/views/v1/sessions/new.json.erb
+++ b/app/views/api/sessions/new.json.erb
diff --git a/app/views/common/_action_buttons.html.haml b/app/views/common/_action_buttons.html.haml
index 81ebf67..eb663c0 100644
--- a/app/views/common/_action_buttons.html.haml
+++ b/app/views/common/_action_buttons.html.haml
@@ -1,14 +1,14 @@
.home-buttons
- .row-fluid.second
- .login.span4
+ .row.second
+ .login.col-md-4
%span.link= btn icon('ok-sign') + t(:login), login_path
%span.info= t(:login_info, default: "")
- if APP_CONFIG[:allow_registration]
- .signup.span4
+ .signup.col-md-4
%span.link= btn icon('user') + t(:signup), signup_path
%span.info= t(:signup_info, default: "")
- else
- .signup.span4
- .help.span4
+ .signup.col-md-4
+ .help.col-md-4
%span.link= btn icon('question-sign') + t(:get_help), new_ticket_path
%span.info= t(:support_info, default: "")
diff --git a/app/views/common/_download_button.html.haml b/app/views/common/_download_button.html.haml
index 9c26860..1278230 100644
--- a/app/views/common/_download_button.html.haml
+++ b/app/views/common/_download_button.html.haml
@@ -1,8 +1,8 @@
.home-buttons
- .row-fluid.first
- .span2
- .download.span8
+ .row.first
+ .col-md-2
+ .download.col-md-8
= btn client_download_url, type: [:large, :primary] do
= big_icon('download')
= t(:download_bitmask)
- .span2
+ .col-md-2
diff --git a/app/views/common/_home_page_buttons.html.haml b/app/views/common/_home_page_buttons.html.haml
index fc6348e..cfe3734 100644
--- a/app/views/common/_home_page_buttons.html.haml
+++ b/app/views/common/_home_page_buttons.html.haml
@@ -1,6 +1,6 @@
= render 'common/download_button'
- if local_assigns[:divider]
- .row-fluid
- .span12
+ .row
+ .col-md-12
= render local_assigns[:divider]
= render 'common/action_buttons'
diff --git a/app/views/home/_content.html.haml b/app/views/home/_content.html.haml
index d96a1e0..c7902b1 100644
--- a/app/views/home/_content.html.haml
+++ b/app/views/home/_content.html.haml
@@ -1,12 +1,31 @@
-.row-fluid
- %h1= t(:welcome, :provider => APP_CONFIG[:domain])
- .p=t(:welcome_message_html)
-
-.row-fluid
- = home_page_buttons
-
- - if Rails.env == 'development'
- .row-fluid
- %hr
- %p
- = link_to "make donation", new_payment_path if APP_CONFIG[:payment].present?
+- if twitter_enabled == true
+ .col-md-8
+ .row
+ %h1= t(:welcome, :provider => APP_CONFIG[:domain])
+ .p=t(:welcome_message_html)
+
+ .row
+ = home_page_buttons
+ .row
+ = render 'home/provider_message'
+
+ .col-md-1
+
+ .col-md-3
+ .row
+ = render 'twitter/index'
+- else
+ .row
+ %h1= t(:welcome, :provider => APP_CONFIG[:domain])
+ .p=t(:welcome_message_html)
+
+ .row
+ = home_page_buttons
+ .row
+ = render 'home/provider_message'
+
+ - if Rails.env == 'development'
+ .row
+ %hr
+ %p
+ = link_to "make donation", new_payment_path if APP_CONFIG[:payment].present?
diff --git a/app/views/home/_provider_message.html.haml b/app/views/home/_provider_message.html.haml
new file mode 100644
index 0000000..928215a
--- /dev/null
+++ b/app/views/home/_provider_message.html.haml
@@ -0,0 +1,2 @@
+-# please overwrite me in your customization files at
+-# config/customization/views/home/_provider_message.html.haml
diff --git a/app/views/layouts/_content.html.haml b/app/views/layouts/_content.html.haml
index 07f9189..b72b7c1 100644
--- a/app/views/layouts/_content.html.haml
+++ b/app/views/layouts/_content.html.haml
@@ -8,7 +8,7 @@
- content = yield
- if @show_navigation && @user
- .span2
+ .col-md-2
= render 'layouts/navigation'
.span10
= render 'layouts/messages'
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 0aeda8b..e242a5f 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -15,13 +15,13 @@
#main
.container-fluid
- if @show_navigation && !admin?
- .row-fluid
+ .row
%h1= t(:user_control_panel)
- if logged_in?
- .row-fluid
- .span12
+ .row
+ .col-md-12
= render 'layouts/header'
- .row-fluid
+ .row
= render 'layouts/content'
#push
-# #push is used for sticky footer in bootstrap 2. remove when upgrading to bootstrap 3
diff --git a/app/views/users/_warnings.html.haml b/app/views/sessions/_warnings.html.haml
index baf80a4..baf80a4 100644
--- a/app/views/users/_warnings.html.haml
+++ b/app/views/sessions/_warnings.html.haml
diff --git a/app/views/sessions/new.html.haml b/app/views/sessions/new.html.haml
index bb7e4bd..6695123 100644
--- a/app/views/sessions/new.html.haml
+++ b/app/views/sessions/new.html.haml
@@ -1,8 +1,8 @@
-.span1
-.span9
+.col-md-1
+.col-md-9
%h2=t :login
.lead=t :login_info
- = render :partial => 'users/warnings'
+ = render 'warnings'
= simple_form_for [:api, @session], validate: true, html: { id: :new_session, class: 'form-horizontal hidden js-show', style: "display:none;" } do |f|
= f.input :login, :required => false, :label => t(:username), :input_html => { :id => :srp_username }
= f.input :password, :required => false, :input_html => { :id => :srp_password }
diff --git a/app/views/twitter/_index.html.erb b/app/views/twitter/_index.html.erb
new file mode 100644
index 0000000..6e592c7
--- /dev/null
+++ b/app/views/twitter/_index.html.erb
@@ -0,0 +1,34 @@
+<% if twitter_enabled == true %>
+ <div class="twitter">
+
+ <div class="twitter_header">
+ <div class="twitter_id">
+ <div class="twitter_image_frame"><%= image_tag("Avatar_Pic.png") %></div>
+ <div class="twitter_name"><%= twitter_name%><br><a href="https://twitter.com/<%= twitter_handle %>">@<%= twitter_handle %></a></div>
+ </div>
+ </div>
+
+ <div class="twitter_list">
+ <%if tweets == [] then%><%= error_message %><% end %>
+ <% tweets.each do |e| %>
+ <div class="tweet">
+ <div class="tweet_text"><%= sanitize(e.text) %>
+ </div>
+ <div class="tweet_text_date">tweeted on <% t = e.created_at%> <%= t.strftime("%m/%d/%y").to_s %>
+ </div>
+ </div>
+ <% end %>
+ <%if all_tweets_count > num_of_tweets then%>
+ <div class="tweet">
+ <div class="more_tweets_link"><a href="https://twitter.com/<%= twitter_handle %>">Show more tweets...</a>
+ </div>
+ </div>
+ <% end %>
+ </div>
+
+ <div class="twitter_footer">
+
+ <p>This feed uses a Ruby interface to access the Twitter API. Within LEAP Twitter does not track you.</p>
+ </div>
+</div>
+<% end %>
diff --git a/app/views/users/_change_password.html.haml b/app/views/users/_change_password.html.haml
index 425e3ee..64a4d0a 100644
--- a/app/views/users/_change_password.html.haml
+++ b/app/views/users/_change_password.html.haml
@@ -15,7 +15,7 @@
= f.input :login, :label => t(:username), :required => false, :input_html => {:id => :srp_username}
= f.input :password, :required => false, :validate => true, :input_html => { :id => :srp_password }
= f.input :password_confirmation, :required => false, :input_html => { :id => :srp_password_confirmation }
- .control-group
+ .form-group
.controls
= f.submit t(:save), :class => 'btn btn-primary'
diff --git a/app/views/users/_change_pgp_key.html.haml b/app/views/users/_change_pgp_key.html.haml
index e465125..af3eb5e 100644
--- a/app/views/users/_change_pgp_key.html.haml
+++ b/app/views/users/_change_pgp_key.html.haml
@@ -8,6 +8,6 @@
= simple_form_for [:api, @user], form_options do |f|
%legend= t(:advanced_options)
= f.input :public_key, :as => :text, :hint => t(:use_ascii_key), :input_html => {:class => "full-width", :rows => 4}
- .control-group
+ .form-group
.controls
= f.submit t(:save), :class => 'btn', :data => {"loading-text" => "Saving..."}
diff --git a/app/views/users/_change_service_level.html.haml b/app/views/users/_change_service_level.html.haml
index 42315a2..32ea8c0 100644
--- a/app/views/users/_change_service_level.html.haml
+++ b/app/views/users/_change_service_level.html.haml
@@ -1,8 +1,13 @@
--# TODO: probably won't want here, but here for now. Also, we will need way to ensure payment if they pick a non-free plan.
--#
--# SERVICE LEVEL
--#
-- if APP_CONFIG[:service_levels]
+:ruby
+ # DISABLED! this form points to a route that does not exist.
+ # It's a draft for implementing service levels.
+ # TODO: probably won't want here, but here for now.
+ # We will need way to ensure payment for a non-free plan.
+ #
+ # SERVICE LEVEL
+ #
+ #
+- if APP_CONFIG[:service_levels] && false
- form_options = {:html => {:class => user_form_class('form-horizontal'), :id => 'update_service_level', :data => {token: session[:token]}}, :validate => true}
= simple_form_for @user, form_options do |f|
%legend= t(:service_level)
@@ -13,6 +18,6 @@
%p
= t(:effective_service_level)
= f.select :effective_service_level_code, ServiceLevel.select_options, :selected => @user.effective_service_level.id
- .control-group
+ .form-group
.controls
= f.submit t(:save), :class => 'btn', :data => {"loading-text" => "Saving..."}
diff --git a/app/views/users/_contact_email.html.haml b/app/views/users/_contact_email.html.haml
index ad768b7..1627ff6 100644
--- a/app/views/users/_contact_email.html.haml
+++ b/app/views/users/_contact_email.html.haml
@@ -4,6 +4,6 @@
= f.input :contact_email, :label => false
-# %p= t(:public_key)
-# = f.text_area :contact_email_key, {:class => "full-width", :rows => 4}
- .control-group
+ .form-group
.controls
= f.submit t(:save), :class => 'btn', :data => {"loading-text" => "Saving..."} \ No newline at end of file
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index da8e467..3bdf952 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -1,6 +1,6 @@
= render 'overview'
.container-fluid
- .row-fluid
+ .row
%h4 To use bitmask services:
= btn client_download_url, type: "primary" do
%i.icon-arrow-down.icon-white
diff --git a/bin/bundle b/bin/bundle
new file mode 100755
index 0000000..66e9889
--- /dev/null
+++ b/bin/bundle
@@ -0,0 +1,3 @@
+#!/usr/bin/env ruby
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
+load Gem.bin_path('bundler', 'bundle')
diff --git a/bin/rails b/bin/rails
new file mode 100755
index 0000000..5191e69
--- /dev/null
+++ b/bin/rails
@@ -0,0 +1,4 @@
+#!/usr/bin/env ruby
+APP_PATH = File.expand_path('../../config/application', __FILE__)
+require_relative '../config/boot'
+require 'rails/commands'
diff --git a/bin/rake b/bin/rake
new file mode 100755
index 0000000..1724048
--- /dev/null
+++ b/bin/rake
@@ -0,0 +1,4 @@
+#!/usr/bin/env ruby
+require_relative '../config/boot'
+require 'rake'
+Rake.application.run
diff --git a/bin/setup b/bin/setup
new file mode 100755
index 0000000..acdb2c1
--- /dev/null
+++ b/bin/setup
@@ -0,0 +1,29 @@
+#!/usr/bin/env ruby
+require 'pathname'
+
+# path to your application root.
+APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
+
+Dir.chdir APP_ROOT do
+ # This script is a starting point to setup your application.
+ # Add necessary setup steps to this file:
+
+ puts "== Installing dependencies =="
+ system "gem install bundler --conservative"
+ system "bundle check || bundle install"
+
+ # puts "\n== Copying sample files =="
+ # unless File.exist?("config/database.yml")
+ # system "cp config/database.yml.sample config/database.yml"
+ # end
+
+ puts "\n== Preparing database =="
+ system "bin/rake db:setup"
+
+ puts "\n== Removing old logs and tempfiles =="
+ system "rm -f log/*"
+ system "rm -rf tmp/cache"
+
+ puts "\n== Restarting application server =="
+ system "touch tmp/restart.txt"
+end
diff --git a/config/application.rb b/config/application.rb
index b13c7d9..0e00356 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -4,20 +4,16 @@ require File.expand_path('../boot', __FILE__)
# require "active_record/railtie"
require "action_controller/railtie"
require "action_mailer/railtie"
-require "active_resource/railtie"
require "sprockets/railtie"
require "rails/test_unit/railtie"
-if defined?(Bundler)
- # If you precompile assets before deploying to production, use this line
- Bundler.require(*Rails.groups(:assets => %w(development test)))
- # If you want your assets lazily compiled in production, use this line
- # Bundler.require(:default, :assets, Rails.env)
-end
+# Require the gems listed in Gemfile, including any gems
+# you've limited to :test, :development, or :production.
+Bundler.require(*Rails.groups)
APP_CONFIG = ["defaults.yml", "config.yml"].inject({}) {|config, file|
filepath = File.expand_path(file, File.dirname(__FILE__))
- if File.exists?(filepath) && settings = YAML.load_file(filepath)[Rails.env]
+ if File.exist?(filepath) && settings = YAML.load_file(filepath)[Rails.env]
config.merge(settings)
else
config
@@ -30,16 +26,6 @@ module LeapWeb
# Application configuration should go into files in config/initializers
# -- all .rb files in that directory are automatically loaded.
- # Custom directories with classes and modules you want to be autoloadable.
- # config.autoload_paths += %W(#{config.root}/extras)
-
- # Only load the plugins named here, in the order given (default is alphabetical).
- # :all can be used as a placeholder for all plugins not explicitly named.
- # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
-
- # Activate observers that should always be running.
- # config.active_record.observers = :cacher, :garbage_collector, :forum_observer
-
# Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
# Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
# config.time_zone = 'Central Time (US & Canada)'
@@ -48,48 +34,18 @@ module LeapWeb
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
# config.i18n.default_locale = :de
- # Configure the default encoding used in templates for Ruby 1.9.
- config.encoding = "utf-8"
-
- # Configure sensitive parameters which will be filtered from the log file.
- config.filter_parameters += [:password]
-
if APP_CONFIG[:logfile].present?
config.logger = Logger.new(APP_CONFIG[:logfile])
end
- # Enable escaping HTML in JSON.
- config.active_support.escape_html_entities_in_json = true
-
- # Use SQL instead of Active Record's schema dumper when creating the database.
- # This is necessary if your schema can't be completely dumped by the schema dumper,
- # like if you have constraints or database-specific column types
- # config.active_record.schema_format = :sql
-
- # Enforce whitelist mode for mass assignment.
- # This will create an empty whitelist of attributes available for mass-assignment for all models
- # in your app. As such, your models will need to explicitly whitelist or blacklist accessible
- # parameters by using an attr_accessible or attr_protected declaration.
- # config.active_record.whitelist_attributes = true
-
- ##
- ## ASSETS
- ##
-
- # Enable the asset pipeline
- config.assets.enabled = true
- config.assets.initialize_on_precompile = true # don't change this (see customization.rb)
-
- # Version of your assets, change this if you want to expire all your assets
- config.assets.version = '1.0'
-
- # Set to false in order to see asset requests in the log
- config.quiet_assets = true
-
##
## CUSTOMIZATION
## see initializers/customization.rb
##
+
+ # don't change this (see customization.rb)
+ config.assets.initialize_on_precompile = true
+
if APP_CONFIG["customization_directory"]
custom_view_path = (Pathname.new(APP_CONFIG["customization_directory"]).relative_path_from(Rails.root) + 'views').to_s
else
diff --git a/config/boot.rb b/config/boot.rb
index 4489e58..6b750f0 100644
--- a/config/boot.rb
+++ b/config/boot.rb
@@ -1,6 +1,3 @@
-require 'rubygems'
-
-# Set up gems listed in the Gemfile.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
-require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
+require 'bundler/setup' # Set up gems listed in the Gemfile.
diff --git a/config/ci/gitlab/couchdb.yml b/config/ci/gitlab/couchdb.yml
new file mode 100644
index 0000000..68761dc
--- /dev/null
+++ b/config/ci/gitlab/couchdb.yml
@@ -0,0 +1,3 @@
+test:
+ auto_update_design_doc: false
+ host: "couchdb"
diff --git a/test/config/README b/config/ci/travis/README
index 58e37b2..58e37b2 100644
--- a/test/config/README
+++ b/config/ci/travis/README
diff --git a/test/config/couchdb.admin.yml b/config/ci/travis/couchdb.admin.yml
index 0988bc1..a7677f9 100644
--- a/test/config/couchdb.admin.yml
+++ b/config/ci/travis/couchdb.admin.yml
@@ -2,5 +2,4 @@ test:
auto_update_design_doc: false
username: "anna"
password: "secret"
- prefix: ""
-
+
diff --git a/test/config/couchdb.yml b/config/ci/travis/couchdb.yml
index 9c8b67b..a57b888 100644
--- a/test/config/couchdb.yml
+++ b/config/ci/travis/couchdb.yml
@@ -2,4 +2,3 @@ test:
auto_update_design_doc: false
username: "me"
password: "pwd"
- prefix: ""
diff --git a/test/travis/setup_couch.sh b/config/ci/travis/setup_couch.sh
index 0502c12..0502c12 100755
--- a/test/travis/setup_couch.sh
+++ b/config/ci/travis/setup_couch.sh
diff --git a/config/defaults.yml b/config/defaults.yml
index 844adaa..7e2ea58 100644
--- a/config/defaults.yml
+++ b/config/defaults.yml
@@ -87,6 +87,7 @@ common: &common
soledad-service: 'public/1/config/soledad-service.json'
eip-service: 'public/1/config/eip-service.json'
smtp-service: 'public/1/config/smtp-service.json'
+ provider: 'config/provider/provider.json'
mailer:
from_address: 'noreply'
diff --git a/config/environment.rb b/config/environment.rb
index fe16a54..00a613f 100644
--- a/config/environment.rb
+++ b/config/environment.rb
@@ -1,5 +1,5 @@
-# Load the rails application
+# Load the rails application.
require File.expand_path('../application', __FILE__)
-# Initialize the rails application
-LeapWeb::Application.initialize!
+# Initialize the rails application.
+Rails.application.initialize!
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 3c0ff0f..4112c6d 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -1,38 +1,58 @@
-LeapWeb::Application.configure do
- # Settings specified here will take precedence over those in config/application.rb
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ ##
+ # DEFAULT:
+ #
+ # rails default settings in their natural order
+ ##
# In the development environment your application's code is reloaded on
# every request. This slows down response time but is perfect for development
# since you don't have to restart the web server when you make code changes.
config.cache_classes = false
- # Log error messages when you accidentally call methods on nil.
- config.whiny_nils = true
+ # Do not eager load code on boot.
+ config.eager_load = false
- # Show full error reports and disable caching
+ # Show full error reports and disable caching.
config.consider_all_requests_local = true
config.action_controller.perform_caching = false
- # Don't care if the mailer can't send
+ # Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false
- # Print deprecation notices to the Rails logger
+ # Print deprecation notices to the Rails logger.
config.active_support.deprecation = :log
- # Only use best-standards-support built into browsers
- config.action_dispatch.best_standards_support = :builtin
+ # Debug mode disables concatenation and preprocessing of assets.
+ # This option may cause significant delays in view rendering with a large
+ # number of complex assets.
+ config.assets.debug = true
- # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
- # the I18n.default_locale when a translation can not be found)
- config.i18n.fallbacks = true
+ # Asset digests allow you to set far-future HTTP expiration dates on all assets,
+ # yet still be able to expire them through the digest params.
+ config.assets.digest = true
- # Do not compress assets
- config.assets.compress = false
+ # Adds additional error checking when serving assets at runtime.
+ # Checks for improperly declared sprockets dependencies.
+ # Raises helpful error messages.
+ config.assets.raise_runtime_errors = true
# If set to true, each asset file is loaded separately. If you are not
# debugging js or css, it is much faster to set this to false.
config.assets.debug = false
+ ##
+ # CUSTOM:
+ #
+ # our own settings
+ ##
+
+ # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
+ # the I18n.default_locale when a translation can not be found)
+ config.i18n.fallbacks = true
+
# super hacky, but otherwise getting certificate error, and doesn't seem dangerous in development mode:
OpenSSL::SSL.send(:remove_const, "VERIFY_PEER")
OpenSSL::SSL.const_set("VERIFY_PEER", OpenSSL::SSL::VERIFY_NONE)
diff --git a/config/environments/production.rb b/config/environments/production.rb
index 7acca75..acdfc1d 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -1,37 +1,56 @@
-LeapWeb::Application.configure do
- # Settings specified here will take precedence over those in config/application.rb
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
- # Code is not reloaded between requests
+ # Code is not reloaded between requests.
config.cache_classes = true
- # Full error reports are disabled and caching is turned on
+ # Eager load code on boot. This eager loads most of Rails and
+ # your application in memory, allowing both threaded web servers
+ # and those relying on copy on write to perform better.
+ # Rake tasks automatically ignore this option for performance.
+ config.eager_load = true
+
+ # Full error reports are disabled and caching is turned on.
config.consider_all_requests_local = false
config.action_controller.perform_caching = true
- # Disable Rails's static asset server (Apache or nginx will already do this)
- config.serve_static_assets = false
- # Compress JavaScripts and CSS
- config.assets.compress = true
+ # Dble Rack::Cache to put a simple HTTP cache in front of your application
+ # Add `rack-cache` to your Gemfile before enabling this.
+ # For large-scale production use, consider using a caching reverse proxy like
+ # NGINX, varnish or squid.
+ # config.action_dispatch.rack_cache = true
+
+ # Disable serving static files from the `/public` folder by default since
+ # Apache or NGINX already handles this.
+ config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present?
+
+ # Compress JavaScripts and CSS.
+ config.assets.js_compressor = :uglifier
+ # config.assets.css_compressor = :sass
- # Don't fallback to assets pipeline if a precompiled asset is missed
+ #Do not fallback to assets pipeline if a precompiled asset is missed.
config.assets.compile = false
- # Generate digests for assets URLs
+ # Asset digests allow you to set far-future HTTP expiration dates on all assets,
+ # yet still be able to expire them through the digest params.
config.assets.digest = true
- # Defaults to nil and saved in location specified by config.assets.prefix
- # config.assets.manifest = YOUR_PATH
+ # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
- # Specifies the header that your server uses for sending files
- # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache
- # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
+ # Specifies the header that your server uses for sending files.
+ # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
+ # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
config.force_ssl = APP_CONFIG[:force_ssl]
- # See everything in the log (default is :info)
- # config.log_level = :debug
+ # Use the lowest log level to ensure availability of diagnostic information
+ # when problems arise.
+ config.log_level = :debug
+
+ # Prepend all log lines with the following tags.
+ # config.log_tags = [ :subdomain, :uuid ]
# Use syslog if no file has been specified
if APP_CONFIG[:logfile].blank?
@@ -39,26 +58,23 @@ LeapWeb::Application.configure do
config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new('webapp'))
end
- # Use a different cache store in production
+ # Use a different cache store in production.
# config.cache_store = :mem_cache_store
- # Enable serving of images, stylesheets, and JavaScripts from an asset server
+ # Enable serving of images, stylesheets, and JavaScripts from an asset server.
# config.action_controller.asset_host = "http://assets.example.com"
- # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added)
- # config.assets.precompile += %w( search.js )
-
- # Disable delivery errors, bad email addresses will be ignored
+ # Ignore bad email addresses and do not raise email delivery errors.
+ # Set this to true and configure the email server for immediate delivery to raise delivery errors.
# config.action_mailer.raise_delivery_errors = false
- # Enable threaded mode
- # config.threadsafe!
-
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
- # the I18n.default_locale when a translation can not be found)
+ # the I18n.default_locale when a translation cannot be found).
config.i18n.fallbacks = true
# Send deprecation notices to registered listeners
config.active_support.deprecation = :notify
+ # Use default logging formatter so that PID and timestamp are not suppressed.
+ config.log_formatter = ::Logger::Formatter.new
end
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 1a38df7..0ba4fbd 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -1,5 +1,7 @@
-LeapWeb::Application.configure do
- # Settings specified here will take precedence over those in config/application.rb
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ config.assets.debug = true
# The test environment is used exclusively to run your application's
# test suite. You never need to work with it otherwise. Remember that
@@ -7,29 +9,36 @@ LeapWeb::Application.configure do
# and recreated between test runs. Don't rely on the data there!
config.cache_classes = true
- # Configure static asset server for tests with Cache-Control for performance
- config.serve_static_assets = true
- config.static_cache_control = "public, max-age=3600"
+ # Do not eager load code on boot. This avoids loading your whole application
+ # just for the purpose of running a single test. If you are using a tool that
+ # preloads Rails for running tests, you may have to set it to true.
+ config.eager_load = false
- # Log error messages when you accidentally call methods on nil
- config.whiny_nils = true
+ # Configure static asset server for tests with Cache-Control for performance
+ config.serve_static_files = true
+ config.static_cache_control = 'public, max-age=3600'
- # Show full error reports and disable caching
+ # Show full error reports and disable caching.
config.consider_all_requests_local = true
config.action_controller.perform_caching = false
- # Raise exceptions instead of rendering exception templates
+ # Raise exceptions instead of rendering exception templates.
config.action_dispatch.show_exceptions = false
- # Disable request forgery protection in test environment
- config.action_controller.allow_forgery_protection = false
+ # Disable request forgery protection in test environment.
+ config.action_controller.allow_forgery_protection = false
# Tell Action Mailer not to deliver emails to the real world.
# The :test delivery method accumulates sent emails in the
# ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test
+ # Randomize the order test cases are executed.
+ config.active_support.test_order = :random
- # Print deprecation notices to the stderr
+ # Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
+
+ # Raises error for missing translations
+ # config.action_view.raise_on_missing_translations = true
end
diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb
new file mode 100644
index 0000000..ccb32b6
--- /dev/null
+++ b/config/initializers/assets.rb
@@ -0,0 +1 @@
+Rails.application.config.assets.precompile += %w( Avatar_Pic.png )
diff --git a/config/initializers/client_side_validations.rb b/config/initializers/client_side_validations.rb
index 252aded..f7a658a 100644
--- a/config/initializers/client_side_validations.rb
+++ b/config/initializers/client_side_validations.rb
@@ -1,9 +1,16 @@
# ClientSideValidations Initializer
-# Uncomment to disable uniqueness validator, possible security issue
-ClientSideValidations::Config.disabled_validators = [:uniqueness]
+# Disabled validators. The uniqueness validator is disabled by default for security issues. Enable it on your own responsibility!
+# ClientSideValidations::Config.disabled_validators = [:uniqueness]
+
+# Uncomment to validate number format with current I18n locale
+# ClientSideValidations::Config.number_format_with_locale = true
# Uncomment the following block if you want each input field to have the validation messages attached.
+#
+# Note: client_side_validation requires the error to be encapsulated within
+# <label for="#{instance.send(:tag_id)}" class="message"></label>
+
ActionView::Base.field_error_proc = Proc.new do |html_tag, instance|
unless html_tag =~ /^<label/
%{<div class="field_with_errors">#{html_tag}<label for="#{instance.send(:tag_id)}" class="message">#{instance.error_message.first}</label></div>}.html_safe
@@ -11,4 +18,3 @@ ActionView::Base.field_error_proc = Proc.new do |html_tag, instance|
%{<div class="field_with_errors">#{html_tag}</div>}.html_safe
end
end
-
diff --git a/config/initializers/customization.rb b/config/initializers/customization.rb
index aaa2939..6d9c741 100644
--- a/config/initializers/customization.rb
+++ b/config/initializers/customization.rb
@@ -1,10 +1,8 @@
-#
# When deploying, common customizations can be dropped in config/customizations. This initializer makes this work.
#
APP_CONFIG["customization_directory"] ||= "#{Rails.root}/config/customization"
customization_directory = APP_CONFIG["customization_directory"]
-#
# Set customization views as the first view path
#
# Rails.application.config.paths['app/views'].unshift "config/customization/views"
@@ -21,12 +19,12 @@ customization_directory = APP_CONFIG["customization_directory"]
# * For this to work, config.assets.initialize_on_precompile MUST be set to true, otherwise
# this initializer will never get called in production mode when the assets are precompiled.
#
+Rails.application.config.assets.paths.unshift "#{customization_directory}/images"
Rails.application.config.assets.paths.unshift "#{customization_directory}/stylesheets"
-
#
# Copy files to public
#
-if !defined?(RAKE) && Dir.exists?("#{customization_directory}/public")
+if !defined?(RAKE) && Dir.exist?("#{customization_directory}/public")
require 'fileutils'
FileUtils.cp_r("#{customization_directory}/public/.", "#{Rails.root}/public", :preserve => true)
end
diff --git a/config/initializers/error_constants.rb b/config/initializers/error_constants.rb
index fdd3624..bf2dad4 100644
--- a/config/initializers/error_constants.rb
+++ b/config/initializers/error_constants.rb
@@ -1,3 +1,8 @@
require 'ruby-srp'
WRONG_PASSWORD = SRP::WrongPassword
+
+# In case we use a different ORM at some point
+VALIDATION_FAILED = CouchRest::Model::Errors::Validations
+RECORD_NOT_FOUND = CouchRest::Model::DocumentNotFound
+RESOURCE_NOT_FOUDN = CouchRest::NotFound
diff --git a/config/initializers/simple_form_bootstrap.rb b/config/initializers/simple_form_bootstrap.rb
index c949f5e..720174f 100644
--- a/config/initializers/simple_form_bootstrap.rb
+++ b/config/initializers/simple_form_bootstrap.rb
@@ -1,17 +1,17 @@
# Use this setup block to configure all options available in SimpleForm.
SimpleForm.setup do |config|
- config.wrappers :bootstrap, :tag => 'div', :class => 'control-group', :error_class => 'error' do |b|
+ config.wrappers :bootstrap, :tag => 'div', :class => 'form-group', :error_class => 'error' do |b|
b.use :html5
b.use :placeholder
- b.use :label
- b.wrapper :tag => 'div', :class => 'controls' do |ba|
+ b.use :label, :class => 'col-sm-2'
+ b.wrapper :tag => 'div', :class => 'col-sm-10' do |ba|
ba.use :input
ba.use :error, :wrap_with => { :tag => 'span', :class => 'help-inline' }
ba.use :hint, :wrap_with => { :tag => 'p', :class => 'help-block' }
end
end
- config.wrappers :prepend, :tag => 'div', :class => "control-group", :error_class => 'error' do |b|
+ config.wrappers :prepend, :tag => 'div', :class => "form-group", :error_class => 'error' do |b|
b.use :html5
b.use :placeholder
b.use :label
@@ -24,7 +24,7 @@ SimpleForm.setup do |config|
end
end
- config.wrappers :append, :tag => 'div', :class => "control-group", :error_class => 'error' do |b|
+ config.wrappers :append, :tag => 'div', :class => "form-group", :error_class => 'error' do |b|
b.use :html5
b.use :placeholder
b.use :label
@@ -38,7 +38,7 @@ SimpleForm.setup do |config|
end
#
- # when you don't want any bootstrap "control-group" or "controls" wrappers.
+ # when you don't want any bootstrap "form-group" or "controls" wrappers.
#
config.wrappers :none, :tag => 'div', :error_class => 'error' do |b|
b.use :html5
diff --git a/config/initializers/validations.rb b/config/initializers/validations.rb
deleted file mode 100644
index e8acfbe..0000000
--- a/config/initializers/validations.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# In case we use a different ORM at some point
-VALIDATION_FAILED = CouchRest::Model::Errors::Validations
-RECORD_NOT_FOUND = CouchRest::Model::DocumentNotFound
-RESOURCE_NOT_FOUND = RestClient::ResourceNotFound
diff --git a/config/locales/en/generic.en.yml b/config/locales/en/generic.en.yml
index be62a40..617116f 100644
--- a/config/locales/en/generic.en.yml
+++ b/config/locales/en/generic.en.yml
@@ -21,7 +21,7 @@ en:
example_email: 'user@domain.org'
- no_such_thing: "No such %{thing}."
+ no_such_user: "No such user."
create_thing: "Create %{thing}"
overview: "Overview"
diff --git a/config/routes.rb b/config/routes.rb
index e370aa4..b152c9c 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -10,8 +10,8 @@ LeapWeb::Application.routes.draw do
# HTTP Error Handling
# instead of the default error pages use the errors controller and views
#
- match '/404' => 'errors#not_found'
- match '/500' => 'errors#server_error'
+ match '/404' => 'errors#not_found', via: [:get, :post]
+ match '/500' => 'errors#server_error', via: [:get, :post]
scope "(:locale)", :locale => CommonLanguages.match_available, :controller => 'pages', :action => 'show' do
get 'privacy-policy', :as => 'privacy_policy'
@@ -24,10 +24,10 @@ LeapWeb::Application.routes.draw do
get '/provider.json' => 'static_config#provider'
- namespace "api", { module: "v1",
- path: "/1/",
- defaults: {format: 'json'},
- :constraints => { :id => /[^\/]+(?=\.json\z)|[^\/]+/ }
+ namespace "api", { module: "api",
+ path: "/:version/",
+ defaults: {version: '2', format: 'json'},
+ :constraints => { :id => /[^\/]+(?=\.json\z)|[^\/]+/, :version => /[12]/ }
} do
resources :sessions, :only => [:new, :create, :update]
delete "logout" => "sessions#destroy", :as => "logout"
@@ -44,8 +44,8 @@ LeapWeb::Application.routes.draw do
get "login" => "sessions#new", :as => "login"
delete "logout" => "sessions#destroy", :as => "logout"
- get "signup" => "users#new", :as => "signup"
- resources :users, :except => [:create, :update] do
+ get "signup" => "account#new", :as => "signup"
+ resources :users, :except => [:new, :create, :update] do
# resource :email_settings, :only => [:edit, :update]
# resources :email_aliases, :only => [:destroy], :id => /.*/
post 'deactivate', on: :member
diff --git a/doc/DEPLOY.md b/doc/DEPLOY.md
index 33d5598..4d59701 100644
--- a/doc/DEPLOY.md
+++ b/doc/DEPLOY.md
@@ -1,5 +1,10 @@
# Deployment #
+LEAP Web is provisioned and run as part of the overall [LEAP platform](https://leap.se/en/docs/platform).
+We strongly recomment using the whole Platform and following its instructions.
+If you want to directly deploy the webapp never the less these instructions are
+for you.
+
These instructions are targeting a Debian GNU/Linux system. You might need to
change the commands to match your own needs.
@@ -10,9 +15,9 @@ change the commands to match your own needs.
The following packages need to be installed:
* git
-* ruby1.9
-* rubygems1.9
+* ruby (2.1.5)
* couchdb (if you want to use a local couch)
+* bundler
### Setup Capistrano ###
diff --git a/doc/DEVELOP.md b/doc/DEVELOP.md
index cdd0867..97ecd25 100644
--- a/doc/DEVELOP.md
+++ b/doc/DEVELOP.md
@@ -1,5 +1,19 @@
# Development #
+## Branches
+
+We use the 'master' branch to hold the version currently deployed to the
+production servers. Only hotfixes are applied here.
+
+Most of development happens based upon the 'develop' branch. So unless
+you are investigating a specific issue that occured in production you
+probably want to base your changes on 'develop':
+```
+git checkout origin/develop -b my-new-feature
+```
+This will create a new branch called 'my-new-feature' based on the develop
+branch from the origin remote.
+
## Setting up the local CouchDB
CouchDB operates in Admin Party by default, meaning there are no access
@@ -7,17 +21,24 @@ control checks. This is handy for local development. However, there is
the risk that running tests with Couch in Admin Party yields false
results.
-You have two options:
+We recommend keeping the default CouchDB configuration locally and testing
+the more complex setup with access control in Continuous Integration.
-1) Use Admin Party and accept the risk
-2) Stop Admin Party by creating user accounts & security docs by running the
-following script:
+Please see .travis.yml for the configuration of our CI runs.
- test/travis/setup_couch.sh
+In order to prepare you local couch for development run
+```
+bin/rake db:rotate
+bin/rake db:migrate
+```
-### Database configuration
+### Customized database configuration (advanced)
+
+If you want to stop Admin Party mode you need to create user accounts &
+security docs. You can use the following script as a guideline:
+ test/travis/setup_couch.sh
-Copy & adapt the default database configuration:
+Afterwards copy & adapt the default database configuration:
```
mv config/couchdb.example.yml config/couchdb.yml
@@ -37,14 +58,14 @@ Some tips on modifying the views:
## Engines ##
-Leap Web contains some. They live in their own subdirectory and are included through bundler via their path. This way changes to the engines immediately affect the server as if they were in the main `app` directory.
+We use engines to separate optional functionality from the core. They live in their own subdirectory and are included through bundler via their path. This way changes to the engines immediately affect the server as if they were in the main `app` directory.
Currently Leap Web includes 2 Engines:
* [support](https://github.com/leapcode/leap_web/blob/master/engines/support) - Help ticket management
* [billing](https://github.com/leapcode/leap_web/blob/master/engines/billing) - Billing System
-## Creating a new engine ##
+## Creating a new engine (advanced) ##
If you want to add functionality to the webapp but keep it easy to remove you might consider adding an engine. This only makes sense if your engine really is a plugin - so no other pieces of code depend on it.
@@ -99,7 +120,7 @@ For example:
visit robot_path(@robot, :locale => nil)
end
-## Debugging
+## Debugging Production (advanced)
Sometimes bugs only show up when deployed to the live production server. Debugging can be tricky,
because the open source mod_passenger does not support debugger. You can't just run
diff --git a/doc/TROUBLESHOOT.md b/doc/TROUBLESHOOT.md
index f3db006..0e2957d 100644
--- a/doc/TROUBLESHOOT.md
+++ b/doc/TROUBLESHOOT.md
@@ -13,15 +13,19 @@ Here are some less common issues you might run into when installing Leap Web.
Make sure bundler is installed. `gem list bundler` should list `bundler`.
You also need to be able to access the `bundler` executable in your PATH.
-## Outdated version of rubygems ##
+## Incompatible ruby version ##
-### Error Messages ###
+### Detecting the problem ###
+The rubyversion we use for development and testing is noted in the file
+
+ .ruby-version
-`bundler requires rubygems >= 1.3.6`
+It should match what `ruby --version` prints.
### Solution ###
-`gem update --system` will install the latest rubygems
+Install the matching ruby version. For some operation systems this may require
+the use of rbenv or rvm.
## Missing development tools ##
@@ -42,5 +46,7 @@ Some gem dependencies might not compile because they lack the needed c libraries
### Solution ###
Install the libraries in question including their development files.
+Usually the missing library is mentioned in the error message. Searching the
+internet for similar errors is a good starting point aswell.
diff --git a/doc/TWITTER_FEED.md b/doc/TWITTER_FEED.md
new file mode 100644
index 0000000..6deb432
--- /dev/null
+++ b/doc/TWITTER_FEED.md
@@ -0,0 +1,53 @@
+# Display of status updates from twitter on main view #
+
+This is a feature to include status updates that displays most recent tweets
+of a (determined) twitter account (accessed via Twitter API).
+If you chose to use it, the feature gets included in `home/index` of
+LEAP web app (as part of the main view).
+
+## How to use it ##
+
+* Create Twitter Application on https://apps.twitter.com/
+ * Visit https://apps.twitter.com/ and log in with the twitter account you want to use
+ * Make sure you have a mobile phone number registered with your account to be able to proceed
+ * Choose the option to `Create New App`
+ * Fill in Application Details and Developer Agreement and `Create your Twitter application`
+ * Choose the section "Keys and Access Tokens" to get your consumer key and consumer secret
+ * Optional: Go to section "Permissions" and change the "Access" from `Read and Write` (by default) to `Read only`
+ * Have your consumer key and secret by hand for one of the next steps
+
+* Activate the feature within your local LEAP Web Application
+ * If not already existing create a secrets-file in /config with the name secrets.yml (`/config/secrets.yml`)
+ * Secrets-file should contain the following, make sure its in YAML: {"development"=> {"twitter"=>{"enabled"=>false, "twitter_handle"=>"", "bearer_token"=>"", "twitter_picture"=>nil}}, "test"=>{"twitter"=>{"enabled"=>false, "twitter_handle"=>"", "bearer_token"=>"", "twitter_picture"=>nil}}}
+```
+development:
+ twitter:
+ enabled: false # set to true for usage
+ twitter_handle: XXXXX #put your twitter handle here
+ bearer_token: XXXXX #put your bearer token here
+test:
+ twitter:
+ enabled: false # set to true for usage
+ twitter_handle: XXXXX #put your twitter handle here
+ bearer_token: XXXXX #put your bearer token here
+production:
+ twitter:
+ enabled: false # set to true for usage
+ twitter_handle: XXXXX #put your twitter handle here
+ bearer_token: XXXXX #put your bearer token here
+```
+ * To have your bearer token created, run script in terminal being in the file of leap_web: `script/generate_bearer_token`
+ * To have the script run properly you have to add before running: `--key your_consumerkey --secret your_consumersecret`
+ * Add also `--projectroot your_projectroot --twitterhandle your_twitterhandle` as well to not have manually put the data in your secrets-file
+ * The full command looks like this: `script/generate_bearer_token --key your_consumerkey --secret your_consumersecret --projectroot your_projectroot --twitterhandle your_twitterhandle`
+ * If you didn't give all your information to the script, had a typo or want to change anything else, please do so by finding the secrets-file at `/config/secrets.yml`
+ * Make sure that the correct twitter-handle and bearer-token is included. The account's tweets must not be protected, otherwise they cannot be displayed.
+
+* Deactivate your bearer token
+ * To deactivate your generated bearer token you can run script/invalidate_bearer_token
+ * The full command looks like this: script/invalidate_bearer_token --key your_consumerkey --secret your_consumersecret --token your_bearer_token
+
+### Default avatar image ###
+
+This feature uses by default the twitter bird as avatar picture, you can find here (app/assets/images/Avatar_Pic.png). For customization you can upload your own avatar picture to 'config/customization/images' naming the image file 'Avatar_Pic.png'. This will replace the default image file.
+By using the Twitter trademarks, you agree to follow the Twitter Trademark Guidelines as well as Twitter's Terms of Service and all other Twitter rules and policies. Please find more details here: https://brand.twitter.com/.
diff --git a/engines/billing/app/controllers/billing_admin_controller.rb b/engines/billing/app/controllers/billing_admin_controller.rb
index 23740d6..7a1de30 100644
--- a/engines/billing/app/controllers/billing_admin_controller.rb
+++ b/engines/billing/app/controllers/billing_admin_controller.rb
@@ -1,5 +1,6 @@
class BillingAdminController < BillingBaseController
before_filter :require_admin
+ respond_to :html
#not sure if this controller is still needed. Admin can easly acess
#braintree's dashboard and check subscriptions. Don't know if everything
diff --git a/engines/billing/app/controllers/billing_base_controller.rb b/engines/billing/app/controllers/billing_base_controller.rb
index c343938..39d41e4 100644
--- a/engines/billing/app/controllers/billing_base_controller.rb
+++ b/engines/billing/app/controllers/billing_base_controller.rb
@@ -3,6 +3,8 @@ class BillingBaseController < ApplicationController
helper 'billing'
+ protected
+
# required for navigation to work.
def assign_user
if params[:user_id]
diff --git a/engines/billing/app/controllers/payments_controller.rb b/engines/billing/app/controllers/payments_controller.rb
index 871f1b4..4f93aa6 100644
--- a/engines/billing/app/controllers/payments_controller.rb
+++ b/engines/billing/app/controllers/payments_controller.rb
@@ -1,6 +1,8 @@
class PaymentsController < BillingBaseController
before_filter :require_login, :only => [:index]
+ respond_to :html
+
def new
if current_user.has_payment_info?
@client_token = Braintree::ClientToken.generate(customer_id: current_user.braintree_customer_id)
diff --git a/engines/billing/app/controllers/subscriptions_controller.rb b/engines/billing/app/controllers/subscriptions_controller.rb
index 1d29cac..9df4ecd 100644
--- a/engines/billing/app/controllers/subscriptions_controller.rb
+++ b/engines/billing/app/controllers/subscriptions_controller.rb
@@ -1,4 +1,7 @@
class SubscriptionsController < BillingBaseController
+
+ respond_to :html
+
before_filter :require_login
before_filter :assign_user
before_filter :confirm_cancel_subscription, only: [:destroy]
diff --git a/engines/billing/config/routes.rb b/engines/billing/config/routes.rb
index 357c55b..12a6778 100644
--- a/engines/billing/config/routes.rb
+++ b/engines/billing/config/routes.rb
@@ -2,26 +2,23 @@ Rails.application.routes.draw do
scope "(:locale)", :locale => CommonLanguages.match_available do
- get 'payments/new' => 'payments#new', :as => :new_payment
- post 'payments/confirm' => 'payments#confirm', :as => :confirm_payment
- # match 'payments/new' => 'payments#new', :as => :new_payment
- # match 'payments/confirm' => 'payments#confirm', :as => :confirm_payment
- #resources :users do
- # resources :payments, :only => [:new, :confirm]
- # resources :subscriptions, :only => [:index, :destroy]
- #end
- resources :subscriptions, :only => [:index, :show] do
- member do
- post 'subscribe'
- delete 'unsubscribe'
+ get 'payments/new' => 'payments#new', :as => :new_payment
+ post 'payments/confirm' => 'payments#confirm', :as => :confirm_payment
+ # match 'payments/new' => 'payments#new', :as => :new_payment
+ # match 'payments/confirm' => 'payments#confirm', :as => :confirm_payment
+ #resources :users do
+ # resources :payments, :only => [:new, :confirm]
+ # resources :subscriptions, :only => [:index, :destroy]
+ #end
+ resources :subscriptions, :only => [:index, :show] do
+ member do
+ post 'subscribe'
+ delete 'unsubscribe'
+ end
end
- end
-
- resources :customer, :only => [:new, :edit]
- match 'customer/confirm/' => 'customer#confirm', :as => :confirm_customer
- match 'customer/show/:id' => 'customer#show', :as => :show_customer
+ resources :customer, :only => [:new, :edit]
- match 'billing_admin' => 'billing_admin#show', :as => :billing_admin
+ get 'billing_admin' => 'billing_admin#show'
end
end
diff --git a/engines/support/app/models/account_extension/tickets.rb b/engines/support/app/models/account_extension/tickets.rb
deleted file mode 100644
index f38d5fd..0000000
--- a/engines/support/app/models/account_extension/tickets.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-module AccountExtension::Tickets
- extend ActiveSupport::Concern
-
- def destroy_with_tickets(destroy_identities=false)
- Ticket.destroy_all_from(self.user)
- destroy_without_tickets(destroy_identities)
- end
-
- included do
- alias_method_chain :destroy, :tickets
- end
-
-end
diff --git a/engines/support/app/models/ticket.rb b/engines/support/app/models/ticket.rb
index b1bdf8d..025e2ab 100644
--- a/engines/support/app/models/ticket.rb
+++ b/engines/support/app/models/ticket.rb
@@ -37,9 +37,11 @@ class Ticket < CouchRest::Model::Base
# email can be nil, "", or valid address.
# validation provided by 'valid_email' gem.
+ # mx validation depends on network availability and is disabled in test
+ # and development environment
validates :email, :allow_blank => true,
:email => true,
- :mx_with_fallback => true
+ :mx_with_fallback => Rails.env.production?
def self.search(options = {})
@selection = TicketSelection.new(options)
@@ -50,7 +52,7 @@ class Ticket < CouchRest::Model::Base
self.by_created_by.key(user.id).each do |ticket|
ticket.destroy
end
- rescue RestClient::ResourceNotFound
+ rescue RESOURCE_NOT_FOUND
# silently ignore if design docs are not yet created
end
diff --git a/engines/support/app/views/tickets/_edit_form.html.haml b/engines/support/app/views/tickets/_edit_form.html.haml
index cd1dbe4..8d64256 100644
--- a/engines/support/app/views/tickets/_edit_form.html.haml
+++ b/engines/support/app/views/tickets/_edit_form.html.haml
@@ -34,12 +34,12 @@
= simple_form_for @ticket do |f|
= hidden_ticket_fields
= f.input :subject, input_html: {:class => 'large full-width'}
- .row-fluid
- .span4
+ .row
+ .col-md-4
= f.input :is_open, as: :select, collection: [:true, :false], include_blank: false
- .span4
+ .col-md-4
= f.input :email
- .span4
+ .col-md-4
= f.input :regarding_user, label: (Ticket.human_attribute_name(:regarding_user) + " " + regarding_user_link).html_safe
= f.button :loading
- if admin?
diff --git a/engines/support/config/initializers/account_lifecycle.rb b/engines/support/config/initializers/account_lifecycle.rb
index d9f04c1..9060757 100644
--- a/engines/support/config/initializers/account_lifecycle.rb
+++ b/engines/support/config/initializers/account_lifecycle.rb
@@ -1,3 +1,5 @@
+require 'account_extension/tickets'
+
ActiveSupport.on_load(:account) do
include AccountExtension::Tickets
end
diff --git a/engines/support/config/locales/en.yml b/engines/support/config/locales/en.yml
index 83af2c4..3b509d5 100644
--- a/engines/support/config/locales/en.yml
+++ b/engines/support/config/locales/en.yml
@@ -92,7 +92,7 @@ en:
# mouse over hints for the given fields
hints:
ticket:
- email: "Provide an email address in order to notified when this ticket is updated."
+ email: "Provide an email address in order to be notified when this ticket is updated."
# these will fallback to translations in "simple_form.hints.defaults"
# placeholders inside the fields before anything was typed
#placeholders:
diff --git a/engines/support/config/routes.rb b/engines/support/config/routes.rb
index 81bdf9a..5647477 100644
--- a/engines/support/config/routes.rb
+++ b/engines/support/config/routes.rb
@@ -3,8 +3,8 @@ Rails.application.routes.draw do
resources :tickets, except: :edit do
member do
- put 'open'
- put 'close'
+ patch 'open'
+ patch 'close'
end
end
diff --git a/engines/support/lib/account_extension/tickets.rb b/engines/support/lib/account_extension/tickets.rb
new file mode 100644
index 0000000..63f4873
--- /dev/null
+++ b/engines/support/lib/account_extension/tickets.rb
@@ -0,0 +1,15 @@
+module AccountExtension
+ module Tickets
+ extend ActiveSupport::Concern
+
+ def destroy_with_tickets(destroy_identities=false)
+ Ticket.destroy_all_from(self.user)
+ destroy_without_tickets(destroy_identities)
+ end
+
+ included do
+ alias_method_chain :destroy, :tickets
+ end
+
+ end
+end
diff --git a/engines/support/test/integration/create_ticket_test.rb b/engines/support/test/integration/create_ticket_test.rb
index 00f9a6b..6abb3d3 100644
--- a/engines/support/test/integration/create_ticket_test.rb
+++ b/engines/support/test/integration/create_ticket_test.rb
@@ -29,7 +29,7 @@ class CreateTicketTest < BrowserIntegrationTest
fill_in 'Description', with: 'description of the problem goes here'
click_on 'Submit Ticket'
assert page.has_content?("is invalid")
- assert_equal 'invalid data', find_field('Email').value
+ assert_equal 'invaliddata', find_field('Email').value
assert_equal 'some user', find_field('Regarding User').value
end
diff --git a/engines/support/test/unit/ticket_test.rb b/engines/support/test/unit/ticket_test.rb
index 7b5281f..373f06c 100644
--- a/engines/support/test/unit/ticket_test.rb
+++ b/engines/support/test/unit/ticket_test.rb
@@ -8,12 +8,12 @@ class TicketTest < ActiveSupport::TestCase
test "ticket with default attribs is valid" do
t = FactoryGirl.build :ticket
- assert t.valid?
+ assert t.valid?, t.errors.full_messages.to_sentence
end
test "ticket without email is valid" do
t = FactoryGirl.build :ticket, email: ""
- assert t.valid?
+ assert t.valid?, t.errors.full_messages.to_sentence
end
test "ticket validates email format" do
@@ -63,7 +63,7 @@ class TicketTest < ActiveSupport::TestCase
test "find tickets user commented on" do
# clear old tickets just in case
- # this will cause RestClient::ResourceNotFound errors if there are multiple copies of the same ticket returned
+ # this will cause RESOURCE_NOT_FOUND errors if there are multiple copies of the same ticket returned
Ticket.by_includes_post_by.key('123').each {|t| t.destroy}
# TODO: the by_includes_post_by view is only used for tests. Maybe we should get rid of it and change the test to including ordering?
diff --git a/features/1/anonymous.feature b/features/1/anonymous.feature
new file mode 100644
index 0000000..73a6d3f
--- /dev/null
+++ b/features/1/anonymous.feature
@@ -0,0 +1,34 @@
+@config
+Feature: Anonymous access to EIP
+
+ A provider may choose to allow anonymous access to EIP.
+ In this case some endpoints that would normally require authentication
+ will be available without authentication.
+
+ Background:
+ Given "allow_anonymous_certs" is enabled in the config
+ And I set headers:
+ | Accept | application/json |
+ | Content-Type | application/json |
+
+ Scenario: Fetch configs when anonymous certs are allowed
+ When I send a GET request to "/1/configs.json"
+ Then the response status should be "200"
+
+ Scenario: Fetch EIP config when anonymous certs are allowed
+ Given there is a config for the eip
+ When I send a GET request to "/1/configs/eip-service.json"
+ Then the response status should be "200"
+
+ Scenario: Fetch service description
+ When I send a GET request to "/1/service.json"
+ Then the response status should be "200"
+ And the response should be:
+ """
+ {
+ "name": "anonymous",
+ "description": "anonymous access to the VPN",
+ "eip_rate_limit": false
+ }
+ """
+
diff --git a/features/1/authentication.feature b/features/1/authentication.feature
new file mode 100644
index 0000000..52b562f
--- /dev/null
+++ b/features/1/authentication.feature
@@ -0,0 +1,24 @@
+Feature: Authentication
+
+ Authentication is handled with SRP. Once the SRP handshake has been successful a token will be transmitted. This token is used to authenticate further requests.
+
+ In the scenarios MY_AUTH_TOKEN will serve as a placeholder for the actual token received.
+
+ Background:
+ Given I set headers:
+ | Accept | application/json |
+ | Content-Type | application/json |
+
+ Scenario: Submitting a valid token
+ Given I authenticated
+ And I set headers:
+ | Authorization | Token token="MY_AUTH_TOKEN" |
+ When I send a GET request to "/1/configs.json"
+ Then the response status should be "200"
+
+ Scenario: Submitting an invalid token
+ Given I authenticated
+ And I set headers:
+ | Authorization | Token token="InvalidToken" |
+ When I send a GET request to "/1/configs.json"
+ Then the response status should be "401"
diff --git a/features/1/config.feature b/features/1/config.feature
new file mode 100644
index 0000000..ff04e9d
--- /dev/null
+++ b/features/1/config.feature
@@ -0,0 +1,58 @@
+Feature: Download Provider Configuration
+
+ The LEAP Provider exposes parts of its configuration through the API.
+
+ This can be used to find out about services offered. The big picture can be retrieved from `/provider.json`. Which is available without authentication (see unauthenticated.feature).
+
+ More detailed settings of the services are available after authentication. You can get a list of the available settings from `/1/configs.json`.
+
+ Background:
+ Given I authenticated
+ Given I set headers:
+ | Accept | application/json |
+ | Content-Type | application/json |
+ | Authorization | Token token="MY_AUTH_TOKEN" |
+
+ @tempfile
+ Scenario: Fetch provider config
+ Given there is a config for the provider
+ When I send a GET request to "/provider.json"
+ Then the response status should be "200"
+ And the response should be that config
+
+ Scenario: Missing provider config
+ When I send a GET request to "/provider.json"
+ Then the response status should be "404"
+ And the response should have "error" with "not_found"
+
+ Scenario: Fetch list of available configs
+ When I send a GET request to "/1/configs.json"
+ Then the response status should be "200"
+ And the response should be:
+ """
+ {
+ "services": {
+ "soledad": "/1/configs/soledad-service.json",
+ "eip": "/1/configs/eip-service.json",
+ "smtp": "/1/configs/smtp-service.json"
+ }
+ }
+ """
+
+ Scenario: Attempt to fetch an invalid config
+ When I send a GET request to "/1/configs/non-existing.json"
+ Then the response status should be "403"
+
+ # I am not sure what this test is about, that config is not
+ # actually missing.
+ #Scenario: Attempt to fetch a config that is missing on the server
+ # When I send a GET request to "/1/configs/eip-service.json"
+ # Then the response status should be "404"
+
+ @tempfile, @config
+ Scenario: Attempt to fetch the EIP config
+ Given there is a config for the eip
+ When I send a GET request to "/1/configs/eip-service.json"
+ Then the response status should be "200"
+ And the response should be that config
+
diff --git a/features/1/service.feature b/features/1/service.feature
new file mode 100644
index 0000000..ea49c74
--- /dev/null
+++ b/features/1/service.feature
@@ -0,0 +1,33 @@
+Feature: Get service description for current user
+
+ The LEAP provider can offer different services and their availability may
+ depend upon a users service level - so wether they are paying or not.
+
+ The /1/service endpoint allows the client to find out about the services
+ available to the authenticated user.
+
+ Background:
+ Given I authenticated
+ Given I set headers:
+ | Accept | application/json |
+ | Content-Type | application/json |
+ | Authorization | Token token="MY_AUTH_TOKEN" |
+
+ Scenario: Get service settings
+ When I send a GET request to "/1/service"
+ Then the response status should be "200"
+ And the response should be:
+ """
+ {
+ "name": "free",
+ "description": "free account, with rate limited VPN",
+ "eip_rate_limit": true,
+ "storage": 100,
+ "services": [
+ "eip"
+ ]
+ }
+ """
+
+
+
diff --git a/features/1/unauthenticated.feature b/features/1/unauthenticated.feature
new file mode 100644
index 0000000..aea7117
--- /dev/null
+++ b/features/1/unauthenticated.feature
@@ -0,0 +1,31 @@
+Feature: Unauthenticated API endpoints
+
+ Most of the LEAP Provider API requires authentication.
+ However there are a few exceptions - mostly prerequisits of authenticating. This feature and the authentication feature document these.
+
+ Background:
+ Given I set headers:
+ | Accept | application/json |
+ | Content-Type | application/json |
+
+ @tempfile
+ Scenario: Fetch provider config
+ Given there is a config for the provider
+ When I send a GET request to "/provider.json"
+ Then the response status should be "200"
+ And the response should be that config
+
+ Scenario: Authentication required response
+ When I send a GET request to "/1/configs"
+ Then the response status should be "401"
+ And the response should have "error" with "not_authorized_login"
+ And the response should have "message"
+
+ Scenario: Authentication required for all other API endpoints (incomplete)
+ Given I am not logged in
+ When I send requests to these endpoints:
+ | GET | /1/configs |
+ | GET | /1/configs/config_id.json |
+ | GET | /1/service |
+ | DELETE | /1/logout |
+ Then they should require authentication
diff --git a/features/anonymous.feature b/features/anonymous.feature
index 73a6d3f..d6b3ce2 100644
--- a/features/anonymous.feature
+++ b/features/anonymous.feature
@@ -5,23 +5,23 @@ Feature: Anonymous access to EIP
In this case some endpoints that would normally require authentication
will be available without authentication.
- Background:
+ Background:
Given "allow_anonymous_certs" is enabled in the config
And I set headers:
| Accept | application/json |
| Content-Type | application/json |
Scenario: Fetch configs when anonymous certs are allowed
- When I send a GET request to "/1/configs.json"
+ When I send a GET request to "/2/configs.json"
Then the response status should be "200"
Scenario: Fetch EIP config when anonymous certs are allowed
Given there is a config for the eip
- When I send a GET request to "/1/configs/eip-service.json"
+ When I send a GET request to "/2/configs/eip-service.json"
Then the response status should be "200"
Scenario: Fetch service description
- When I send a GET request to "/1/service.json"
+ When I send a GET request to "/2/service.json"
Then the response status should be "200"
And the response should be:
"""
diff --git a/features/authentication.feature b/features/authentication.feature
index 52b562f..806e2b7 100644
--- a/features/authentication.feature
+++ b/features/authentication.feature
@@ -13,12 +13,12 @@ Feature: Authentication
Given I authenticated
And I set headers:
| Authorization | Token token="MY_AUTH_TOKEN" |
- When I send a GET request to "/1/configs.json"
+ When I send a GET request to "/2/configs.json"
Then the response status should be "200"
Scenario: Submitting an invalid token
Given I authenticated
And I set headers:
| Authorization | Token token="InvalidToken" |
- When I send a GET request to "/1/configs.json"
+ When I send a GET request to "/2/configs.json"
Then the response status should be "401"
diff --git a/features/config.feature b/features/config.feature
index ff04e9d..bd627de 100644
--- a/features/config.feature
+++ b/features/config.feature
@@ -4,7 +4,7 @@ Feature: Download Provider Configuration
This can be used to find out about services offered. The big picture can be retrieved from `/provider.json`. Which is available without authentication (see unauthenticated.feature).
- More detailed settings of the services are available after authentication. You can get a list of the available settings from `/1/configs.json`.
+ More detailed settings of the services are available after authentication. You can get a list of the available settings from `/2/configs.json`.
Background:
Given I authenticated
@@ -26,33 +26,33 @@ Feature: Download Provider Configuration
And the response should have "error" with "not_found"
Scenario: Fetch list of available configs
- When I send a GET request to "/1/configs.json"
+ When I send a GET request to "/2/configs.json"
Then the response status should be "200"
And the response should be:
"""
{
"services": {
- "soledad": "/1/configs/soledad-service.json",
- "eip": "/1/configs/eip-service.json",
- "smtp": "/1/configs/smtp-service.json"
+ "soledad": "/2/configs/soledad-service.json",
+ "eip": "/2/configs/eip-service.json",
+ "smtp": "/2/configs/smtp-service.json"
}
}
"""
Scenario: Attempt to fetch an invalid config
- When I send a GET request to "/1/configs/non-existing.json"
+ When I send a GET request to "/2/configs/non-existing.json"
Then the response status should be "403"
# I am not sure what this test is about, that config is not
# actually missing.
#Scenario: Attempt to fetch a config that is missing on the server
- # When I send a GET request to "/1/configs/eip-service.json"
+ # When I send a GET request to "/2/configs/eip-service.json"
# Then the response status should be "404"
@tempfile, @config
Scenario: Attempt to fetch the EIP config
Given there is a config for the eip
- When I send a GET request to "/1/configs/eip-service.json"
+ When I send a GET request to "/2/configs/eip-service.json"
Then the response status should be "200"
And the response should be that config
diff --git a/features/service.feature b/features/service.feature
index ea49c74..6244f6c 100644
--- a/features/service.feature
+++ b/features/service.feature
@@ -3,7 +3,7 @@ Feature: Get service description for current user
The LEAP provider can offer different services and their availability may
depend upon a users service level - so wether they are paying or not.
- The /1/service endpoint allows the client to find out about the services
+ The /2/service endpoint allows the client to find out about the services
available to the authenticated user.
Background:
@@ -14,7 +14,7 @@ Feature: Get service description for current user
| Authorization | Token token="MY_AUTH_TOKEN" |
Scenario: Get service settings
- When I send a GET request to "/1/service"
+ When I send a GET request to "/2/service"
Then the response status should be "200"
And the response should be:
"""
diff --git a/features/step_definitions/config_steps.rb b/features/step_definitions/config_steps.rb
index 1fc67f5..a635d06 100644
--- a/features/step_definitions/config_steps.rb
+++ b/features/step_definitions/config_steps.rb
@@ -4,12 +4,9 @@ Given /there is a config for the (.*)$/ do |config|
@tempfile = Tempfile.new("#{config}.json")
@tempfile.write @dummy_config
@tempfile.close
- if config == 'provider'
- StaticConfigController::PROVIDER_JSON = @tempfile.path
- else
- @orig_config ||= APP_CONFIG.dup
- APP_CONFIG[:config_file_paths].merge! "#{config}-service" => @tempfile.path
- end
+ @orig_config ||= APP_CONFIG.dup
+ config = "#{config}-service" unless config == 'provider'
+ APP_CONFIG[:config_file_paths].merge! config => @tempfile.path
end
# use with @config
diff --git a/features/support/hooks.rb b/features/support/hooks.rb
index 256e5d8..4ddc77e 100644
--- a/features/support/hooks.rb
+++ b/features/support/hooks.rb
@@ -13,9 +13,9 @@ end
After do |scenario|
if scenario.failed?
logfile_path = Rails.root + 'tmp'
- logfile_path += "#{scenario.title.gsub(/\s/, '_')}.log"
+ logfile_path += "#{scenario.name.gsub(/\s/, '_')}.log"
File.open(logfile_path, 'w') do |test_log|
- test_log.puts scenario.title
+ test_log.puts scenario.name
test_log.puts "========================="
test_log.puts `tail log/test.log -n 200`
end
diff --git a/features/unauthenticated.feature b/features/unauthenticated.feature
index aea7117..b4b0f55 100644
--- a/features/unauthenticated.feature
+++ b/features/unauthenticated.feature
@@ -16,7 +16,7 @@ Feature: Unauthenticated API endpoints
And the response should be that config
Scenario: Authentication required response
- When I send a GET request to "/1/configs"
+ When I send a GET request to "/2/configs"
Then the response status should be "401"
And the response should have "error" with "not_authorized_login"
And the response should have "message"
@@ -24,8 +24,8 @@ Feature: Unauthenticated API endpoints
Scenario: Authentication required for all other API endpoints (incomplete)
Given I am not logged in
When I send requests to these endpoints:
- | GET | /1/configs |
- | GET | /1/configs/config_id.json |
- | GET | /1/service |
- | DELETE | /1/logout |
+ | GET | /2/configs |
+ | GET | /2/configs/config_id.json |
+ | GET | /2/service |
+ | DELETE | /2/logout |
Then they should require authentication
diff --git a/app/models/email.rb b/lib/email.rb
index 4090275..4090275 100644
--- a/app/models/email.rb
+++ b/lib/email.rb
diff --git a/lib/extensions/couchrest.rb b/lib/extensions/couchrest.rb
index cc041e0..4578926 100644
--- a/lib/extensions/couchrest.rb
+++ b/lib/extensions/couchrest.rb
@@ -69,7 +69,7 @@ module CouchRest
def prepare_directory(dir = '')
dir = Rails.root + 'tmp' + 'designs' + dir
- Dir.mkdir(dir) unless Dir.exists?(dir)
+ Dir.mkdir(dir) unless Dir.exist?(dir)
return dir
end
@@ -81,7 +81,7 @@ module CouchRest
class ModelRailtie
config.action_dispatch.rescue_responses.merge!(
'CouchRest::Model::DocumentNotFound' => :not_found,
- 'RestClient::ResourceNotFound' => :not_found
+ 'CouchRest::NotFound' => :not_found
)
end
end
diff --git a/lib/gemfile_tools.rb b/lib/gemfile_tools.rb
index dce2448..d1c00dc 100644
--- a/lib/gemfile_tools.rb
+++ b/lib/gemfile_tools.rb
@@ -68,7 +68,7 @@ def local_config
empty_hash.default_proc = proc{|h, k| h.key?(k.to_s) ? h[k.to_s] : nil}
["defaults.yml", "config.yml"].inject(empty_hash.dup) {|config, file|
filepath = File.join(File.expand_path("../../config", __FILE__), file)
- if File.exists?(filepath)
+ if File.exist?(filepath)
new_config = YAML.load_file(filepath)
['development', 'test','production'].each do |env|
config[env] ||= empty_hash.dup
@@ -87,7 +87,7 @@ end
# or nil if not actually a gem directory
#
def gem_info(gem_dir)
- if Dir.exists?(gem_dir)
+ if Dir.exist?(gem_dir)
gemspec = Dir["#{gem_dir}/*.gemspec"]
if gemspec.any?
gem_name = File.basename(gemspec.first).sub(/\.gemspec$/,'')
diff --git a/app/models/local_email.rb b/lib/local_email.rb
index ded7baf..7c592e1 100644
--- a/app/models/local_email.rb
+++ b/lib/local_email.rb
@@ -1,3 +1,4 @@
+require 'email'
class LocalEmail < Email
BLACKLIST_FROM_RFC2142 = [
diff --git a/app/models/login_format_validation.rb b/lib/login_format_validation.rb
index c1fcf70..c1fcf70 100644
--- a/app/models/login_format_validation.rb
+++ b/lib/login_format_validation.rb
diff --git a/lib/tasks/i18n.rake b/lib/tasks/i18n.rake
index 6ffbb23..1034211 100644
--- a/lib/tasks/i18n.rake
+++ b/lib/tasks/i18n.rake
@@ -63,7 +63,7 @@ namespace :i18n do
desc "pull translations from transifex"
task :download do
Dir.chdir('config/') do
- if !File.exists?('transifex.netrc')
+ if !File.exist?('transifex.netrc')
puts "In order to download translations, you need a config/transifex.netrc file."
puts "For example:"
puts "machine www.transifex.com login yourusername password yourpassword"
diff --git a/lib/tasks/test.rake b/lib/tasks/test.rake
index d96b625..9859729 100644
--- a/lib/tasks/test.rake
+++ b/lib/tasks/test.rake
@@ -1,10 +1,15 @@
namespace :test do
- [:units, :functionals, :integration].each do |type|
- Rails::SubTestTask.new(type => "test:prepare") do |t|
- t.libs << "test"
- subdir = type.to_s.singularize
- t.pattern = "engines/*/test/#{subdir}/**/*_test.rb"
+ namespace :engines do
+ [:units, :functionals, :integration].each do |type|
+ desc "Test engine #{type}"
+ Rails::TestTask.new(type => "test:prepare") do |t|
+ t.libs << "test"
+ subdir = type.to_s.singularize
+ t.pattern = "engines/*/test/#{subdir}/**/*_test.rb"
+ end
+ Rake::Task["test:#{type}"].enhance ["test:engines:#{type}"]
+ Rake::Task["test"].enhance ["test:engines:#{type}"]
end
end
diff --git a/app/models/temporary_user.rb b/lib/temporary_user.rb
index 2afae15..d0db1c4 100644
--- a/app/models/temporary_user.rb
+++ b/lib/temporary_user.rb
@@ -12,16 +12,13 @@
module TemporaryUser
extend ActiveSupport::Concern
- include CouchRest::Model::DatabaseMethod
- USER_DB = 'users'
+ USER_DB = 'users'
TMP_USER_DB = 'tmp_users'
TMP_LOGIN = 'tmp_user' # created and deleted frequently
TEST_LOGIN = 'test_user' # created, rarely deleted
included do
- use_database_method :db_name
-
# since the original find_by_login is dynamically created with
# instance_eval, it appears that we also need to use instance eval to
# override it.
@@ -42,20 +39,15 @@ module TemporaryUser
end
alias :find :get
- # calls db_name(TMP_LOGIN), then creates a CouchRest::Database
- # from the name
- def tmp_database
- choose_database(TMP_LOGIN)
+ def database
+ @database ||= prepare_database USER_DB
end
- def db_name(login=nil)
- if !login.nil? && login.include?(TMP_LOGIN)
- TMP_USER_DB
- else
- USER_DB
- end
+ def tmp_database
+ @tmp_database ||= prepare_database TMP_USER_DB
end
+
# create the tmp db if it doesn't exist.
# requires admin access.
def create_tmp_database!
@@ -71,12 +63,12 @@ module TemporaryUser
end
end
- #
- # this gets called each and every time a User object needs to
- # access the database.
- #
- def db_name
- self.class.db_name(self.login)
+ def database
+ if login.present? && login.include?(TMP_LOGIN)
+ self.class.tmp_database
+ else
+ self.class.database
+ end
end
# returns true if this User instance is stored in tmp db.
diff --git a/script/generate_bearer_token b/script/generate_bearer_token
new file mode 100755
index 0000000..3a09382
--- /dev/null
+++ b/script/generate_bearer_token
@@ -0,0 +1,86 @@
+#!/usr/bin/env ruby
+
+require "net/http"
+require "uri"
+require "json"
+require "base64"
+require "optparse"
+require "yaml"
+
+options = {}
+
+option_parser = OptionParser.new do |opts|
+ opts.banner = "Create your bearer_token for twitter by including following two [options], feel free to have your secrets-file created/filled giving the other information as well:"
+
+ opts.on("--key KEY", "consumer_key of your twitter application") do |key|
+ options[:conkey] = key
+ end
+
+ opts.on("--secret SECRET", "consumer_secret of your twitter application") do |secret|
+ options[:consec] = secret
+ end
+
+ opts.on("--projectroot DIR", "directory where leapweb is") do |projectroot|
+ options[:projectroot] = projectroot
+ end
+
+ opts.on("--twitterhandle TWI", "twitterhandle without @ which will be passed into secrets-file") do |twitterhandle|
+ options[:twitterhandle] = twitterhandle
+ end
+
+end
+
+option_parser.parse!
+
+if options[:conkey].nil? || options[:consec].nil? then
+ puts option_parser
+ exit
+else
+ consumer_key = options[:conkey]
+ consumer_secret = options[:consec]
+end
+
+uri = URI("https://api.twitter.com/oauth2/token")
+data = "grant_type=client_credentials"
+cre = Base64.strict_encode64("#{consumer_key}:#{consumer_secret}")
+authorization_headers = { "Authorization" => "Basic #{cre}"}
+
+Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
+ response = http.request_post(uri, data, authorization_headers)
+ token_hash = JSON.parse(response.body)
+ $bearer_token = token_hash["access_token"]
+end
+
+if options[:projectroot].nil? then
+ puts "You didn't tell us the directory to have your secrets-file created or being filled. Feel free to copy/paste your bearer_token:"
+ puts $bearer_token
+else
+ if File.exist?("#{options[:projectroot]}/config/secrets.yml")
+ secrets = YAML.load_file("#{options[:projectroot]}/config/secrets.yml")
+ else
+ puts "Please make sure that you created a secrets-file as described in the documentation or have given the correct directory. No secrets-file could be found."
+ exit
+ # secrets_content = {"twitter"=>{"enabled"=>false, "twitter_handle"=>"", "bearer_token"=>"", "twitter_picture"=>nil}}
+ # secrets = {"development"=> secrets_content, "test"=>secrets_content}
+ # secrets = {"development"=> {"twitter"=>{"enabled"=>false, "twitter_handle"=>"", "bearer_token"=>"", "twitter_picture"=>nil}}, "test"=>{"twitter"=>{"enabled"=>false, "twitter_handle"=>"", "bearer_token"=>"", "twitter_picture"=>nil}}}
+ # File.new("#{options[:projectroot]}/leap_web/config/secrets.yml", "w")
+ end
+
+ if options[:twitterhandle].nil? then
+ if secrets["development"]["twitter"]["twitter_handle"] == "" then
+ puts "You didn't put your twitter-handle neither in the secrets-file nor passed it as a flag. Don't forget that you can't use the twitter-feature without your twitter-handle."
+ end
+ else
+ secrets["development"]["twitter"]["twitter_handle"] = options[:twitterhandle]
+ secrets["test"]["twitter"]["twitter_handle"] = options[:twitterhandle]
+ secrets["production"]["twitter"]["twitter_handle"] = options[:twitterhandle]
+ end
+
+ secrets["development"]["twitter"]["bearer_token"] = $bearer_token
+ secrets["test"]["twitter"]["bearer_token"] = $bearer_token
+ secrets["production"]["twitter"]["twitter_handle"] = $bearer_token
+
+ File.open("#{options[:projectroot]}/leap_web/config/secrets.yml", "r+") do |file|
+ file.write(secrets.to_yaml)
+ end
+end
diff --git a/script/invalidate_bearer_token b/script/invalidate_bearer_token
new file mode 100755
index 0000000..eda1c7d
--- /dev/null
+++ b/script/invalidate_bearer_token
@@ -0,0 +1,47 @@
+#!/usr/bin/env ruby
+
+require "net/http"
+require "uri"
+require "json"
+require "base64"
+require "optparse"
+
+options = {}
+
+option_parser = OptionParser.new do |opts|
+ opts.banner = "Invalidate your bearer_token for twitter by including the following [options]. The bearer token can't be used afterwards anymore. Please create a new bearer-token if you want to activate the twitter feature again."
+
+ opts.on("--key KEY", "consumer_key of your twitter application") do |key|
+ options[:conkey] = key
+ end
+
+ opts.on("--secret SECRET", "consumer_secret of your twitter application") do |secret|
+ options[:consec] = secret
+ end
+
+ opts.on("--token TOKEN", "bearer token for twitter") do |token|
+ options[:token] = token
+ end
+
+end
+
+option_parser.parse!
+
+if options[:conkey].nil? || options[:consec].nil? || options[:token].nil? then
+ puts option_parser
+ exit
+else
+ consumer_key = options[:conkey]
+ consumer_secret = options[:consec]
+ bearer_token = options[:token]
+end
+
+uri = URI("https://api.twitter.com/oauth2/invalidate_token")
+data = "access_token=#{bearer_token}"
+cre = Base64.strict_encode64("#{consumer_key}:#{consumer_secret}")
+authorization_headers = { "Authorization" => "Basic #{cre}"}
+
+Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
+ response = http.request_post(uri, data, authorization_headers)
+ puts JSON.parse(response.body)
+end
diff --git a/test/functional/account_controller_test.rb b/test/functional/account_controller_test.rb
new file mode 100644
index 0000000..f5f1446
--- /dev/null
+++ b/test/functional/account_controller_test.rb
@@ -0,0 +1,26 @@
+require 'test_helper'
+
+class AccountControllerTest < ActionController::TestCase
+
+ test "should get new" do
+ get :new
+ assert_equal User, assigns(:user).class
+ assert_response :success
+ end
+
+ test "new should redirect logged in users" do
+ login
+ get :new
+ assert_response :redirect
+ assert_redirected_to home_path
+ end
+
+ test "new redirects if registration is closed" do
+ with_config(allow_registration: false) do
+ get :new
+ assert_response :redirect
+ assert_redirected_to home_path
+ end
+ end
+end
+
diff --git a/test/functional/v1/certs_controller_test.rb b/test/functional/api/certs_controller_test.rb
index 04c1c86..25ceb8e 100644
--- a/test/functional/v1/certs_controller_test.rb
+++ b/test/functional/api/certs_controller_test.rb
@@ -1,11 +1,11 @@
-require_relative '../../test_helper'
+require 'test_helper'
-class V1::CertsControllerTest < ActionController::TestCase
+class Api::CertsControllerTest < ApiControllerTest
test "create unlimited cert without login" do
with_config allow_anonymous_certs: true do
cert = expect_cert('UNLIMITED')
- post :create
+ api_post :create
assert_response :success
assert_equal cert.to_s, @response.body
end
@@ -15,7 +15,7 @@ class V1::CertsControllerTest < ActionController::TestCase
with_config allow_limited_certs: true do
login
cert = expect_cert('LIMITED')
- post :create
+ api_post :create
assert_response :success
assert_equal cert.to_s, @response.body
end
@@ -23,14 +23,14 @@ class V1::CertsControllerTest < ActionController::TestCase
test "fail to create cert when disabled" do
login :enabled? => false
- post :create
+ api_post :create
assert_access_denied
end
test "create unlimited cert" do
login effective_service_level: ServiceLevel.new(id: 2)
cert = expect_cert('UNLIMITED')
- post :create
+ api_post :create
assert_response :success
assert_equal cert.to_s, @response.body
end
@@ -38,13 +38,13 @@ class V1::CertsControllerTest < ActionController::TestCase
test "GET still works as an alias" do
login effective_service_level: ServiceLevel.new(id: 2)
cert = expect_cert('UNLIMITED')
- get :show
+ api_get :show
assert_response :success
assert_equal cert.to_s, @response.body
end
test "redirect if no eip service offered" do
- post :create
+ api_post :create
assert_response :redirect
end
@@ -57,4 +57,10 @@ class V1::CertsControllerTest < ActionController::TestCase
returns(cert)
return cert
end
+
+ # overwrite defaults from ApiController because we don't do json here.
+ def add_api_defaults(args)
+ add_defaults args, version: '2'
+ end
+
end
diff --git a/test/functional/v1/identities_controller_test.rb b/test/functional/api/identities_controller_test.rb
index 6410c44..57345c8 100644
--- a/test/functional/v1/identities_controller_test.rb
+++ b/test/functional/api/identities_controller_test.rb
@@ -1,15 +1,15 @@
require_relative '../../test_helper'
-class V1::IdentitiesControllerTest < ActionController::TestCase
+class Api::IdentitiesControllerTest < ApiControllerTest
test "api monitor can fetch identity" do
monitor_auth do
identity = FactoryGirl.create :identity
- get :show, :id => identity.address, :format => 'json'
+ api_get :show, :id => identity.address, :format => 'json'
assert_response :success
assert_equal identity, assigns(:identity)
- get :show, :id => "blahblahblah", :format => 'json'
+ api_get :show, :id => "blahblahblah", :format => 'json'
assert_response :not_found
end
end
@@ -17,7 +17,7 @@ class V1::IdentitiesControllerTest < ActionController::TestCase
test "anonymous cannot fetch identity" do
identity = FactoryGirl.create :identity
- get :show, :id => identity.address, :format => 'json'
+ api_get :show, :id => identity.address, :format => 'json'
assert_response :forbidden
end
diff --git a/test/functional/v1/messages_controller_test.rb b/test/functional/api/messages_controller_test.rb
index 67f34a1..e586980 100644
--- a/test/functional/v1/messages_controller_test.rb
+++ b/test/functional/api/messages_controller_test.rb
@@ -1,6 +1,6 @@
require 'test_helper'
-class V1::MessagesControllerTest < ActionController::TestCase
+class Api::MessagesControllerTest < ApiControllerTest
setup do
@user = FactoryGirl.build(:user)
@@ -13,9 +13,8 @@ class V1::MessagesControllerTest < ActionController::TestCase
test "get the motd" do
with_config("customization_directory" => Rails.root+'test/files') do
login @user
- get :index, :locale => 'es'
+ api_get :index, :locale => 'es'
body = JSON.parse(response.body)
- p body
message1 = "<p>\"This\" is a <strong>very</strong> fine message. <a href=\"https://bitmask.net\">https://bitmask.net</a></p>\n"
assert_equal 2, body.size, 'there should be two messages'
assert_equal message1, body.first["text"], 'first message text should match files/motd/1.en.md'
@@ -25,7 +24,7 @@ class V1::MessagesControllerTest < ActionController::TestCase
test "get localized motd" do
with_config("customization_directory" => Rails.root+'test/files') do
login @user
- get :index, :locale => 'de'
+ api_get :index, :locale => 'de'
body = JSON.parse(response.body)
message1 = "<p>Dies ist eine sehr feine Nachricht. <a href=\"https://bitmask.net\">https://bitmask.net</a></p>\n"
assert_equal message1, body.first["text"], 'first message text should match files/motd/1.de.md'
@@ -34,7 +33,7 @@ class V1::MessagesControllerTest < ActionController::TestCase
test "get empty motd" do
login @user
- get :index
+ api_get :index
assert_equal "[]", response.body, "motd response should be empty if no motd directory exists"
end
@@ -59,7 +58,7 @@ class V1::MessagesControllerTest < ActionController::TestCase
test "get messages for user" do
login @user
- get :index
+ api_get :index
assert response.body.include? @message.text
assert response.body.include? @message.id
end
@@ -79,7 +78,7 @@ class V1::MessagesControllerTest < ActionController::TestCase
login @user
put :update, :id => @message.id
@message.reload
- get :index
+ api_get :index
assert !(response.body.include? @message.text)
assert !(response.body.include? @message.id)
end
@@ -92,7 +91,7 @@ class V1::MessagesControllerTest < ActionController::TestCase
end
test "fails if not authenticated" do
- get :index, :format => :json
+ api_get :index, :format => :json
assert_login_required
end
=end
diff --git a/test/functional/v1/services_controller_test.rb b/test/functional/api/services_controller_test.rb
index 039eb27..cb85edf 100644
--- a/test/functional/v1/services_controller_test.rb
+++ b/test/functional/api/services_controller_test.rb
@@ -1,16 +1,16 @@
require 'test_helper'
-class V1::ServicesControllerTest < ActionController::TestCase
+class Api::ServicesControllerTest < ApiControllerTest
test "anonymous user gets login required service info" do
- get :show, format: :json
+ api_get :show, format: :json
assert_json_response error: 'not_authorized_login',
message: 'Please log in to perform that action.'
end
test "anonymous user gets vpn service info" do
with_config allow_anonymous_certs: true do
- get :show, format: :json
+ api_get :show, format: :json
assert_json_response name: 'anonymous',
eip_rate_limit: false,
description: 'anonymous access to the VPN'
@@ -19,7 +19,7 @@ class V1::ServicesControllerTest < ActionController::TestCase
test "user can see their service info" do
login
- get :show, format: :json
+ api_get :show, format: :json
default_level = APP_CONFIG[:default_service_level]
assert_json_response APP_CONFIG[:service_levels][default_level]
end
diff --git a/test/functional/v1/sessions_controller_test.rb b/test/functional/api/sessions_controller_test.rb
index 8bb6acd..06a3c22 100644
--- a/test/functional/v1/sessions_controller_test.rb
+++ b/test/functional/api/sessions_controller_test.rb
@@ -3,7 +3,7 @@ require 'test_helper'
# This is a simple controller unit test.
# We're stubbing out both warden and srp.
# There's an integration test testing the full rack stack and srp
-class V1::SessionsControllerTest < ActionController::TestCase
+class Api::SessionsControllerTest < ApiControllerTest
setup do
@request.env['HTTP_HOST'] = 'api.lvh.me'
@@ -12,7 +12,7 @@ class V1::SessionsControllerTest < ActionController::TestCase
end
test "renders json" do
- get :new, :format => :json
+ api_get :new, :format => :json
assert_response :success
assert_json_error nil
end
@@ -22,7 +22,7 @@ class V1::SessionsControllerTest < ActionController::TestCase
strategy = stub :message => {:field => :translate_me}
request.env['warden'].stubs(:winning_strategy).returns(strategy)
I18n.expects(:t).with(:translate_me).at_least_once.returns("translation stub")
- get :new, :format => :json
+ api_get :new, :format => :json
assert_response 422
assert_json_error :field => "translation stub"
end
@@ -33,7 +33,7 @@ class V1::SessionsControllerTest < ActionController::TestCase
request.env['warden'].expects(:authenticate!)
# make sure we don't get a template missing error:
@controller.stubs(:render)
- post :create, :login => @user.login, 'A' => @client_hex
+ api_post :create, :login => @user.login, 'A' => @client_hex
end
test "should authenticate" do
@@ -42,9 +42,10 @@ class V1::SessionsControllerTest < ActionController::TestCase
handshake = stub(:to_hash => {h: "ash"})
session[:handshake] = handshake
- post :update, :id => @user.login, :client_auth => @client_hex
+ api_post :update, :id => @user.login, :client_auth => @client_hex
- assert_nil session[:handshake]
+ assert_nil session[:handshake],
+ 'session should be cleared to prevent session fixation attacks'
assert_response :success
assert json_response.keys.include?("id")
assert json_response.keys.include?("token")
@@ -55,7 +56,7 @@ class V1::SessionsControllerTest < ActionController::TestCase
test "destroy should logout" do
login
expect_logout
- delete :destroy
+ api_delete :destroy
assert_response 204
end
diff --git a/test/functional/v1/smtp_certs_controller_test.rb b/test/functional/api/smtp_certs_controller_test.rb
index 1b03995..393f090 100644
--- a/test/functional/v1/smtp_certs_controller_test.rb
+++ b/test/functional/api/smtp_certs_controller_test.rb
@@ -1,17 +1,17 @@
require 'test_helper'
-class V1::SmtpCertsControllerTest < ActionController::TestCase
+class Api::SmtpCertsControllerTest < ApiControllerTest
test "no smtp cert without login" do
with_config allow_anonymous_certs: true do
- post :create
+ api_post :create
assert_login_required
end
end
test "require service level with email" do
login
- post :create
+ api_post :create
assert_access_denied
end
@@ -19,14 +19,14 @@ class V1::SmtpCertsControllerTest < ActionController::TestCase
login effective_service_level: ServiceLevel.new(id: 2)
cert = expect_cert(@current_user.email_address)
cert.expects(:fingerprint).returns('fingerprint')
- post :create
+ api_post :create
assert_response :success
assert_equal cert.to_s, @response.body
end
test "fail to create cert when disabled" do
login :enabled? => false
- post :create
+ api_post :create
assert_access_denied
end
diff --git a/test/functional/token_auth_test.rb b/test/functional/api/token_auth_test.rb
index 53d5fb3..c7f91c7 100644
--- a/test/functional/token_auth_test.rb
+++ b/test/functional/api/token_auth_test.rb
@@ -3,15 +3,15 @@
# via static configured tokens.
#
-require_relative '../test_helper'
+require 'test_helper'
-class TokenAuthTest < ActionController::TestCase
- tests V1::ConfigsController
+class Api::TokenAuthTest < ApiControllerTest
+ tests Api::ConfigsController
def test_login_via_api_token
with_config(:allow_anonymous_certs => false) do
monitor_auth do
- get :index
+ api_get :index
assert assigns(:token), 'should have authenticated via api token'
assert assigns(:token).is_a? ApiToken
assert @controller.send(:current_user).is_a? ApiMonitorUser
@@ -26,10 +26,10 @@ class TokenAuthTest < ActionController::TestCase
with_config(new_config) do
monitor_auth do
request.env['REMOTE_ADDR'] = "1.1.1.1"
- get :index
+ api_get :index
assert_nil assigns(:token), "should not be able to auth with api token when ip restriction doesn't allow it"
request.env['REMOTE_ADDR'] = allowed
- get :index
+ api_get :index
assert assigns(:token), "should have authenticated via api token"
end
end
diff --git a/test/functional/v1/users_controller_test.rb b/test/functional/api/users_controller_test.rb
index df59c4d..b69770d 100644
--- a/test/functional/v1/users_controller_test.rb
+++ b/test/functional/api/users_controller_test.rb
@@ -1,6 +1,6 @@
-require_relative '../../test_helper'
+require 'test_helper'
-class V1::UsersControllerTest < ActionController::TestCase
+class Api::UsersControllerTest < ApiControllerTest
test "user can change settings" do
user = find_record :user
@@ -10,11 +10,11 @@ class V1::UsersControllerTest < ActionController::TestCase
Account.expects(:new).with(user).returns(account_settings)
login user
- put :update, :user => changed_attribs, :id => user.id, :format => :json
+ api_put :update, :user => changed_attribs, :id => user.id, :format => :json
assert_equal user, assigns[:user]
assert_response 204
- assert_equal " ", @response.body
+ assert @response.body.blank?, "Response should be blank"
end
test "admin can update user" do
@@ -25,7 +25,7 @@ class V1::UsersControllerTest < ActionController::TestCase
Account.expects(:new).with(user).returns(account_settings)
login :is_admin? => true
- put :update, :user => changed_attribs, :id => user.id, :format => :json
+ api_put :update, :user => changed_attribs, :id => user.id, :format => :json
assert_equal user, assigns[:user]
assert_response 204
@@ -34,7 +34,7 @@ class V1::UsersControllerTest < ActionController::TestCase
test "user cannot update other user" do
user = find_record :user
login
- put :update, id: user.id,
+ api_put :update, id: user.id,
user: record_attributes_for(:user_with_settings),
:format => :json
assert_access_denied
@@ -45,7 +45,7 @@ class V1::UsersControllerTest < ActionController::TestCase
user = User.new(user_attribs)
Account.expects(:create).with(user_attribs).returns(user)
- post :create, :user => user_attribs, :format => :json
+ api_post :create, :user => user_attribs, :format => :json
assert_nil session[:user_id]
assert_json_response user
@@ -59,7 +59,7 @@ class V1::UsersControllerTest < ActionController::TestCase
assert !user.valid?
Account.expects(:create).with(user_attribs).returns(user)
- post :create, :user => user_attribs, :format => :json
+ api_post :create, :user => user_attribs, :format => :json
assert_json_error user.errors.messages
assert_response 422
@@ -67,7 +67,7 @@ class V1::UsersControllerTest < ActionController::TestCase
test "admin can autocomplete users" do
login :is_admin? => true
- get :index, :query => 'a', :format => :json
+ api_get :index, :query => 'a', :format => :json
assert_response :success
assert assigns(:users)
@@ -76,7 +76,7 @@ class V1::UsersControllerTest < ActionController::TestCase
test "create returns forbidden if registration is closed" do
user_attribs = record_attributes_for :user
with_config(allow_registration: false) do
- post :create, :user => user_attribs, :format => :json
+ api_post :create, :user => user_attribs, :format => :json
assert_response :forbidden
end
end
@@ -84,20 +84,28 @@ class V1::UsersControllerTest < ActionController::TestCase
test "admin can show user" do
user = FactoryGirl.create :user
login :is_admin? => true
- get :show, :id => 0, :login => user.login, :format => :json
+ api_get :show, :id => 0, :login => user.login, :format => :json
assert_response :success
- assert_json_response user
- get :show, :id => user.id, :format => :json
+ assert_json_response user.to_hash
+ api_get :show, :id => user.id, :format => :json
assert_response :success
- assert_json_response user
- get :show, :id => "0", :format => :json
+ assert_json_response user.to_hash
+ api_get :show, :id => "0", :format => :json
assert_response :not_found
end
+ test "admin can show is_admin property" do
+ user = FactoryGirl.create :user, login: "admin2"
+ login user
+ api_get :show, :id => user.id, :format => :json
+ assert_response :success
+ assert_json_response user.to_hash.merge(:is_admin => true)
+ end
+
test "normal users cannot show user" do
user = find_record :user
login
- get :show, :id => 0, :login => user.login, :format => :json
+ api_get :show, :id => 0, :login => user.login, :format => :json
assert_access_denied
end
@@ -106,9 +114,9 @@ class V1::UsersControllerTest < ActionController::TestCase
with_config(allow_registration: false, invite_required: true) do
monitor_auth do
user_attribs = record_attributes_for :test_user
- post :create, :user => user_attribs, :format => :json
+ api_post :create, :user => user_attribs, :format => :json
assert_response :success
- delete :destroy, :id => assigns(:user).id, :format => :json
+ api_delete :destroy, :id => assigns(:user).id, :format => :json
assert_response :success
end
end
@@ -117,17 +125,17 @@ class V1::UsersControllerTest < ActionController::TestCase
test "api monitor auth cannot create normal users" do
monitor_auth do
user_attribs = record_attributes_for :user
- post :create, :user => user_attribs, :format => :json
+ api_post :create, :user => user_attribs, :format => :json
assert_response :forbidden
end
end
- test "api monitor auth cannot delete normal users" do
- post :create, :user => record_attributes_for(:user), :format => :json
+ test "api monitor auth cannot api_delete normal users" do
+ api_post :create, :user => record_attributes_for(:user), :format => :json
assert_response :success
normal_user_id = assigns(:user).id
monitor_auth do
- delete :destroy, :id => normal_user_id, :format => :json
+ api_delete :destroy, :id => normal_user_id, :format => :json
assert_response :forbidden
end
end
diff --git a/test/functional/home_controller_test.rb b/test/functional/home_controller_test.rb
new file mode 100644
index 0000000..cafaac5
--- /dev/null
+++ b/test/functional/home_controller_test.rb
@@ -0,0 +1,16 @@
+require 'test_helper'
+
+class HomeControllerTest < ActionController::TestCase
+
+ def test_renders_okay
+ get :index
+ assert_response :success
+ end
+
+ def test_other_formats_trigger_406
+ assert_raises ActionController::UnknownFormat do
+ get :index, format: :xml
+ end
+ end
+
+end
diff --git a/test/functional/static_config_controller_test.rb b/test/functional/static_config_controller_test.rb
index 9c2cfef..7027bf8 100644
--- a/test/functional/static_config_controller_test.rb
+++ b/test/functional/static_config_controller_test.rb
@@ -1,7 +1,7 @@
require 'test_helper'
# use minitest for stubbing, rather than bloated mocha
-require 'minitest/stub_const'
+require 'minitest/mock'
class StaticConfigControllerTest < ActionController::TestCase
@@ -9,7 +9,7 @@ class StaticConfigControllerTest < ActionController::TestCase
end
def test_provider_success
- StaticConfigController.stub_const(:PROVIDER_JSON, file_path('provider.json')) do
+ @controller.stub(:provider_json, file_path('provider.json')) do
get :provider, format: :json
assert_equal 'application/json', @response.content_type
assert_response :success
@@ -17,7 +17,7 @@ class StaticConfigControllerTest < ActionController::TestCase
end
def test_provider_not_modified
- StaticConfigController.stub_const(:PROVIDER_JSON, file_path('provider.json')) do
+ @controller.stub(:provider_json, file_path('provider.json')) do
request.env["HTTP_IF_MODIFIED_SINCE"] = File.mtime(file_path('provider.json')).rfc2822()
get :provider, format: :json
assert_response 304
diff --git a/test/functional/users_controller_test.rb b/test/functional/users_controller_test.rb
index 7b24098..2794422 100644
--- a/test/functional/users_controller_test.rb
+++ b/test/functional/users_controller_test.rb
@@ -1,20 +1,7 @@
-require_relative '../test_helper'
+require 'test_helper'
class UsersControllerTest < ActionController::TestCase
- test "should get new" do
- get :new
- assert_equal User, assigns(:user).class
- assert_response :success
- end
-
- test "new should redirect logged in users" do
- login
- get :new
- assert_response :redirect
- assert_redirected_to home_path
- end
-
test "failed show without login" do
user = find_record :user
get :show, :id => user.id
@@ -67,8 +54,8 @@ class UsersControllerTest < ActionController::TestCase
nonid = 'thisisnotanexistinguserid'
login :is_admin? => true
get :show, :id => nonid
+ assert_error_response :no_such_user
assert_response :redirect
- assert_equal({:alert => "No such user."}, flash.to_hash)
assert_redirected_to users_path
end
@@ -163,11 +150,4 @@ class UsersControllerTest < ActionController::TestCase
assert !assigns(:user).enabled?
end
- test "new redirects if registration is closed" do
- with_config(allow_registration: false) do
- get :new
- assert_response :redirect
- assert_redirected_to home_path
- end
- end
end
diff --git a/test/integration/api/cert_test.rb b/test/integration/api/cert_test.rb
index 772901d..289d3c6 100644
--- a/test/integration/api/cert_test.rb
+++ b/test/integration/api/cert_test.rb
@@ -5,7 +5,7 @@ class CertTest < ApiIntegrationTest
test "retrieve eip cert" do
login
- get '/1/cert', {}, RACK_ENV
+ get cert_url, {}, RACK_ENV
assert_text_response
assert_response_includes "BEGIN RSA PRIVATE KEY"
assert_response_includes "END RSA PRIVATE KEY"
@@ -14,13 +14,13 @@ class CertTest < ApiIntegrationTest
end
test "fetching certs requires login by default" do
- get '/1/cert', {}, RACK_ENV
+ get cert_url, {}, RACK_ENV
assert_login_required
end
test "retrieve anonymous eip cert" do
with_config allow_anonymous_certs: true do
- get '/1/cert', {}, RACK_ENV
+ get cert_url, {}, RACK_ENV
assert_text_response
assert_response_includes "BEGIN RSA PRIVATE KEY"
assert_response_includes "END RSA PRIVATE KEY"
@@ -28,4 +28,9 @@ class CertTest < ApiIntegrationTest
assert_response_includes "END CERTIFICATE"
end
end
+
+ def cert_url
+ "/#{api_version}/cert"
+ end
+
end
diff --git a/test/integration/api/signup_test.rb b/test/integration/api/signup_test.rb
index 7216496..dc24420 100644
--- a/test/integration/api/signup_test.rb
+++ b/test/integration/api/signup_test.rb
@@ -1,4 +1,4 @@
-require_relative '../../test_helper'
+require 'test_helper'
require_relative 'srp_test'
class SignupTest < SrpTest
diff --git a/test/integration/api/smtp_cert_test.rb b/test/integration/api/smtp_cert_test.rb
index 681d509..3adddfd 100644
--- a/test/integration/api/smtp_cert_test.rb
+++ b/test/integration/api/smtp_cert_test.rb
@@ -3,15 +3,10 @@ require 'openssl'
class SmtpCertTest < ApiIntegrationTest
- setup do
- @testcode = InviteCode.new
- @testcode.save!
- end
-
test "retrieve smtp cert" do
- @user = FactoryGirl.create :user, effective_service_level_code: 2, :invite_code => @testcode.invite_code
+ @user = create_invited_user effective_service_level_code: 2
login
- post '/1/smtp_cert', {}, RACK_ENV
+ post smtp_cert_url, {}, RACK_ENV
assert_text_response
assert_response_includes "BEGIN RSA PRIVATE KEY"
assert_response_includes "END RSA PRIVATE KEY"
@@ -20,9 +15,9 @@ class SmtpCertTest < ApiIntegrationTest
end
test "cert and key" do
- @user = FactoryGirl.create :user, effective_service_level_code: 2, :invite_code => @testcode.invite_code
+ @user = create_invited_user effective_service_level_code: 2
login
- post '/1/smtp_cert', {}, RACK_ENV
+ post smtp_cert_url, {}, RACK_ENV
assert_text_response
cert = OpenSSL::X509::Certificate.new(get_response.body)
key = OpenSSL::PKey::RSA.new(get_response.body)
@@ -32,9 +27,9 @@ class SmtpCertTest < ApiIntegrationTest
end
test "fingerprint is stored with identity" do
- @user = FactoryGirl.create :user, effective_service_level_code: 2, :invite_code => @testcode.invite_code
+ @user = create_invited_user effective_service_level_code: 2
login
- post '/1/smtp_cert', {}, RACK_ENV
+ post smtp_cert_url, {}, RACK_ENV
assert_text_response
cert = OpenSSL::X509::Certificate.new(get_response.body)
fingerprint = OpenSSL::Digest::SHA1.hexdigest(cert.to_der).scan(/../).join(':')
@@ -46,16 +41,19 @@ class SmtpCertTest < ApiIntegrationTest
end
test "fetching smtp certs requires email account" do
-
login
- post '/1/smtp_cert', {}, RACK_ENV
+ post smtp_cert_url, {}, RACK_ENV
assert_access_denied
end
test "no anonymous smtp certs" do
with_config allow_anonymous_certs: true do
- post '/1/smtp_cert', {}, RACK_ENV
+ post smtp_cert_url, {}, RACK_ENV
assert_login_required
end
end
+
+ def smtp_cert_url
+ "/#{api_version}/smtp_cert"
+ end
end
diff --git a/test/integration/api/srp_test.rb b/test/integration/api/srp_test.rb
index 463abcd..b9605f9 100644
--- a/test/integration/api/srp_test.rb
+++ b/test/integration/api/srp_test.rb
@@ -14,7 +14,7 @@ class SrpTest < RackTest
# this test wraps the api and implements the interface the ruby-srp client.
def handshake(login, aa)
- post "http://api.lvh.me:3000/1/sessions.json",
+ post api_url("sessions.json"),
:login => login,
'A' => aa,
:format => :json
@@ -27,7 +27,7 @@ class SrpTest < RackTest
end
def validate(m)
- put "http://api.lvh.me:3000/1/sessions/" + @login + '.json',
+ put api_url("sessions/#{@login}.json"),
:client_auth => m,
:format => :json
return JSON.parse(last_response.body)
@@ -39,7 +39,7 @@ class SrpTest < RackTest
def register_user(login = "integration_test", password = 'srp, verify me!', invite_code = @testcode.invite_code)
cleanup_user(login)
- post 'http://api.lvh.me:3000/1/users.json',
+ post api_url('users.json'),
user_params(login: login, password: password, invite_code: invite_code)
assert(@user = User.find_by_login(login), 'user should have been created: %s' % last_response_errors)
@login = login
@@ -47,7 +47,7 @@ class SrpTest < RackTest
end
def update_user(params)
- put "http://api.lvh.me:3000/1/users/" + @user.id + '.json',
+ put api_url("users/#{@user.id}.json"),
user_params(params),
auth_headers
end
@@ -68,7 +68,7 @@ class SrpTest < RackTest
end
def logout(params=nil, headers=nil)
- delete "http://api.lvh.me:3000/1/logout.json",
+ delete api_url("logout.json"),
params || {format: :json},
headers || auth_headers
end
@@ -112,4 +112,12 @@ class SrpTest < RackTest
rescue
""
end
+
+ def api_url(path)
+ "http://api.lvh.me:3000/#{api_version}/#{path}"
+ end
+
+ def api_version
+ 2
+ end
end
diff --git a/test/integration/api/token_test.rb b/test/integration/api/token_auth_test.rb
index dafbfb7..7b20b00 100644
--- a/test/integration/api/token_test.rb
+++ b/test/integration/api/token_auth_test.rb
@@ -1,7 +1,7 @@
-require_relative '../../test_helper'
+require 'test_helper'
require_relative 'srp_test'
-class TokenTest < SrpTest
+class TokenAuthTest < SrpTest
setup do
register_user
diff --git a/test/integration/api/update_account_test.rb b/test/integration/api/update_account_test.rb
index 16bbb8c..1492006 100644
--- a/test/integration/api/update_account_test.rb
+++ b/test/integration/api/update_account_test.rb
@@ -14,7 +14,7 @@ class UpdateAccountTest < SrpTest
test "require token" do
authenticate
- put "http://api.lvh.me:3000/1/users/" + @user.id + '.json',
+ put "http://api.lvh.me:3000/2/users/" + @user.id + '.json',
user_params(password: "No! Verify me instead.")
assert_login_required
end
diff --git a/test/integration/browser/account_livecycle_test.rb b/test/integration/browser/account_livecycle_test.rb
new file mode 100644
index 0000000..85dbf13
--- /dev/null
+++ b/test/integration/browser/account_livecycle_test.rb
@@ -0,0 +1,114 @@
+require 'test_helper'
+
+class AccountLivecycleTest < BrowserIntegrationTest
+
+ teardown do
+ Identity.destroy_all_orphaned
+ end
+
+ test "signup successfully when invited" do
+ username, password = submit_signup
+ assert page.has_content?("Welcome #{username}")
+ click_on 'Log Out'
+ assert page.has_content?("Log In")
+ assert_equal '/', current_path
+ assert user = User.find_by_login(username)
+ user.account.destroy
+ end
+
+ test "signup successfully without invitation" do
+ with_config invite_required: false do
+
+ username ||= "test_#{SecureRandom.urlsafe_base64}".downcase
+ password ||= SecureRandom.base64
+
+ visit '/signup'
+ fill_in 'Username', with: username
+ fill_in 'Password', with: password
+ fill_in 'Password confirmation', with: password
+ click_on 'Sign Up'
+
+ assert page.has_content?("Welcome #{username}")
+ end
+ end
+
+ test "signup with username ending in dot json" do
+ username = Faker::Internet.user_name + '.json'
+ submit_signup username
+ assert page.has_content?("Welcome #{username}")
+ end
+
+ test "signup with reserved username" do
+ username = 'certmaster'
+ submit_signup username
+ assert page.has_content?("is reserved.")
+ end
+
+ test "successful login" do
+ username, password = submit_signup
+ click_on 'Log Out'
+ attempt_login(username, password)
+ assert page.has_content?("Welcome #{username}")
+ within('.sidenav li.active') do
+ assert page.has_content?("Overview")
+ end
+ User.find_by_login(username).account.destroy
+ end
+
+ test "failed login" do
+ visit '/'
+ attempt_login("username", "wrong password")
+ assert_invalid_login(page)
+ end
+
+ test "account destruction" do
+ username, password = submit_signup
+
+ click_on I18n.t('account_settings')
+ click_on I18n.t('destroy_my_account')
+ assert page.has_content?(I18n.t('account_destroyed'))
+ assert_equal 1, Identity.by_address.key("#{username}@test.me").count
+ attempt_login(username, password)
+ assert_invalid_login(page)
+ end
+
+ test "handle blocked after account destruction" do
+ username, password = submit_signup
+ click_on I18n.t('account_settings')
+ click_on I18n.t('destroy_my_account')
+ submit_signup(username)
+ assert page.has_content?('has already been taken')
+ end
+
+ test "change pgp key" do
+ with_config user_actions: ['change_pgp_key'] do
+ pgp_key = FactoryGirl.build :pgp_key
+ login
+ click_on "Account Settings"
+ within('#update_pgp_key') do
+ fill_in 'Public key', with: pgp_key
+ click_on 'Save'
+ end
+ page.assert_selector 'input[value="Saving..."]'
+ # at some point we're done:
+ page.assert_no_selector 'input[value="Saving..."]'
+ assert page.has_field? 'Public key', with: pgp_key.to_s
+ @user.reload
+ assert_equal pgp_key, @user.public_key
+ end
+ end
+
+ def attempt_login(username, password)
+ click_on 'Log In'
+ fill_in 'Username', with: username
+ fill_in 'Password', with: password
+ click_on 'Log In'
+ end
+
+ def assert_invalid_login(page)
+ assert page.has_selector? '.btn-primary.disabled'
+ assert page.has_content? I18n.t(:invalid_user_pass)
+ assert page.has_no_selector? '.btn-primary.disabled'
+ end
+
+end
diff --git a/test/integration/browser/account_test.rb b/test/integration/browser/account_livecycle_test.rb.orig
index 50adb23..d1f800b 100644
--- a/test/integration/browser/account_test.rb
+++ b/test/integration/browser/account_livecycle_test.rb.orig
@@ -1,6 +1,6 @@
require 'test_helper'
-class AccountTest < BrowserIntegrationTest
+class AccountLivecycleTest < BrowserIntegrationTest
teardown do
Identity.destroy_all_orphaned
@@ -80,24 +80,6 @@ class AccountTest < BrowserIntegrationTest
assert page.has_content?('has already been taken')
end
- test "default user actions" do
- login
- click_on "Account Settings"
- assert page.has_content? I18n.t('destroy_my_account')
- assert page.has_no_css? '#update_login_and_password'
- assert page.has_no_css? '#update_pgp_key'
- end
-
- test "default admin actions" do
- login
- with_config admins: [@user.login] do
- click_on "Account Settings"
- assert page.has_content? I18n.t('destroy_my_account')
- assert page.has_no_css? '#update_login_and_password'
- assert page.has_css? '#update_pgp_key'
- end
- end
-
test "change pgp key" do
with_config user_actions: ['change_pgp_key'] do
pgp_key = FactoryGirl.build :pgp_key
@@ -116,6 +98,8 @@ class AccountTest < BrowserIntegrationTest
end
end
+<<<<<<< HEAD:test/integration/browser/account_livecycle_test.rb
+=======
# trying to seed an invalid A for srp login
test "detects attempt to circumvent SRP" do
@@ -133,7 +117,7 @@ class AccountTest < BrowserIntegrationTest
end
test "reports internal server errors" do
- V1::UsersController.any_instance.stubs(:create).raises
+ Api::UsersController.any_instance.stubs(:create).raises
submit_signup
assert page.has_content?("server failed")
end
@@ -152,6 +136,7 @@ class AccountTest < BrowserIntegrationTest
assert page.has_no_content?("Password")
end
+>>>>>>> api: allow version bumping - bump to 2:test/integration/browser/account_test.rb
def attempt_login(username, password)
click_on 'Log In'
fill_in 'Username', with: username
@@ -165,12 +150,4 @@ class AccountTest < BrowserIntegrationTest
assert page.has_no_selector? '.btn-primary.disabled'
end
- def inject_malicious_js
- page.execute_script <<-EOJS
- var calc = new srp.Calculate();
- calc.A = function(_a) {return "00";};
- calc.S = calc.A;
- srp.session = new srp.Session(null, calc);
- EOJS
- end
end
diff --git a/test/integration/browser/admin_test.rb b/test/integration/browser/admin_test.rb
index 902c981..0b43c29 100644
--- a/test/integration/browser/admin_test.rb
+++ b/test/integration/browser/admin_test.rb
@@ -2,6 +2,24 @@ require 'test_helper'
class AdminTest < BrowserIntegrationTest
+ test "default user actions" do
+ login
+ click_on "Account Settings"
+ assert page.has_content? I18n.t('destroy_my_account')
+ assert page.has_no_css? '#update_login_and_password'
+ assert page.has_no_css? '#update_pgp_key'
+ end
+
+ test "default admin actions" do
+ login
+ with_config admins: [@user.login] do
+ click_on "Account Settings"
+ assert page.has_content? I18n.t('destroy_my_account')
+ assert page.has_no_css? '#update_login_and_password'
+ assert page.has_css? '#update_pgp_key'
+ end
+ end
+
test "clear blocked handle" do
id = FactoryGirl.create :identity
submit_signup(id.login)
diff --git a/test/integration/browser/password_validation_test.rb b/test/integration/browser/password_validation_test.rb
index 45eb0bf..51fcc5d 100644
--- a/test/integration/browser/password_validation_test.rb
+++ b/test/integration/browser/password_validation_test.rb
@@ -5,26 +5,26 @@ class PasswordValidationTest < BrowserIntegrationTest
test "password confirmation is validated" do
username ||= "test_#{SecureRandom.urlsafe_base64}".downcase
password ||= SecureRandom.base64
- visit '/users/new'
+ visit '/signup'
fill_in 'Username', with: username
fill_in 'Password', with: password
fill_in 'Password confirmation', with: password + "-typo"
click_on 'Sign Up'
assert page.has_content? "does not match."
- assert_equal '/users/new', current_path
+ assert_equal '/signup', current_path
assert page.has_selector? ".error #srp_password_confirmation"
end
test "password needs to be at least 8 chars long" do
username ||= "test_#{SecureRandom.urlsafe_base64}".downcase
password ||= SecureRandom.base64[0,7]
- visit '/users/new'
+ visit '/signup'
fill_in 'Username', with: username
fill_in 'Password', with: password
fill_in 'Password confirmation', with: password
click_on 'Sign Up'
assert page.has_content? "needs to be at least 8 characters long"
- assert_equal '/users/new', current_path
+ assert_equal '/signup', current_path
assert page.has_selector? ".error #srp_password"
end
end
diff --git a/test/integration/browser/security_test.rb b/test/integration/browser/security_test.rb
new file mode 100644
index 0000000..825d50b
--- /dev/null
+++ b/test/integration/browser/security_test.rb
@@ -0,0 +1,52 @@
+require 'test_helper'
+
+class SecurityTest < BrowserIntegrationTest
+
+ teardown do
+ Identity.destroy_all_orphaned
+ end
+
+ # trying to seed an invalid A for srp login
+ test "detects attempt to circumvent SRP" do
+ InviteCodeValidator.any_instance.stubs(:validate)
+
+ user = FactoryGirl.create :user
+ visit '/login'
+ fill_in 'Username', with: user.login
+ fill_in 'Password', with: "password"
+ inject_malicious_js
+ click_on 'Log In'
+ assert page.has_content?("Invalid random key")
+ assert page.has_no_content?("Welcome")
+ user.destroy
+ end
+
+ test "reports internal server errors" do
+ Api::UsersController.any_instance.stubs(:create).raises
+ submit_signup
+ assert page.has_content?("server failed")
+ end
+
+ test "does not render signup form without js" do
+ Capybara.current_driver = :rack_test # no js
+ visit '/signup'
+ assert page.has_no_content?("Username")
+ assert page.has_no_content?("Password")
+ end
+
+ test "does not render login form without js" do
+ Capybara.current_driver = :rack_test # no js
+ visit '/login'
+ assert page.has_no_content?("Username")
+ assert page.has_no_content?("Password")
+ end
+
+ def inject_malicious_js
+ page.execute_script <<-EOJS
+ var calc = new srp.Calculate();
+ calc.A = function(_a) {return "00";};
+ calc.S = calc.A;
+ srp.session = new srp.Session(null, calc);
+ EOJS
+ end
+end
diff --git a/test/integration/locale_path_test.rb b/test/integration/locale_path_test.rb
index 738e7f5..22293dc 100644
--- a/test/integration/locale_path_test.rb
+++ b/test/integration/locale_path_test.rb
@@ -21,6 +21,11 @@ require 'test_helper'
#
class LocalePathTest < ActionDispatch::IntegrationTest
+
+ teardown do
+ I18n.locale = 'en'
+ end
+
test "redirect if accept-language is not default locale" do
get_via_redirect '/', {}, 'HTTP_ACCEPT_LANGUAGE' => 'de'
assert_equal '/de', path
@@ -55,4 +60,4 @@ class LocalePathTest < ActionDispatch::IntegrationTest
@controller.send(:default_url_options)
end
-end \ No newline at end of file
+end
diff --git a/test/integration/navigation_test.rb b/test/integration/navigation_test.rb
deleted file mode 100644
index eec8c0e..0000000
--- a/test/integration/navigation_test.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-require 'test_helper'
-
-class NavigationTest < ActionDispatch::IntegrationTest
-
- # test "the truth" do
- # assert true
- # end
-end
-
diff --git a/test/leap_web_users_test.rb b/test/leap_web_users_test.rb
deleted file mode 100644
index f142e54..0000000
--- a/test/leap_web_users_test.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-require 'test_helper'
-
-class LeapWebUsersTest < ActiveSupport::TestCase
- test "module exists" do
- assert_kind_of Module, LeapWebUsers
- end
-end
diff --git a/test/performance/browsing_test.rb b/test/performance/browsing_test.rb
deleted file mode 100644
index 3fea27b..0000000
--- a/test/performance/browsing_test.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-require 'test_helper'
-require 'rails/performance_test_help'
-
-class BrowsingTest < ActionDispatch::PerformanceTest
- # Refer to the documentation for all available options
- # self.profile_options = { :runs => 5, :metrics => [:wall_time, :memory]
- # :output => 'tmp/performance', :formats => [:flat] }
-
- def test_homepage
- get '/'
- end
-end
diff --git a/test/support/api_controller_test.rb b/test/support/api_controller_test.rb
new file mode 100644
index 0000000..97d86fc
--- /dev/null
+++ b/test/support/api_controller_test.rb
@@ -0,0 +1,29 @@
+class ApiControllerTest < ActionController::TestCase
+
+ def api_get(*args)
+ get *add_api_defaults(args)
+ end
+
+ def api_post(*args)
+ post *add_api_defaults(args)
+ end
+
+ def api_delete(*args)
+ delete *add_api_defaults(args)
+ end
+
+ def api_put(*args)
+ put *add_api_defaults(args)
+ end
+
+ def add_api_defaults(args)
+ add_defaults args, version: '2', format: :json
+ end
+
+ def add_defaults(args, defaults)
+ opts = args.extract_options!
+ opts.reverse_merge! defaults
+ args << opts
+ args
+ end
+end
diff --git a/test/support/api_integration_test.rb b/test/support/api_integration_test.rb
index 3b3481b..7942558 100644
--- a/test/support/api_integration_test.rb
+++ b/test/support/api_integration_test.rb
@@ -3,13 +3,12 @@ class ApiIntegrationTest < ActionDispatch::IntegrationTest
DUMMY_TOKEN = Token.new
RACK_ENV = {'HTTP_AUTHORIZATION' => %Q(Token token="#{DUMMY_TOKEN.to_s}")}
- setup do
- @testcode = InviteCode.new
- @testcode.save!
+ def api_version
+ 2
end
def login(user = nil)
- @user ||= user ||= FactoryGirl.create(:user, :invite_code => @testcode.invite_code)
+ @user ||= user ||= create_invited_user
# DUMMY_TOKEN will be frozen. So let's use a dup
@token ||= DUMMY_TOKEN.dup
# make sure @token is up to date if it already exists
@@ -19,6 +18,13 @@ class ApiIntegrationTest < ActionDispatch::IntegrationTest
@token.save
end
+ def create_invited_user(options = {})
+ @testcode = InviteCode.new
+ @testcode.save!
+ options.reverse_merge! invite_code: @testcode.invite_code
+ FactoryGirl.create :user, options
+ end
+
teardown do
if @user && @user.persisted?
@user.destroy_identities
diff --git a/test/support/assert_responses.rb b/test/support/assert_responses.rb
index 7724fb4..6a22642 100644
--- a/test/support/assert_responses.rb
+++ b/test/support/assert_responses.rb
@@ -71,21 +71,24 @@ module AssertResponses
end
def assert_login_required
- assert_error_response :not_authorized_login, :unauthorized
+ assert_error_response :not_authorized_login,
+ status: :unauthorized
end
def assert_access_denied
- assert_error_response :not_authorized, :forbidden
+ assert_error_response :not_authorized,
+ status: :forbidden
end
- def assert_error_response(key, status=nil)
- message = I18n.t(key)
+ def assert_error_response(key, options = {})
+ status=options.delete :status
+ message = I18n.t(key, options)
if content_type == 'application/json'
status ||= :unprocessable_entity
assert_json_response('error' => key.to_s, 'message' => message)
assert_response status
else
- assert_equal({:alert => message}, flash.to_hash)
+ assert_equal({'alert' => message}, flash.to_hash)
end
end
diff --git a/test/support/browser_integration_test.rb b/test/support/browser_integration_test.rb
index 1deb8fa..1f5e3d2 100644
--- a/test/support/browser_integration_test.rb
+++ b/test/support/browser_integration_test.rb
@@ -3,10 +3,11 @@
#
# Use this class for capybara based integration tests for the ui.
#
+require 'capybara/rails'
class BrowserIntegrationTest < ActionDispatch::IntegrationTest
# let's use dom_id inorder to identify sections
- include ActionController::RecordIdentifier
+ include ActionView::RecordIdentifier
CONFIG_RU = (Rails.root + 'config.ru').to_s
OUTER_APP = Rack::Builder.parse_file(CONFIG_RU).first
@@ -28,7 +29,7 @@ class BrowserIntegrationTest < ActionDispatch::IntegrationTest
Capybara.app_host = 'http://lvh.me:3003'
Capybara.server_port = 3003
Capybara.javascript_driver = :poltergeist
- Capybara.default_wait_time = 5
+ Capybara.default_max_wait_time = 5
# Make the Capybara DSL available
include Capybara::DSL
@@ -46,32 +47,17 @@ class BrowserIntegrationTest < ActionDispatch::IntegrationTest
end
def submit_signup(username = nil, password = nil)
-
- with_config invite_required: true do
-
- username ||= "test_#{SecureRandom.urlsafe_base64}".downcase
- password ||= SecureRandom.base64
- visit '/users/new'
- fill_in 'Username', with: username
- fill_in 'Password', with: password
+ username ||= "test_#{SecureRandom.urlsafe_base64}".downcase
+ password ||= SecureRandom.base64
+ visit '/signup'
+ fill_in 'Username', with: username
+ fill_in 'Password', with: password
+ if APP_CONFIG[:invite_required]
fill_in 'Invite code', with: @testcode.invite_code
- fill_in 'Password confirmation', with: password
- click_on 'Sign Up'
- return username, password
- end
-
- with_config invite_required: false do
-
- username ||= "test_#{SecureRandom.urlsafe_base64}".downcase
- password ||= SecureRandom.base64
- visit '/users/new'
- fill_in 'Username', with: username
- fill_in 'Password', with: password
- fill_in 'Password confirmation', with: password
- click_on 'Sign Up'
- return username, password
end
-
+ fill_in 'Password confirmation', with: password
+ click_on 'Sign Up'
+ return username, password
end
# currently this only works for tests with poltergeist.
@@ -101,7 +87,7 @@ class BrowserIntegrationTest < ActionDispatch::IntegrationTest
File.open(logfile_path, 'w') do |test_log|
test_log.puts self.class.name
test_log.puts "========================="
- test_log.puts __name__
+ test_log.puts name
test_log.puts Time.now
test_log.puts current_path
test_log.puts page.status_code
diff --git a/test/support/record_assertions.rb b/test/support/record_assertions.rb
new file mode 100644
index 0000000..30b947f
--- /dev/null
+++ b/test/support/record_assertions.rb
@@ -0,0 +1,10 @@
+module RecordAssertions
+
+ def assert_error(record, options)
+ options.each do |k, v|
+ errors = record.errors[k]
+ assert_equal I18n.t("errors.messages.#{v}"), errors.first
+ end
+ end
+
+end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index dfc6627..a06f710 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -1,5 +1,5 @@
ENV["RAILS_ENV"] = "test"
-require File.expand_path('../../config/environment', __FILE__)
+require_relative '../config/environment'
require 'rails/test_help'
require 'mocha/setup'
@@ -16,11 +16,11 @@ class ActiveSupport::TestCase
protected
def logfile_path
- Rails.root + 'tmp' + "#{self.class.name.underscore}.#{__name__}.log"
+ Rails.root + 'tmp' + "#{self.class.name.underscore}.#{name}.log"
end
def screenshot_path
- Rails.root + 'tmp' + "#{self.class.name.underscore}.#{__name__}.png"
+ Rails.root + 'tmp' + "#{self.class.name.underscore}.#{name}.png"
end
def file_path(name)
diff --git a/test/travis/ruby-version b/test/travis/ruby-version
deleted file mode 100644
index 68b3a4c..0000000
--- a/test/travis/ruby-version
+++ /dev/null
@@ -1 +0,0 @@
-1.9.3-p551
diff --git a/test/unit/email_test.rb b/test/unit/email_test.rb
index e858bd5..739b43e 100644
--- a/test/unit/email_test.rb
+++ b/test/unit/email_test.rb
@@ -1,4 +1,5 @@
require 'test_helper'
+require 'email'
class EmailTest < ActiveSupport::TestCase
diff --git a/test/unit/identity_test.rb b/test/unit/identity_test.rb
index 9d4bc90..e9173af 100644
--- a/test/unit/identity_test.rb
+++ b/test/unit/identity_test.rb
@@ -2,6 +2,7 @@ require_relative '../test_helper'
class IdentityTest < ActiveSupport::TestCase
include StubRecordHelper
+ include RecordAssertions
setup do
@user = find_record :user
@@ -22,7 +23,7 @@ class IdentityTest < ActiveSupport::TestCase
test "enabled identity requires destination" do
@id = Identity.new user: @user, address: @user.email_address
assert !@id.valid?
- assert_equal ["can't be blank"], @id.errors[:destination]
+ assert_error @id, destination: :blank
end
test "disabled identity requires no destination" do
@@ -62,7 +63,7 @@ class IdentityTest < ActiveSupport::TestCase
@id = Identity.create_for @user, address: alias_name, destination: forward_address
dup = Identity.build_for @user, address: alias_name, destination: forward_address
assert !dup.valid?
- assert_equal ["has already been taken"], dup.errors[:destination]
+ assert_error dup, destination: :taken
end
test "validates availability" do
@@ -70,7 +71,7 @@ class IdentityTest < ActiveSupport::TestCase
@id = Identity.create_for @user, address: alias_name, destination: forward_address
taken = Identity.build_for other_user, address: alias_name
assert !taken.valid?
- assert_equal ["has already been taken"], taken.errors[:address]
+ assert_error taken, address: :taken
end
test "setting and getting pgp key" do
@@ -133,7 +134,7 @@ class IdentityTest < ActiveSupport::TestCase
other_user = find_record :user
taken = Identity.build_for other_user, address: @id.address
assert !taken.valid?
- assert_equal ["has already been taken"], taken.errors[:address]
+ assert_error taken, address: :taken
end
test "destroy all orphaned identities" do
diff --git a/test/unit/invite_code_validator_test.rb b/test/unit/invite_code_validator_test.rb
index 62eeae6..934ba2e 100644
--- a/test/unit/invite_code_validator_test.rb
+++ b/test/unit/invite_code_validator_test.rb
@@ -3,9 +3,9 @@ require 'test_helper'
class InviteCodeValidatorTest < ActiveSupport::TestCase
test "user should not be created with invalid invite code" do
with_config invite_required: true do
- invalid_user = FactoryGirl.build(:user)
+ invalid_user = FactoryGirl.build(:user)
- assert !invalid_user.valid?
+ assert !invalid_user.valid?
end
end
@@ -30,7 +30,7 @@ class InviteCodeValidatorTest < ActiveSupport::TestCase
test "Invite count >= invite max uses is not accepted for new account signup" do
- validator = InviteCodeValidator.new nil
+ validator = InviteCodeValidator.new
user_code = InviteCode.new
user_code.invite_count = 1
@@ -46,7 +46,7 @@ class InviteCodeValidatorTest < ActiveSupport::TestCase
end
test "Invite count < invite max uses is accepted for new account signup" do
- validator = InviteCodeValidator.new nil
+ validator = InviteCodeValidator.new
user_code = InviteCode.create
user_code.save
@@ -60,7 +60,7 @@ class InviteCodeValidatorTest < ActiveSupport::TestCase
end
test "Invite count 0 is accepted for new account signup" do
- validator = InviteCodeValidator.new nil
+ validator = InviteCodeValidator.new
user_code = InviteCode.create
@@ -73,7 +73,7 @@ class InviteCodeValidatorTest < ActiveSupport::TestCase
end
test "There is an error message if the invite code does not exist" do
- validator = InviteCodeValidator.new nil
+ validator = InviteCodeValidator.new
user = FactoryGirl.build :user
user.invite_code = "wrongcode"
@@ -83,4 +83,4 @@ class InviteCodeValidatorTest < ActiveSupport::TestCase
assert_equal ["This is not a valid code"], user.errors[:invite_code]
end
-end \ No newline at end of file
+end
diff --git a/test/unit/tmp_user_test.rb b/test/unit/temporary_user_test.rb
index 1dea5f9..2c9e70f 100644
--- a/test/unit/tmp_user_test.rb
+++ b/test/unit/temporary_user_test.rb
@@ -1,21 +1,42 @@
require 'test_helper'
-class TmpUserTest < ActiveSupport::TestCase
+class TemporaryUserTest < ActiveSupport::TestCase
setup do
InviteCodeValidator.any_instance.stubs(:validate)
end
- test "tmp_user saved to tmp_users" do
- begin
- assert User.ancestors.include?(TemporaryUser)
+ test "TemporaryUser concern is applied" do
+ assert User.ancestors.include?(TemporaryUser)
+ end
+
+ test "temporary user has tmp_users as db" do
+ tmp_user = User.new :login => 'tmp_user_'+SecureRandom.hex(5).downcase
+ assert_equal 'leap_web_tmp_users', tmp_user.database.name
+ end
+ test "normal user has users as db" do
+ user = User.new :login => 'a'+SecureRandom.hex(5).downcase
+ assert_equal 'leap_web_users', user.database.name
+ end
+
+ test "user saved to users" do
+ begin
assert_difference('User.database.info["doc_count"]') do
normal_user = User.create!(:login => 'a'+SecureRandom.hex(5).downcase,
:password_verifier => 'ABCDEF0010101', :password_salt => 'ABCDEF')
refute normal_user.database.to_s.include?('tmp')
end
+ ensure
+ begin
+ normal_user.destroy
+ rescue
+ end
+ end
+ end
+ test "tmp_user saved to tmp_users" do
+ begin
assert_difference('User.tmp_database.info["doc_count"]') do
tmp_user = User.create!(:login => 'tmp_user_'+SecureRandom.hex(5).downcase,
:password_verifier => 'ABCDEF0010101', :password_salt => 'ABCDEF')
@@ -23,7 +44,6 @@ class TmpUserTest < ActiveSupport::TestCase
end
ensure
begin
- normal_user.destroy
tmp_user.destroy
rescue
end
diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb
index 9501d34..02e94df 100644
--- a/test/unit/user_test.rb
+++ b/test/unit/user_test.rb
@@ -71,6 +71,13 @@ class UserTest < ActiveSupport::TestCase
assert_equal key, @user.public_key
end
+ test "user to hash includes id, login, valid and enabled" do
+ hash = @user.to_hash
+ assert_equal @user.id, hash[:id]
+ assert_equal @user.valid?, hash[:ok]
+ assert_equal @user.login, hash[:login]
+ assert_equal @user.enabled?, hash[:enabled]
+ end
#