diff --git a/.gitignore b/.gitignore index b73c12c..a3c0dc4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ .bundle/ -log/*.log +*.log pkg/ catarse_moip-0.0.2.gem diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index fad9d6f..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "test/dummy"] - path = test/dummy - url = git://github.com/danielweinmann/catarse.git diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..46935a8 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,17 @@ +rvm: + - 1.9.3 + +node_js: + - "0.10" + +before_script: + - "psql -c 'create role catarse SUPERUSER LOGIN;' postgres" + - "psql -c 'create database catarse_test;' -U catarse postgres" + - "npm install -g jquery jsdom jasmine-node" + +script: + - "bundle exec rake" + +branches: + only: + - master diff --git a/Gemfile b/Gemfile index d4cc3ca..0bae460 100644 --- a/Gemfile +++ b/Gemfile @@ -1,122 +1,6 @@ source "http://rubygems.org" -# Declare your gem's dependencies in catarse_moip.gemspec. -# Bundler will treat runtime dependencies like base dependencies, and -# development dependencies will be added by default to the :development group. gemspec -source :gemcutter -source 'http://gems.github.com' -gem 'rails', '3.2.11' - -gem 'sidekiq', '= 2.4.0' -gem 'sinatra', require: false -gem 'foreman' -gem 'best_in_place' - -gem 'state_machine', require: 'state_machine/core' - -# Database and data related +gem 'moip', git: 'git://github.com/catarse/moip-ruby.git' gem 'pg' -gem 'pg_search' -gem 'postgres-copy' -gem 'schema_plus' - -gem 'catarse_paypal_express', git: 'git://github.com/devton/catarse_paypal_express.git', ref: '4fd17e269395ee4b3a32528ace0bcb7eec57a953' -#gem 'catarse_paypal_express', path: '../catarse_paypal_express' -#gem 'catarse_moip', git: 'git://github.com/devton/catarse_moip.git' -#gem 'catarse_moip', path: '../catarse_moip' -gem 'moip', git: 'git://github.com/moiplabs/moip-ruby.git' - -gem 'draper' - -# Frontend stuff -gem 'slim' -gem 'slim-rails' -gem 'jquery-rails' -gem 'initjs' - -# Authentication and Authorization -gem 'omniauth', "~> 1.1.0" -gem 'omniauth-openid', '~> 1.0.1' -gem 'omniauth-twitter', '~> 0.0.12' -gem 'omniauth-facebook', '~> 1.2.0' -gem 'omniauth-github', '~> 1.0.1' -gem 'omniauth-linkedin', '~> 0.0.6' -gem 'omniauth-yahoo', '~> 0.0.4' -gem 'devise', '1.5.3' -gem 'cancan', git: 'git://github.com/ryanb/cancan.git', branch: '2.0', ref: 'f1cebde51a87be149b4970a3287826bb63c0ac0b' - - -# Error reporting -gem "airbrake" - -# Email marketing -#gem 'mailchimp' -gem 'catarse_mailchimp', git: 'git://github.com/devton/catarse_mailchimp' - -# HTML manipulation and formatting -gem 'formtastic', "~> 2.1.1" -gem "auto_html", '= 1.4.2' -gem 'kaminari' -gem 'rails_autolink', '~> 1.0.7' - -# Uploads -gem 'carrierwave', '~> 0.7.0' -gem 'rmagick' -gem 'fog' - -# Other Tools -gem 'feedzirra' -gem 'validation_reflection', git: 'git://github.com/ncri/validation_reflection.git' -gem 'inherited_resources', '1.3.1' -gem 'has_scope' -gem 'spectator-validates_email', require: 'validates_email' -gem 'has_vimeo_video', '~> 0.0.5' -gem 'memoist', '~> 0.2.0' -gem 'wirble' -gem "on_the_spot" -gem 'weekdays' -gem 'brcep' -gem "RedCloth" -gem 'unicode' -gem 'enumerate_it' -gem 'httparty', '~> 0.6.1' -gem "rack-timeout" - -# Translations -gem 'http_accept_language' -gem 'routing-filter' #, :git => 'git://github.com/svenfuchs/routing-filter.git' - -# Administration -gem "meta_search", "1.1.3" - -# Payment -gem 'activemerchant', '1.17.0', require: 'active_merchant' -gem 'httpclient', '2.2.5' -gem 'selenium-webdriver' - -# Server -gem 'thin' - -group :assets do - gem 'sass-rails', '~> 3.2.5' - gem 'coffee-rails', '~> 3.2.2' - gem "compass-rails", "~> 1.0.2" - gem 'uglifier', '>= 1.0.3' - gem 'compass-960-plugin', '~> 0.10.4' -end - -group :test, :development do - gem 'launchy' - gem 'database_cleaner' - gem 'rspec-rails', "~> 2.10.0" - gem 'mocha', '0.10.4' - gem 'shoulda' - gem 'factory_girl_rails', '1.7.0' - gem 'capybara', ">= 1.0.1" -end - -group :development do - gem 'mailcatcher' -end diff --git a/Gemfile.lock b/Gemfile.lock index 97159ce..02f8dfd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,522 +1,128 @@ GIT - remote: git://github.com/devton/catarse_mailchimp - revision: a5be917969a1df6010fbbdea25a5c1407c6f8659 - specs: - catarse_mailchimp (0.0.1) - mailchimp - supermodel - -GIT - remote: git://github.com/devton/catarse_paypal_express.git - revision: 4fd17e269395ee4b3a32528ace0bcb7eec57a953 - ref: 4fd17e269395ee4b3a32528ace0bcb7eec57a953 - specs: - catarse_paypal_express (0.1.0) - activemerchant (~> 1.17.0) - rails (~> 3.2.6) - -GIT - remote: git://github.com/moiplabs/moip-ruby.git - revision: 2b61a77355cb8c5487ff68be0eef8873696a110e + remote: git://github.com/catarse/moip-ruby.git + revision: 39f9dab38fc33fe9bf614deb8e90ca166ab2d6bf specs: moip (1.0.2) activesupport (>= 2.3.2) httparty (~> 0.6.1) - nokogiri (~> 1.4.3) - -GIT - remote: git://github.com/ncri/validation_reflection.git - revision: 60320e6beb088808fd625a8d958dbd0d2661d494 - specs: - validation_reflection (1.0.0) - -GIT - remote: git://github.com/ryanb/cancan.git - revision: f1cebde51a87be149b4970a3287826bb63c0ac0b - ref: f1cebde51a87be149b4970a3287826bb63c0ac0b - branch: 2.0 - specs: - cancan (2.0.0.alpha) + nokogiri (~> 1.5.0) PATH remote: . specs: - catarse_moip (0.1.1) - libxml-ruby (~> 2.3.3) - rails (~> 3.2.6) + catarse_moip (2.3.4) + enumerate_it + libxml-ruby (~> 2.6.0) + rails (~> 4.0) GEM remote: http://rubygems.org/ - remote: http://rubygems.org/ - remote: http://gems.github.com/ specs: - RedCloth (4.2.9) - actionmailer (3.2.11) - actionpack (= 3.2.11) - mail (~> 2.4.4) - actionpack (3.2.11) - activemodel (= 3.2.11) - activesupport (= 3.2.11) - builder (~> 3.0.0) + actionmailer (4.0.0) + actionpack (= 4.0.0) + mail (~> 2.5.3) + actionpack (4.0.0) + activesupport (= 4.0.0) + builder (~> 3.1.0) erubis (~> 2.7.0) - journey (~> 1.0.4) - rack (~> 1.4.0) - rack-cache (~> 1.2) - rack-test (~> 0.6.1) - sprockets (~> 2.2.1) - activemerchant (1.17.0) - activesupport (>= 2.3.11) - braintree (>= 2.0.0) - builder (>= 2.0.0) - json (>= 1.5.1) - activemodel (3.2.11) - activesupport (= 3.2.11) - builder (~> 3.0.0) - activerecord (3.2.11) - activemodel (= 3.2.11) - activesupport (= 3.2.11) - arel (~> 3.0.2) - tzinfo (~> 0.3.29) - activeresource (3.2.11) - activemodel (= 3.2.11) - activesupport (= 3.2.11) - activesupport (3.2.11) - i18n (~> 0.6) - multi_json (~> 1.0) - addressable (2.3.2) - airbrake (3.1.2) - activesupport - builder - arel (3.0.2) - auto_html (1.4.2) - RedCloth - redcarpet - rinku - tag_helper - bcrypt-ruby (3.0.1) - best_in_place (2.0.3) - jquery-rails - rails (~> 3.1) - braintree (2.16.0) - builder (>= 2.0.0) - brcep (3.2.0) - builder (3.0.4) - capybara (1.1.2) - mime-types (>= 1.16) - nokogiri (>= 1.3.3) - rack (>= 1.0.0) - rack-test (>= 0.5.4) - selenium-webdriver (~> 2.0) - xpath (~> 0.1.4) - carrierwave (0.7.1) - activemodel (>= 3.2.0) - activesupport (>= 3.2.0) - celluloid (0.12.3) - facter (>= 1.6.12) - timers (>= 1.0.0) - childprocess (0.3.5) - ffi (~> 1.0, >= 1.0.6) - chunky_png (1.2.6) - coffee-rails (3.2.2) - coffee-script (>= 2.2.0) - railties (~> 3.2.0) - coffee-script (2.2.0) - coffee-script-source - execjs - coffee-script-source (1.3.3) - compass (0.12.2) - chunky_png (~> 1.2) - fssm (>= 0.2.7) - sass (~> 3.1) - compass-960-plugin (0.10.4) - compass (>= 0.10.0) - compass-rails (1.0.3) - compass (>= 0.12.2, < 0.14) - connection_pool (0.9.2) + rack (~> 1.5.2) + rack-test (~> 0.6.2) + activemodel (4.0.0) + activesupport (= 4.0.0) + builder (~> 3.1.0) + activerecord (4.0.0) + activemodel (= 4.0.0) + activerecord-deprecated_finders (~> 1.0.2) + activesupport (= 4.0.0) + arel (~> 4.0.0) + activerecord-deprecated_finders (1.0.3) + activesupport (4.0.0) + i18n (~> 0.6, >= 0.6.4) + minitest (~> 4.2) + multi_json (~> 1.3) + thread_safe (~> 0.1) + tzinfo (~> 0.3.37) + arel (4.0.1) + atomic (1.1.14) + builder (3.1.4) crack (0.1.8) - curb (0.7.18) - daemons (1.1.9) - database_cleaner (0.8.0) - devise (1.5.3) - bcrypt-ruby (~> 3.0) - orm_adapter (~> 0.0.3) - warden (~> 1.1) - diff-lcs (1.1.3) - draper (0.17.0) - actionpack (~> 3.2) - activesupport (~> 3.2) - enumerate_it (0.7.17) + database_cleaner (1.1.1) + diff-lcs (1.2.4) + enumerate_it (1.2.0) activesupport (>= 3.0.0) erubis (2.7.0) - eventmachine (0.12.10) - excon (0.6.6) - execjs (1.4.0) - multi_json (~> 1.0) - facter (1.6.16) - factory_girl (2.6.4) - activesupport (>= 2.3.9) - factory_girl_rails (1.7.0) - factory_girl (~> 2.6.0) + factory_girl (4.2.0) + activesupport (>= 3.0.0) + factory_girl_rails (4.2.1) + factory_girl (~> 4.2.0) railties (>= 3.0.0) - faraday (0.8.4) - multipart-post (~> 1.1) - feedzirra (0.0.31) - activesupport (>= 3.0.8) - builder (~> 3.0.0) - curb (~> 0.7.15) - i18n (>= 0.5.0) - loofah (~> 1.0.0) - nokogiri (~> 1.4.4) - rake (>= 0.9.2) - rdoc (~> 3.8) - sax-machine (~> 0.0.20) - ffi (1.1.5) - fog (0.9.0) - builder - excon (~> 0.6.1) - formatador (>= 0.1.3) - json - mime-types - net-scp (>= 1.0.4) - net-ssh (>= 2.1.4) - nokogiri (>= 1.4.4) - ruby-hmac - foreman (0.60.2) - thor (>= 0.13.6) - formatador (0.2.3) - formtastic (2.1.1) - actionpack (~> 3.0) - fssm (0.2.9) - haml (3.1.7) - has_scope (0.5.1) - has_vimeo_video (0.0.5) - supermodel - vimeo - hashie (1.2.0) - hike (1.2.1) - http_accept_language (1.0.2) + hike (1.2.3) httparty (0.6.1) crack (= 0.1.8) - httpauth (0.1) - httpclient (2.2.5) - i18n (0.6.1) - inherited_resources (1.3.1) - has_scope (~> 0.5.0) - responders (~> 0.6) - initjs (0.1.2) - rails (~> 3.1) - journey (1.0.4) - jquery-rails (2.1.1) - railties (>= 3.1.0, < 5.0) - thor (~> 0.14) - json (1.7.6) - json_pure (1.7.5) - jwt (0.1.5) - multi_json (>= 1.0) - kaminari (0.14.0) - actionpack (>= 3.0.0) - activesupport (>= 3.0.0) - launchy (2.1.2) - addressable (~> 2.3) - libwebsocket (0.1.5) - addressable - libxml-ruby (2.3.3) - loofah (1.0.0) - nokogiri (>= 1.3.3) - mail (2.4.4) - i18n (>= 0.4.0) + i18n (0.6.5) + libxml-ruby (2.6.0) + mail (2.5.4) mime-types (~> 1.16) treetop (~> 1.4.8) - mailcatcher (0.5.8) - activesupport (~> 3.0) - eventmachine (~> 0.12) - haml (~> 3.1) - mail (~> 2.3) - sinatra (~> 1.2) - skinny (~> 0.2, >= 0.2.1) - sqlite3 (~> 1.3) - thin (~> 1.2) - mailchimp (0.0.7.alpha) - httparty - memoist (0.2.0) - meta_search (1.1.3) - actionpack (~> 3.1) - activerecord (~> 3.1) - activesupport (~> 3.1) - polyamorous (~> 0.5.0) - metaclass (0.0.1) - mime-types (1.20.1) - mocha (0.10.4) - metaclass (~> 0.0.1) - multi_json (1.5.0) - multipart-post (1.1.5) - net-scp (1.0.4) - net-ssh (>= 1.99.1) - net-ssh (2.5.2) - nokogiri (1.4.7) - oauth (0.4.6) - oauth2 (0.8.0) - faraday (~> 0.8) - httpauth (~> 0.1) - jwt (~> 0.1.4) - multi_json (~> 1.0) - rack (~> 1.2) - omniauth (1.1.1) - hashie (~> 1.2) - rack - omniauth-facebook (1.2.0) - omniauth-oauth2 (~> 1.0.0) - omniauth-github (1.0.1) - omniauth (~> 1.0) - omniauth-oauth2 (~> 1.0) - omniauth-linkedin (0.0.8) - omniauth-oauth (~> 1.0) - omniauth-oauth (1.0.1) - oauth - omniauth (~> 1.0) - omniauth-oauth2 (1.0.3) - oauth2 (~> 0.8.0) - omniauth (~> 1.0) - omniauth-openid (1.0.1) - omniauth (~> 1.0) - rack-openid (~> 1.3.1) - omniauth-twitter (0.0.12) - multi_json (~> 1.3) - omniauth-oauth (~> 1.0) - omniauth-yahoo (0.0.4) - omniauth-oauth (~> 1.0) - on_the_spot (1.0.1) - json_pure (>= 1.4.6) - orm_adapter (0.0.7) - pg (0.14.1) - pg_search (0.5.7) - activerecord (>= 3) - activesupport (>= 3) - polyamorous (0.5.0) - activerecord (~> 3.0) + mime-types (1.25.1) + minitest (4.7.5) + multi_json (1.8.0) + nokogiri (1.5.10) + pg (0.17.0) polyglot (0.3.3) - postgres-copy (0.5.7) - activerecord (>= 3.0.0) - pg - rails (>= 3.0.0) - responders - rack (1.4.4) - rack-cache (1.2) - rack (>= 0.4) - rack-openid (1.3.1) - rack (>= 1.1.0) - ruby-openid (>= 2.1.8) - rack-protection (1.2.0) - rack - rack-ssl (1.3.3) - rack + rack (1.5.2) rack-test (0.6.2) rack (>= 1.0) - rack-timeout (0.0.3) - rails (3.2.11) - actionmailer (= 3.2.11) - actionpack (= 3.2.11) - activerecord (= 3.2.11) - activeresource (= 3.2.11) - activesupport (= 3.2.11) - bundler (~> 1.0) - railties (= 3.2.11) - rails_autolink (1.0.9) - rails (~> 3.1) - railties (3.2.11) - actionpack (= 3.2.11) - activesupport (= 3.2.11) - rack-ssl (~> 1.3.2) + rails (4.0.0) + actionmailer (= 4.0.0) + actionpack (= 4.0.0) + activerecord (= 4.0.0) + activesupport (= 4.0.0) + bundler (>= 1.3.0, < 2.0) + railties (= 4.0.0) + sprockets-rails (~> 2.0.0) + railties (4.0.0) + actionpack (= 4.0.0) + activesupport (= 4.0.0) rake (>= 0.8.7) - rdoc (~> 3.4) - thor (>= 0.14.6, < 2.0) - rake (10.0.3) - rdoc (3.12) - json (~> 1.4) - redcarpet (2.1.1) - redis (3.0.2) - redis-namespace (1.2.1) - redis (~> 3.0.0) - responders (0.9.2) - railties (~> 3.1) - rinku (1.7.0) - rmagick (2.13.1) - routing-filter (0.3.1) - actionpack - rspec (2.10.0) - rspec-core (~> 2.10.0) - rspec-expectations (~> 2.10.0) - rspec-mocks (~> 2.10.0) - rspec-core (2.10.1) - rspec-expectations (2.10.0) - diff-lcs (~> 1.1.3) - rspec-mocks (2.10.1) - rspec-rails (2.10.1) + thor (>= 0.18.1, < 2.0) + rake (10.1.0) + rspec-core (2.14.5) + rspec-expectations (2.14.2) + diff-lcs (>= 1.1.3, < 2.0) + rspec-mocks (2.14.3) + rspec-rails (2.14.0) actionpack (>= 3.0) activesupport (>= 3.0) railties (>= 3.0) - rspec (~> 2.10.0) - ruby-hmac (0.4.0) - ruby-openid (2.2.0) - rubyzip (0.9.9) - sass (3.2.1) - sass-rails (3.2.5) - railties (~> 3.2.0) - sass (>= 3.1.10) - tilt (~> 1.3) - sax-machine (0.0.20) - nokogiri (> 0.0.0) - schema_plus (1.0.1) - rails (>= 3.2) - valuable - selenium-webdriver (2.25.0) - childprocess (>= 0.2.5) - libwebsocket (~> 0.1.3) - multi_json (~> 1.0) - rubyzip - shoulda (3.1.1) - shoulda-context (~> 1.0) - shoulda-matchers (~> 1.2) - shoulda-context (1.0.0) - shoulda-matchers (1.3.0) - activesupport (>= 3.0.0) - sidekiq (2.4.0) - celluloid (~> 0.12.0) - connection_pool (~> 0.9.2) - multi_json (~> 1) - redis (~> 3) - redis-namespace - sinatra (1.3.3) - rack (~> 1.3, >= 1.3.6) - rack-protection (~> 1.2) - tilt (~> 1.3, >= 1.3.3) - skinny (0.2.1) - eventmachine (~> 0.12) - thin (~> 1.2) - slim (1.2.2) - temple (~> 0.4.0) - tilt (~> 1.3.3) - slim-rails (1.0.3) - actionpack (~> 3.0) - activesupport (~> 3.0) - railties (~> 3.0) - slim (~> 1.0) - spectator-validates_email (0.1.1) - actionpack (>= 3.0.0) - activemodel (>= 3.0.0) - sprockets (2.2.2) + rspec-core (~> 2.14.0) + rspec-expectations (~> 2.14.0) + rspec-mocks (~> 2.14.0) + sprockets (2.10.1) hike (~> 1.2) multi_json (~> 1.0) rack (~> 1.0) tilt (~> 1.1, != 1.3.0) - sqlite3 (1.3.6) - state_machine (1.1.2) - supermodel (0.1.4) - activemodel (>= 3.0.0.beta) - tag_helper (0.0.3) - temple (0.4.0) - thin (1.4.1) - daemons (>= 1.0.9) - eventmachine (>= 0.12.6) - rack (>= 1.0.0) - thor (0.17.0) - tilt (1.3.3) - timers (1.0.1) - treetop (1.4.12) + sprockets-rails (2.0.1) + actionpack (>= 3.0) + activesupport (>= 3.0) + sprockets (~> 2.8) + thor (0.18.1) + thread_safe (0.1.3) + atomic + tilt (1.4.1) + treetop (1.4.15) polyglot polyglot (>= 0.3.1) - tzinfo (0.3.35) - uglifier (1.3.0) - execjs (>= 0.3.0) - multi_json (~> 1.0, >= 1.0.2) - unicode (0.4.3) - valuable (0.9.6) - vimeo (1.5.3) - httparty (>= 0.4.5) - httpclient (>= 2.1.5.2) - json (>= 1.1.9) - multipart-post (>= 1.0.1) - oauth (>= 0.4.3) - warden (1.2.1) - rack (>= 1.0) - weekdays (1.0.2) - wirble (0.1.3) - xpath (0.1.4) - nokogiri (~> 1.3) + tzinfo (0.3.37) PLATFORMS ruby DEPENDENCIES - RedCloth - activemerchant (= 1.17.0) - airbrake - auto_html (= 1.4.2) - best_in_place - brcep - cancan! - capybara (>= 1.0.1) - carrierwave (~> 0.7.0) - catarse_mailchimp! catarse_moip! - catarse_paypal_express! - coffee-rails (~> 3.2.2) - compass-960-plugin (~> 0.10.4) - compass-rails (~> 1.0.2) database_cleaner - devise (= 1.5.3) - draper - enumerate_it - factory_girl_rails (= 1.7.0) - feedzirra - fog - foreman - formtastic (~> 2.1.1) - has_scope - has_vimeo_video (~> 0.0.5) - http_accept_language - httparty (~> 0.6.1) - httpclient (= 2.2.5) - inherited_resources (= 1.3.1) - initjs - jquery-rails - kaminari - launchy - mailcatcher - memoist (~> 0.2.0) - meta_search (= 1.1.3) - mocha (= 0.10.4) + factory_girl_rails moip! - omniauth (~> 1.1.0) - omniauth-facebook (~> 1.2.0) - omniauth-github (~> 1.0.1) - omniauth-linkedin (~> 0.0.6) - omniauth-openid (~> 1.0.1) - omniauth-twitter (~> 0.0.12) - omniauth-yahoo (~> 0.0.4) - on_the_spot pg - pg_search - postgres-copy - rack-timeout - rails (= 3.2.11) - rails_autolink (~> 1.0.7) - rmagick - routing-filter - rspec-rails (~> 2.10.0) - sass-rails (~> 3.2.5) - schema_plus - selenium-webdriver - shoulda - sidekiq (= 2.4.0) - sinatra - slim - slim-rails - spectator-validates_email - state_machine - thin - uglifier (>= 1.0.3) - unicode - validation_reflection! - weekdays - wirble + rspec-rails (~> 2.14.0) diff --git a/README.md b/README.md index f87991c..e597092 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# CatarseMoip +# CatarseMoip [![Build Status](https://travis-ci.org/catarse/catarse_moip.png)](https://travis-ci.org/catarse/catarse_moip) -Catarse moip integration with [Catarse](http://github.com/danielweinmann/catarse) crowdfunding platform +Catarse moip integration with [Catarse](http://github.com/catarse/catarse) crowdfunding platform ## Installation @@ -18,6 +18,12 @@ Configure the routes for your Catarse application. Add the following lines in th mount CatarseMoip::Engine => "/", :as => "catarse_moip" +## Rails 3.2.x and Rails 4 support + +If you are using the Rails 3.2.x on Catarse's code, you can use the version `1.0.0`. + +For Rails 4 support use the `2.0.0` version. + ### Configurations Create this configurations into Catarse database: @@ -30,30 +36,6 @@ In Rails console, run this: Configuration.create!(name: "moip_token", value: "TOKEN") Configuration.create!(name: "moip_key", value: "KEY") -## Development environment setup - -Clone the repository: - - $ git clone git://github.com/devton/catarse_moip.git - -Add the catarse code into test/dummy: - - $ git submodule add git://github.com/danielweinmann/catarse.git test/dummy - -Copy the Catarse's gems to Gemfile: - - $ cat test/dummy/Gemfile >> Gemfile - -And then execute: - - $ bundle - -## Troubleshooting in development environment - -Remove the admin folder from test/dummy application to prevent a weird active admin bug: - - $ rm -rf test/dummy/app/admin - ## Contributing 1. Fork it diff --git a/Rakefile b/Rakefile index 37f9b2d..6631f5b 100644 --- a/Rakefile +++ b/Rakefile @@ -1,6 +1,7 @@ #!/usr/bin/env rake begin require 'bundler/setup' + require 'rspec/core/rake_task' rescue LoadError puts 'You must `gem install bundler` and `bundle install` to run rake tasks' end @@ -20,8 +21,12 @@ RDoc::Task.new(:rdoc) do |rdoc| rdoc.rdoc_files.include('lib/**/*.rb') end +Bundler::GemHelper.install_tasks +RSpec::Core::RakeTask.new(:spec) +task :jasmine do + sh "jasmine-node spec/javascripts" +end - -Bundler::GemHelper.install_tasks +task default: [:spec, :jasmine] diff --git a/app/assets/javascripts/catarse_moip.js b/app/assets/javascripts/catarse_moip.js index 50726e8..a563a3a 100644 --- a/app/assets/javascripts/catarse_moip.js +++ b/app/assets/javascripts/catarse_moip.js @@ -1,6 +1,7 @@ +//= require ./catarse_moip/moip_form //= require ./catarse_moip/user_document //= require_tree ./catarse_moip $(function(){ - var view = window.moipForm = new CATARSE.MoipForm(); + app.createViewGetters(); }); diff --git a/app/assets/javascripts/catarse_moip/moip_form.js b/app/assets/javascripts/catarse_moip/moip_form.js index 2f98e7d..90b609e 100644 --- a/app/assets/javascripts/catarse_moip/moip_form.js +++ b/app/assets/javascripts/catarse_moip/moip_form.js @@ -1,21 +1,20 @@ -CATARSE.MoipForm = Backbone.View.extend({ +App.addChild('MoipForm', { el: 'form.moip', getMoipToken: function(onSuccess){ var that = this; - //$('#MoipWidget').remove(); if($('#MoipWidget').length > 0) { if(_.isFunction(onSuccess)){ onSuccess(); } } else { - $.post('/payment/moip/' + this.backerId + '/get_moip_token').success(function(response, textStatus){ + $.post('/payment/moip/' + this.contributionId + '/get_moip_token').success(function(response, textStatus){ that.paymentChoice.$('input').attr('disabled', 'disabled'); if(response.get_token_response.status == 'fail'){ that.checkoutFailure({Code: 0, Mensagem: response.get_token_response.message}); } else{ - $('#catarse_moip_form').prepend(response.widget_tag); + that.createMoipWidget(response); if(_.isFunction(onSuccess)){ onSuccess(response); } @@ -24,6 +23,17 @@ CATARSE.MoipForm = Backbone.View.extend({ } }, + createMoipWidget: function(data) { + widget_tag = $("
").attr({ + id: data.widget_tag.tag_id, + 'data-token': data.widget_tag.token, + 'callback-method-success': data.widget_tag.callback_success, + 'callback-method-error': data.widget_tag.callback_error, + }); + + $("#catarse_moip_form").prepend(widget_tag); + }, + checkoutFailure: function(data) { this.loader.hide(); var response_data = (data.length > 0 ? data[0] : data); @@ -37,7 +47,7 @@ CATARSE.MoipForm = Backbone.View.extend({ checkoutSuccessful: function(data) { var that = this; - $.post('/payment/moip/' + this.backerId + '/moip_response', {response: data}).success(function(){ + $.post('/payment/moip/' + this.contributionId + '/moip_response', {response: data}).success(function(){ that.loader.hide(); // Bail out when get an error from MoIP if(data.Status == 'Cancelado'){ @@ -45,15 +55,14 @@ CATARSE.MoipForm = Backbone.View.extend({ } // Go on otherwise - if(data.url) { + if(data.url && $('#payment_type_cards_section').css('display') != 'block') { var link = $(''+data.url+'') link.attr('href', data.url); $('.link_content:visible').empty().html(link); $('.payment_section:visible .subtitle').fadeIn('fast'); } - - var thank_you = $('#project_review').data('thank-you-path'); - if($('#payment_type_cards_section').css('display') == 'block') { + else { + var thank_you = $('#project_review').data('thank-you-path'); if(thank_you){ location.href = thank_you; } @@ -64,17 +73,13 @@ CATARSE.MoipForm = Backbone.View.extend({ }); }, - initialize: function(){ + activate: function(){ this.message = this.$('.next_step_after_valid_document .alert-danger'); - this.backerId = $('input#backer_id').val(); + this.contributionId = $('input#contribution_id').val(); this.projectId = $('input#project_id').val(); this.loader = this.$('.loader'); - this.paymentChoice = new CATARSE.PaymentChoice(); - this.paymentCard = new CATARSE.PaymentCard({moipForm: this}); - this.paymentSlip = new CATARSE.PaymentSlip({moipForm: this}); - this.paymentAccount = new CATARSE.PaymentAccount({moipForm: this}); window.checkoutSuccessful = _.bind(this.checkoutSuccessful, this); window.checkoutFailure = _.bind(this.checkoutFailure, this); } diff --git a/app/assets/javascripts/catarse_moip/payment_account.js b/app/assets/javascripts/catarse_moip/payment_account.js index 322a471..ec70d38 100644 --- a/app/assets/javascripts/catarse_moip/payment_account.js +++ b/app/assets/javascripts/catarse_moip/payment_account.js @@ -1,14 +1,15 @@ -CATARSE.PaymentAccount = CATARSE.UserDocument.extend({ +App.views.MoipForm.addChild('PaymentAccount', _.extend({ el: '#payment_type_account_section', events: { 'change select#account' : 'onChangeAccount', 'click input#build_account_link' : 'onBuildAccountClick', - 'keyup #user_document_account' : 'onUserDocumentKeyup' + 'keyup #user_document_account' : 'onUserDocumentKeyup', + 'click .link_content a' : 'onContentClick', }, - initialize: function(options){ - this.moipForm = options.moipForm; + activate: function(){ + this.moipForm = this.parent; this.$('input#user_document_account').mask("999.999.999-99"); }, @@ -32,5 +33,5 @@ CATARSE.PaymentAccount = CATARSE.UserDocument.extend({ MoipWidget(settings); }); } -}); +}, App.views.MoipForm.UserDocument)); diff --git a/app/assets/javascripts/catarse_moip/payment_card.js b/app/assets/javascripts/catarse_moip/payment_card.js index 64111e3..b24f91b 100644 --- a/app/assets/javascripts/catarse_moip/payment_card.js +++ b/app/assets/javascripts/catarse_moip/payment_card.js @@ -1,4 +1,4 @@ -CATARSE.PaymentCard = CATARSE.UserDocument.extend({ +App.views.MoipForm.addChild('PaymentCard', _.extend({ el: '#payment_type_cards_section', events: { @@ -8,9 +8,9 @@ CATARSE.PaymentCard = CATARSE.UserDocument.extend({ 'keyup #payment_card_cpf' : 'onUserDocumentKeyup' }, - initialize: function(options){ + activate: function(options){ // Set credit card fields masks - this.moipForm = options.moipForm; + this.moipForm = this.parent; this.$('input#payment_card_date').mask('99/99'); this.$('input#payment_card_birth').mask('99/99/9999'); this.$('input#payment_card_cpf').mask("999.999.999-99"); @@ -86,5 +86,4 @@ CATARSE.PaymentCard = CATARSE.UserDocument.extend({ } return 'Desconhecido'; } -}); - +}, App.views.MoipForm.UserDocument)); diff --git a/app/assets/javascripts/catarse_moip/payment_choice.js b/app/assets/javascripts/catarse_moip/payment_choice.js index be29648..953b13b 100644 --- a/app/assets/javascripts/catarse_moip/payment_choice.js +++ b/app/assets/javascripts/catarse_moip/payment_choice.js @@ -1,4 +1,4 @@ -CATARSE.PaymentChoice = Backbone.View.extend({ +App.views.MoipForm.addChild('PaymentChoice', { el: '.list_payment', events: { @@ -12,7 +12,7 @@ CATARSE.PaymentChoice = Backbone.View.extend({ }); }, - initialize: function(){ + activate: function(){ this.$('input#payment_type_cards').click(); } }); diff --git a/app/assets/javascripts/catarse_moip/payment_slip.js b/app/assets/javascripts/catarse_moip/payment_slip.js index 9e9d991..2fbb331 100644 --- a/app/assets/javascripts/catarse_moip/payment_slip.js +++ b/app/assets/javascripts/catarse_moip/payment_slip.js @@ -1,4 +1,4 @@ -CATARSE.PaymentSlip = CATARSE.UserDocument.extend({ +App.views.MoipForm.addChild('PaymentSlip', _.extend({ el: '#payment_type_boleto_section', events: { @@ -13,8 +13,8 @@ CATARSE.PaymentSlip = CATARSE.UserDocument.extend({ $('input#build_boleto').attr('disabled', !$documentField.hasClass('ok')); }, - initialize: function(options){ - this.moipForm = options.moipForm; + activate: function(options){ + this.moipForm = this.parent; this.$('input#user_document_payment_slip').mask("999.999.999-99"); }, @@ -31,19 +31,5 @@ CATARSE.PaymentSlip = CATARSE.UserDocument.extend({ } MoipWidget(settings); }); - }, - - onContentClick: function(e){ - window.setTimeout(function(){ - location.href="/thank_you"; - var thank_you = $('#project_review').data('thank-you-path'); - if(thank_you){ - location.href = thank_you; - } - else { - location.href = '/'; - } - }, 1000); } -}); - +}, App.views.MoipForm.UserDocument)); diff --git a/app/assets/javascripts/catarse_moip/user_document.js b/app/assets/javascripts/catarse_moip/user_document.js index 5d51c85..d9cf2a9 100644 --- a/app/assets/javascripts/catarse_moip/user_document.js +++ b/app/assets/javascripts/catarse_moip/user_document.js @@ -1,4 +1,10 @@ -CATARSE.UserDocument = Backbone.View.extend({ +App.views.MoipForm.UserDocument = { + onContentClick: function(e){ + window.setTimeout(function(){ + app.moipForm.checkoutSuccessful({'StatusPagamento': 'Success'}); + }, 2000); + }, + onUserDocumentKeyup: function(e){ var $documentField = $(e.currentTarget); @@ -17,8 +23,10 @@ CATARSE.UserDocument = Backbone.View.extend({ if(resultCpf || resultCnpj) { $documentField.addClass('ok').removeClass('error'); - $.post('/projects/' + this.moipForm.projectId + '/backers/' + this.moipForm.backerId + '/update_info', { - backer: { payer_document: documentNumber } + $.ajax({ + url: '/projects/' + this.moipForm.projectId + '/contributions/' + this.moipForm.contributionId, + type: 'PUT', + data: { contribution: { payer_document: documentNumber } } }); } else { @@ -100,6 +108,6 @@ CATARSE.UserDocument = Backbone.View.extend({ else return false; } -}); +}; diff --git a/app/controllers/catarse_moip/moip_controller.rb b/app/controllers/catarse_moip/moip_controller.rb new file mode 100644 index 0000000..6aa2568 --- /dev/null +++ b/app/controllers/catarse_moip/moip_controller.rb @@ -0,0 +1,156 @@ +require 'enumerate_it' +require 'moip_transparente' + +module CatarseMoip + class MoipController < ApplicationController + attr_accessor :contribution + + class TransactionStatus < ::EnumerateIt::Base + associate_values( + :authorized => 1, + :started => 2, + :printed_boleto => 3, + :finished => 4, + :canceled => 5, + :process => 6, + :written_back => 7, + :refunded => 9 + ) + end + + skip_before_filter :force_http + layout :false + + def create_notification + @contribution = PaymentEngines.find_payment key: params[:id_transacao] + process_moip_message if @contribution.payment_method == 'MoIP' || @contribution.payment_method.nil? + return render :nothing => true, :status => 200 + rescue Exception => e + return render :text => "#{e.inspect}: #{e.message} recebemos: #{params}", :status => 422 + end + + def js + tries = 0 + begin + @moip = ::MoipTransparente::Checkout.new + render :text => open(@moip.get_javascript_url).set_encoding('ISO-8859-1').read.encode('utf-8') + rescue Exception => e + tries += 1 + retry unless tries > 3 + raise e + end + end + + def review + @moip = ::MoipTransparente::Checkout.new + end + + def moip_response + @contribution = PaymentEngines.find_payment id: params[:id], user_id: current_user.id + first_update_contribution unless params[:response]['StatusPagamento'] == 'Falha' + render nothing: true, status: 200 + end + + def get_moip_token + @contribution = PaymentEngines.find_payment id: params[:id], user_id: current_user.id + + ::MoipTransparente::Config.test = (PaymentEngines.configuration[:moip_test] == 'true') + ::MoipTransparente::Config.access_token = PaymentEngines.configuration[:moip_token] + ::MoipTransparente::Config.access_key = PaymentEngines.configuration[:moip_key] + + @moip = ::MoipTransparente::Checkout.new + + invoice = { + razao: "Apoio para o projeto '#{contribution.project.name}'", + id: contribution.key, + total: contribution.value.to_s, + acrescimo: '0.00', + desconto: '0.00', + cliente: { + id: contribution.user.id, + nome: contribution.payer_name, + email: contribution.payer_email, + logradouro: "#{contribution.address_street}, #{contribution.address_number}", + complemento: contribution.address_complement, + bairro: contribution.address_neighbourhood, + cidade: contribution.address_city, + uf: contribution.address_state, + cep: contribution.address_zip_code, + telefone: contribution.address_phone_number + } + } + + response = @moip.get_token(invoice) + + session[:thank_you_id] = contribution.project.id + + contribution.update_column :payment_token, response[:token] if response and response[:token] + + render json: { + get_token_response: response, + moip: @moip, + widget_tag: { + tag_id: 'MoipWidget', + token: response[:token], + callback_success: 'checkoutSuccessful', + callback_error: 'checkoutFailure' + } + } + end + + def first_update_contribution + response = ::MoIP.query(contribution.payment_token) + if response && response["Autorizacao"] + params = response["Autorizacao"]["Pagamento"] + params = params.first unless params.respond_to?(:key) + + contribution.with_lock do + if params["Status"] == "Autorizado" + contribution.confirm! + elsif contribution.pending? + contribution.waiting! + end + + contribution.update_attributes({ + :payment_id => params["CodigoMoIP"], + :payment_choice => params["FormaPagamento"], + :payment_method => 'MoIP', + :payment_service_fee => params["TaxaMoIP"] + }) if params + end + end + end + + def process_moip_message + contribution.with_lock do + first_update_contribution if contribution.payment_method.nil? + payment_notification = PaymentEngines.create_payment_notification contribution_id: contribution.id, extra_data: JSON.parse(params.to_json.force_encoding('iso-8859-1').encode('utf-8')) + payment_id = (contribution.payment_id.gsub(".", "").to_i rescue 0) + + if payment_id <= params[:cod_moip].to_i + contribution.update_attributes payment_id: params[:cod_moip] + + if (params[:valor].to_i/100.0) < contribution.value && params[:valor] + return contribution.invalid! unless contribution.invalid_payment? + end + + case params[:status_pagamento].to_i + when TransactionStatus::PROCESS + payment_notification.deliver_process_notification + when TransactionStatus::AUTHORIZED + contribution.confirm! unless contribution.confirmed? + when TransactionStatus::WRITTEN_BACK, TransactionStatus::REFUNDED + contribution.refund! unless contribution.refunded? + when TransactionStatus::CANCELED + unless contribution.canceled? + contribution.cancel! + if contribution.payment_choice.downcase == 'boletobancario' + payment_notification.deliver_slip_canceled_notification + end + end + end + end + end + end + end +end diff --git a/app/controllers/catarse_moip/payment/moip_controller.rb b/app/controllers/catarse_moip/payment/moip_controller.rb deleted file mode 100644 index 2735819..0000000 --- a/app/controllers/catarse_moip/payment/moip_controller.rb +++ /dev/null @@ -1,75 +0,0 @@ -require 'moip_transparente' -module CatarseMoip::Payment - class MoipController < ApplicationController - skip_before_filter :force_http - layout :false - - def js - @moip = ::MoipTransparente::Checkout.new - render :text => open(@moip.get_javascript_url).set_encoding('ISO-8859-1').read.encode('utf-8') - end - - def review - @moip = ::MoipTransparente::Checkout.new - end - - def moip_response - @backer = current_user.backs.find params[:id] - - @backer.payment_notifications.create(extra_data: params[:response]) - - if not @backer.confirmed and params[:response]['Status'] == 'Autorizado' - @backer.confirm! - end - - unless params[:response]['StatusPagamento'] == 'Falha' - @backer.update_attributes({ - payment_id: params[:response]['CodigoMoIP'], - payment_service_fee: params[:response]['TaxaMoIP'].to_f - }) - end - - render nothing: true, status: 200 - end - - def get_moip_token - @backer = current_user.backs.not_confirmed.find params[:id] - - ::MoipTransparente::Config.test = (::Configuration[:moip_test] == 'true') - ::MoipTransparente::Config.access_token = ::Configuration[:moip_token] - ::MoipTransparente::Config.access_key = ::Configuration[:moip_key] - - @moip = ::MoipTransparente::Checkout.new - - invoice = { - razao: "Apoio para o projeto '#{@backer.project.name}'", - id: @backer.key, - total: @backer.value.to_s, - acrescimo: '0.00', - desconto: '0.00', - cliente: { - id: @backer.user.id, - nome: @backer.payer_name, - email: @backer.payer_email, - logradouro: "#{@backer.address_street}, #{@backer.address_number}", - complemento: @backer.address_complement, - bairro: @backer.address_neighbourhood, - cidade: @backer.address_city, - uf: @backer.address_state, - cep: @backer.address_zip_code, - telefone: @backer.address_phone_number - } - } - - response = @moip.get_token(invoice) - - session[:thank_you_id] = @backer.project.id - - if response and response[:token] - @backer.update_column :payment_token, response[:token] - end - - render json: { get_token_response: response, moip: @moip, widget_tag: @moip.widget_tag('checkoutSuccessful', 'checkoutFailure'), javascript_tag: @moip.javascript_tag } - end - end -end diff --git a/app/controllers/catarse_moip/payment/notifications_controller.rb b/app/controllers/catarse_moip/payment/notifications_controller.rb deleted file mode 100644 index 0c4e260..0000000 --- a/app/controllers/catarse_moip/payment/notifications_controller.rb +++ /dev/null @@ -1,18 +0,0 @@ -require 'catarse_moip/processors/moip' - -module CatarseMoip::Payment - class NotificationsController < ApplicationController - skip_before_filter :force_http - def create - @backer = Backer.find_by_key! params[:id_transacao] - @processor = CatarseMoip::Processors::Moip.new @backer - @processor.process!(params) - return render :nothing => true, :status => 200 - rescue Exception => e - ::Airbrake.notify({ :error_class => "MoIP notification", :error_message => "MoIP notification: #{e.inspect}", :parameters => params}) rescue nil - return render :text => "#{e.inspect}: #{e.message} recebemos: #{params}", :status => 422 - end - - end -end - diff --git a/app/views/catarse_moip/payment/moip/review.html.slim b/app/views/catarse_moip/moip/review.html.slim similarity index 56% rename from app/views/catarse_moip/payment/moip/review.html.slim rename to app/views/catarse_moip/moip/review.html.slim index ea28d62..b234dac 100644 --- a/app/views/catarse_moip/payment/moip/review.html.slim +++ b/app/views/catarse_moip/moip/review.html.slim @@ -12,11 +12,12 @@ = radio_button_tag 'payment_type', "boleto" = label_tag :payment_type_boleto, 'Boleto', :class => "boleto" - = radio_button_tag 'payment_type', "account" - = label_tag :payment_type_account, 'Débito em Conta', :class => "account" + / + = radio_button_tag 'payment_type', "account" + = label_tag :payment_type_account, 'Débito em Conta', :class => "account" #payment_type_cards_section.payment_section - h3= t('projects.backers.review.form.labels.payment_card') + h3= t('projects.contributions.review.form.labels.payment_card') .clearfix @@ -29,7 +30,7 @@ .choose_card .div - span= t('projects.backers.review.form.labels.owner_card') + span= t('projects.contributions.review.form.labels.owner_card') .owner_outside .owner ol.inputs @@ -46,7 +47,7 @@ = label_tag :payment_card_birth, "Data de Nascimento" = text_field_tag :payment_card_birth, nil .div - span= t('projects.backers.review.form.labels.number_card') + span= t('projects.contributions.review.form.labels.number_card') .infocard_outside .infocard ol.inputs @@ -66,40 +67,41 @@ .clearfix .bootstrap-twitter .loader.hide= image_tag('loading.gif') - = submit_tag t('projects.backers.review.form.labels.submit'), :disabled => true, :class => 'btn btn-primary btn-large', :id => "credit_card_submit" + = submit_tag t('projects.contributions.review.form.labels.submit'), :disabled => true, :class => 'btn btn-primary btn-large', :id => "credit_card_submit" #payment_type_boleto_section.hide.payment_section - h3= t('projects.backers.review.form.labels.payment_boleto') + h3= t('projects.contributions.review.form.labels.payment_boleto') .clearfix .bootstrap-twitter style="float: none;" .alert.alert-danger.hide p.error_msg .clearfix .bootstrap-twitter - = label_tag 'user_document_payment_slip', t('projects.backers.review.form.labels.document') + = label_tag 'user_document_payment_slip', t('projects.contributions.review.form.labels.document') = text_field_tag 'user_document_payment_slip', nil, { autocomplete: 'off' } .loader.hide= image_tag('loading.gif') - = submit_tag t('projects.backers.review.form.labels.build_boleto'), :class => 'btn btn-primary btn-large', :id => "build_boleto", :disabled => true + = submit_tag t('projects.contributions.review.form.labels.build_boleto'), :class => 'btn btn-primary btn-large', :id => "build_boleto", :disabled => true .clearfix - p.subtitle.hide= t('projects.backers.review.form.labels.payment_boleto_subtitle') + p.subtitle.hide= t('projects.contributions.review.form.labels.payment_boleto_subtitle') .link_content - #payment_type_account_section.hide.payment_section - h3= t('projects.backers.review.form.labels.payment_account') - .clearfix - .bootstrap-twitter style="float: none;" - .alert.alert-danger.hide - p.error_msg - .clearfix - .bootstrap-twitter - = label_tag 'user_document_account', t('projects.backers.review.form.labels.document') - = text_field_tag 'user_document_account', nil, { autocomplete: 'off' } - = label_tag :account, t('projects.backers.review.form.labels.select_account') - = select_tag :account, options_for_select([['Banco do Brasil', 'BancoDoBrasil'], ['Bradesco'], ['Banrisul'], ['Itaú', 'Itau'] ]), :include_blank => true - .loader.hide= image_tag('loading.gif') - = submit_tag t('projects.backers.review.form.labels.submit'), :class => 'btn btn-primary btn-large', :id => "build_account_link", :disabled => true - p.subtitle.hide= t('projects.backers.review.form.labels.select_account') - .link_content + / + #payment_type_account_section.hide.payment_section + h3= t('projects.contributions.review.form.labels.payment_account') + .clearfix + .bootstrap-twitter style="float: none;" + .alert.alert-danger.hide + p.error_msg + .clearfix + .bootstrap-twitter + = label_tag 'user_document_account', t('projects.contributions.review.form.labels.document') + = text_field_tag 'user_document_account', nil, { autocomplete: 'off' } + = label_tag :account, t('projects.contributions.review.form.labels.select_account') + = select_tag :account, options_for_select([['Banco do Brasil', 'BancoDoBrasil'], ['Bradesco'], ['Banrisul'], ['Itaú', 'Itau'] ]), :include_blank => true + .loader.hide= image_tag('loading.gif') + = submit_tag t('projects.contributions.review.form.labels.submit'), :class => 'btn btn-primary btn-large', :id => "build_account_link", :disabled => true + p.subtitle.hide= t('projects.contributions.review.form.labels.select_account') + .link_content -== javascript_include_tag js_payment_moip_index_path +== javascript_include_tag js_moip_index_path diff --git a/catarse_moip.gemspec b/catarse_moip.gemspec index 9ee233c..dfb8e7c 100644 --- a/catarse_moip.gemspec +++ b/catarse_moip.gemspec @@ -8,19 +8,20 @@ require "catarse_moip/version" Gem::Specification.new do |s| s.name = "catarse_moip" s.version = CatarseMoip::VERSION - s.authors = ["Antônio Roberto Silva"] - s.email = ["forevertonny@gmail.com"] - s.homepage = "http://github.com/devton/catarse_moip" + s.authors = ["Antônio Roberto Silva", "Diogo Biazus", "Josemar Davi Luedke"] + s.email = ["forevertonny@gmail.com", "diogob@gmail.com", "josemarluedke@gmail.com"] + s.homepage = "http://github.com/catarse/catarse_moip" s.summary = "MoIP integration with Catarse" s.description = "MoIP integration with Catarse crowdfunding platform" s.files = `git ls-files`.split($\) s.test_files = s.files.grep(%r{^(test|spec|features)/}) - s.add_dependency "rails", "~> 3.2.6" - s.add_dependency('libxml-ruby', '~> 2.3.3') + s.add_dependency "rails", "~> 4.0" + s.add_dependency('libxml-ruby', '~> 2.6.0') + s.add_dependency "enumerate_it" - s.add_development_dependency "rspec-rails" + s.add_development_dependency "rspec-rails", "~> 2.14.0" s.add_development_dependency "factory_girl_rails" s.add_development_dependency "database_cleaner" end diff --git a/config/initializers/moip.rb b/config/initializers/moip.rb index cdd1c26..775e968 100644 --- a/config/initializers/moip.rb +++ b/config/initializers/moip.rb @@ -1,8 +1,5 @@ ::MoIP.setup do |config| - if ::Configuration[:moip_uri] - config.uri = ::Configuration[:moip_uri] - end - - config.token = ::Configuration[:moip_token] or '' - config.key = ::Configuration[:moip_key] or '' + config.uri = (PaymentEngines.configuration[:moip_uri] rescue nil) || '' + config.token = (PaymentEngines.configuration[:moip_token] rescue nil) || '' + config.key = (PaymentEngines.configuration[:moip_key] rescue nil) || '' end diff --git a/config/initializers/register.rb b/config/initializers/register.rb index e70657e..5bbfb1a 100644 --- a/config/initializers/register.rb +++ b/config/initializers/register.rb @@ -1 +1,5 @@ -PaymentEngines.register({name: 'moip', review_path: ->(backer){ CatarseMoip::Engine.routes.url_helpers.review_payment_moip_path(backer) }, locale: 'pt'}) +begin + PaymentEngines.register(CatarseMoip::PaymentEngine.new) +rescue Exception => e + puts "Error while registering payment engine: #{e}" +end diff --git a/config/locales/en.yml b/config/locales/en.yml index 390f2e1..816ffe3 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,6 +1,6 @@ en: projects: - backers: + contributions: review: moip: 'Payments from Brazil' checkout: diff --git a/config/locales/pt.yml b/config/locales/pt.yml index a798711..18e090f 100644 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -1,6 +1,6 @@ pt: projects: - backers: + contributions: review: moip: 'Pagamento nacional' form: diff --git a/config/routes.rb b/config/routes.rb index 4211b2f..3c012dc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,15 +1,13 @@ CatarseMoip::Engine.routes.draw do - namespace :payment do - resources :moip, only: [] do - collection do - post 'notifications' => "notifications#create" - get 'js' - end - member do - match :moip_response - match :review - match :get_moip_token - end + resources :moip, only: [], path: 'payment/moip' do + collection do + post 'notifications' => "moip#create_notification" + get 'js' + end + member do + post :moip_response + get :review + post :get_moip_token end end end diff --git a/lib/catarse_moip.rb b/lib/catarse_moip.rb index 9a9aed6..717fd3f 100644 --- a/lib/catarse_moip.rb +++ b/lib/catarse_moip.rb @@ -1,6 +1,5 @@ require "catarse_moip/engine" -require "catarse_moip/checkout/config" -require "catarse_moip/checkout/checkout" +require "catarse_moip/payment_engine" #require "moip" module CatarseMoip diff --git a/lib/catarse_moip/checkout/checkout.rb b/lib/catarse_moip/checkout/checkout.rb deleted file mode 100644 index 6a6b79e..0000000 --- a/lib/catarse_moip/checkout/checkout.rb +++ /dev/null @@ -1,220 +0,0 @@ -require 'net/https' -require 'uri' -require 'xml' -require 'base64' - -module CatarseMoip - module Checkout - class Checkout - - def javascript_tag - "" - end - - def widget_tag(success, fail) - "
-
" - end - - def get_token(invoice) - doc = XML::Document.new - doc.root = XML::Node.new('EnviarInstrucao') - - unica = XML::Node.new('InstrucaoUnica') - unica['TipoValidacao'] = 'Transparente' - - - description = XML::Node.new('Razao') - description << invoice[:razao] - unica << description - - id_invoice = XML::Node.new('IdProprio') - id_invoice << invoice[:id] - unica << id_invoice - - valores = XML::Node.new('Valores') - - valor = XML::Node.new('Valor') - valor['moeda'] = 'BRL' - valor << invoice[:total] - - valores << valor - - - valor = XML::Node.new('Acrescimo') - valor['moeda'] = 'BRL' - valores << invoice[:acrescimo] || '0.00' - - - valor = XML::Node.new('Deducao') - valor['moeda'] = 'BRL' - valores << invoice[:desconto] || '0.00' - - unica << valores - - pagador = XML::Node.new('Pagador') - nome = XML::Node.new('Nome') - nome << invoice[:cliente][:nome] - pagador << nome - - email = XML::Node.new('Email') - email << invoice[:cliente][:email] - pagador << email - - id = XML::Node.new('IdPagador') - id << invoice[:cliente][:id] - pagador << id - - endereco = XML::Node.new('EnderecoCobranca') - - logradouro = XML::Node.new('Logradouro') - logradouro << invoice[:cliente][:logradouro] - endereco << logradouro - - numero = XML::Node.new('Numero') - numero_or_default = invoice[:cliente][:numero] || '0' - numero << numero_or_default - endereco << numero - - complemento = XML::Node.new('Complemento') - complemento << invoice[:cliente][:complemento] - endereco << complemento - - bairro = XML::Node.new('Bairro') - bairro << invoice[:cliente][:bairro] - endereco << bairro - - cidade = XML::Node.new('Cidade') - cidade << invoice[:cliente][:cidade] - endereco << cidade - - estado = XML::Node.new('Estado') - estado << invoice[:cliente][:uf] - endereco << estado - - pais = XML::Node.new('Pais') - pais_or_default = invoice[:cliente][:pais] || 'BRA' - pais << pais_or_default - endereco << pais - - cep = XML::Node.new('CEP') - cep << invoice[:cliente][:cep] - endereco << cep - - telefone = XML::Node.new('TelefoneFixo') - telefone << invoice[:cliente][:telefone] - endereco << telefone - - pagador << endereco - unica << pagador - - if invoice[:parcelamentos] - parcelamentos = XML::Node.new('Parcelamentos') - invoice[:parcelamentos].each do |parcelamento_item| - parcelamento = XML::Node.new('Parcelamento') - minimo = XML::Node.new('MinimoParcelas') - minimo << parcelamento_item[:minimo] - parcelamento << minimo - - maximo = XML::Node.new('MaximoParcelas') - maximo << parcelamento_item[:maximo] - parcelamento << maximo - - if parcelamento_item[:repassar] - repassar = XML::Node.new('Repassar') - repassar << '1' - parcelamento << repassar - else - juros = XML::Node.new('Juros') - juros << parcelamento_item[:juros] - parcelamento << juros - end - parcelamentos << parcelamento - end - - unica << parcelamentos - end - - comissoes = XML::Node.new('Comissoes') - if invoice[:comissoes] - invoice[:comissoes].each do |comissao_item| - comissionamento = XML::Node.new('Comissionamento') - razao = XML::Node.new('Razao') - razao << comissao_item[:razao] || invoice[:razao] - comissionamento << razao - - comissionado = XML::Node.new("Comissionado") - login_moip = XML::Node.new("LoginMoIP") - login_moip << comissao_item[:login_moip] - comissionado << login_moip - comissionamento << comissionado - - valor = XML::Node.new("ValorFixo") - valor << comissao_item[:valor] - comissionamento << valor - comissoes << comissionamento - end - end - - if invoice[:pagador_taxas] - taxas = XML::Node.new('PagadorTaxa') - login_moip = XML::Node.new("LoginMoIP") - login_moip << invoice[:pagador_taxas] - taxas << login_moip - comissoes << taxas - end - - unica << comissoes - - doc.root << unica - - parser = XML::Parser.string(post_data(doc.to_s(:encoding => XML::Encoding::ISO_8859_1))) - - dom = parser.parse - resposta = dom.find('./Resposta').first - if resposta.find('Status')[0].content == 'Sucesso' - @token = resposta.find('Token')[0].content - return {:status => :ok, :token => resposta.find('Token')[0].content, :id => resposta.find('ID')[0].content} - elsif resposta.find('Status')[0].content == 'Falha' - return {:status => :fail, :code => resposta.find('Erro')[0]['Codigo'], :message => resposta.find('Erro')[0].content, :id => resposta.find('ID')[0].content } - end - end - - private - - def get_token_url - if ::CatarseMoip::Checkout::Config.test? - return "https://desenvolvedor.moip.com.br/sandbox/ws/alpha/EnviarInstrucao/Unica" - else - return "https://www.moip.com.br/ws/alpha/EnviarInstrucao/Unica" - end - end - - def get_javascript_url - if ::CatarseMoip::Checkout::Config.test? - return "https://desenvolvedor.moip.com.br/sandbox/transparente/MoipWidget-v2.js" - else - return "https://www.moip.com.br/transparente/MoipWidget-v2.js" - end - end - - def post_data(xml) - uri = URI.parse(get_token_url) - http = Net::HTTP.new(uri.host, uri.port) - http.use_ssl = true - http.verify_mode = OpenSSL::SSL::VERIFY_NONE - #http.set_debug_output $stderr if Moip::Config.test? - - request = Net::HTTP::Post.new(uri.path) - request.basic_auth ::CatarseMoip::Checkout::Config.access_token, ::CatarseMoip::Checkout::Config.access_key - - request.body = xml - response = http.start {|r| r.request request } - response.body - end - end - end -end diff --git a/lib/catarse_moip/checkout/config.rb b/lib/catarse_moip/checkout/config.rb deleted file mode 100644 index d8ed506..0000000 --- a/lib/catarse_moip/checkout/config.rb +++ /dev/null @@ -1,29 +0,0 @@ -module CatarseMoip - module Checkout - class Config - def self.access_token - @access_token - end - - def self.access_token=(value) - @access_token = value - end - - def self.access_key - @access_key - end - - def self.access_key=(value) - @access_key = value - end - - def self.test? - @test || false - end - - def self.test=(test) - @test = test - end - end - end -end diff --git a/lib/catarse_moip/payment_engine.rb b/lib/catarse_moip/payment_engine.rb new file mode 100644 index 0000000..7b15dae --- /dev/null +++ b/lib/catarse_moip/payment_engine.rb @@ -0,0 +1,25 @@ +module CatarseMoip + class PaymentEngine + + def name + 'MoIP' + end + + def review_path contribution + CatarseMoip::Engine.routes.url_helpers.review_moip_path(contribution) + end + + def locale + 'pt' + end + + def can_do_refund? + false + end + + def direct_refund + false + end + + end +end diff --git a/lib/catarse_moip/processors/moip.rb b/lib/catarse_moip/processors/moip.rb deleted file mode 100644 index 6cf6ab4..0000000 --- a/lib/catarse_moip/processors/moip.rb +++ /dev/null @@ -1,56 +0,0 @@ -module CatarseMoip - module Processors - class Moip - #MoIP API table: - class PaymentMethods < EnumerateIt::Base - associate_values( - :DebitoBancario => 1, - :FinanciamentoBancario => 2, - :BoletoBancario => 3, - :CartaoDeCredito => 4, - :CartaoDeDebito => 5, - :CarteiraMoIP => 6, - :NaoDefinida => 7 - ) - end - - class TransactionStatus < EnumerateIt::Base - associate_values( - :authorized => 1, - :started => 2, - :printed_boleto => 3, - :finished => 4, - :canceled => 5, - :process => 6, - :written_back => 7, - :refunded => 9 - ) - end - - def initialize(backer) - @backer = backer - end - - def update_backer - pagamento = ::MoIP.query(@backer.payment_token)["Autorizacao"]["Pagamento"] - pagamento = pagamento.first unless pagamento.respond_to?(:key) - @backer.update_attributes({ - :payment_id => pagamento["CodigoMoIP"], - :payment_choice => pagamento["FormaPagamento"], - :payment_service_fee => pagamento["TaxaMoIP"] - }) - end - - def process!(params) - update_backer if @backer.payment_id.nil? - @backer.payment_notifications.create! extra_data: JSON.parse(params.to_json.force_encoding('iso-8859-1').encode('utf-8')) - case params[:status_pagamento].to_i - when TransactionStatus::AUTHORIZED - @backer.confirm! unless @backer.confirmed - when TransactionStatus::WRITTEN_BACK, TransactionStatus::REFUNDED - @backer.refund! unless @backer.refunded? - end - end - end - end -end diff --git a/lib/catarse_moip/version.rb b/lib/catarse_moip/version.rb index f3dc8bd..030c5b1 100644 --- a/lib/catarse_moip/version.rb +++ b/lib/catarse_moip/version.rb @@ -1,3 +1,3 @@ module CatarseMoip - VERSION = "0.1.1" + VERSION = "2.3.5" end diff --git a/spec/controllers/catarse_moip/moip_controller_spec.rb b/spec/controllers/catarse_moip/moip_controller_spec.rb new file mode 100644 index 0000000..2ad973b --- /dev/null +++ b/spec/controllers/catarse_moip/moip_controller_spec.rb @@ -0,0 +1,358 @@ +# encoding: utf-8 +require 'spec_helper' + +describe CatarseMoip::MoipController do + subject{ response } + + let(:get_token_response){{:status=>:fail, :code=>"171", :message=>"TelefoneFixo do endereço deverá ser enviado obrigatorio", :id=>"201210192052439150000024698931"}} + let(:contribution){ double('contribution', { + id: 1, + key: 'contribution key', + payment_id: 'payment id', + project: project, + pending?: false, + value: 10, + user: user, + payer_name: 'foo', + payer_email: 'foo@bar.com', + address_street: 'test', + address_number: '123', + address_complement: '123', + address_neighbourhood: '123', + address_city: '123', + address_state: '123', + address_zip_code: '123', + address_phone_number: '123', + confirmed?: true, + confirm!: true, + canceled?: true, + cancel!: true, + refunded?: true, + refund!: true, + payment_method: 'MoIP', + invalid!: true, + invlid_payment?: true + }) } + + let(:user){ double('user', id: 1) } + let(:project){ double('project', id: 1, name: 'test project') } + let(:extra_data){ {"id_transacao"=>contribution.key, "valor"=>"2190", "cod_moip"=>"12345123", "forma_pagamento"=>"1", "tipo_pagamento"=>"CartaoDeCredito", "email_consumidor"=>"some@email.com", "controller"=>"catarse_moip/payment/notifications", "action"=>"create"} } + let(:payment_notification) {} + + before do + controller.stub(:current_user).and_return(user) + ::MoipTransparente::Checkout.any_instance.stub(:get_token).and_return(get_token_response) + ::MoipTransparente::Checkout.any_instance.stub(:moip_widget_tag).and_return('
') + ::MoipTransparente::Checkout.any_instance.stub(:moip_javascript_tag).and_return('\"}" } - end -end diff --git a/spec/controllers/catarse_moip/payment/notifications_controller_spec.rb b/spec/controllers/catarse_moip/payment/notifications_controller_spec.rb deleted file mode 100644 index 9192d28..0000000 --- a/spec/controllers/catarse_moip/payment/notifications_controller_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -require 'spec_helper' - -describe CatarseMoip::Payment::NotificationsController do - let(:backer){ create(:backer, :value => 21.90, :confirmed => true, :refunded => false) } - let(:extra_data){ {"id_transacao"=>backer.key, "valor"=>"2190", "cod_moip"=>"12345123", "forma_pagamento"=>"1", "tipo_pagamento"=>"CartaoDeCredito", "email_consumidor"=>"some@email.com", "controller"=>"catarse_moip/payment/notifications", "action"=>"create"} } - subject{ response } - - describe "POST create" do - context "when we search for a non-existant backer" do - before do - post :create, {:id_transacao => "non-existant backer key", :use_route => 'catarse_moip'} - end - - its(:body){ should == "#: Couldn't find Backer with key = non-existant backer key recebemos: {\"id_transacao\"=>\"non-existant backer key\", \"controller\"=>\"catarse_moip/payment/notifications\", \"action\"=>\"create\"}" } - its(:status){ should == 422 } - end - - context "when we seach for an existing backer" do - before do - CatarseMoip::Processors::Moip.any_instance.should_receive(:process!).with({"id_transacao"=>backer.key, "controller"=>"catarse_moip/payment/notifications", "action"=>"create"}) - post :create, {:id_transacao => backer.key, :use_route => 'catarse_moip'} - end - - its(:body){ should == ' ' } - its(:status){ should == 200 } - it("should assign backer"){ assigns(:backer).should == backer } - it("should assing processor"){ assigns(:processor).class.should == CatarseMoip::Processors::Moip } - end - end -end diff --git a/spec/javascripts/moip_form_spec.js b/spec/javascripts/moip_form_spec.js new file mode 100644 index 0000000..3a8eb2e --- /dev/null +++ b/spec/javascripts/moip_form_spec.js @@ -0,0 +1,25 @@ +global.window = require("jsdom") + .jsdom() + .createWindow(); +global.jQuery = global.$ = require("jquery"); + +global._ = global.underscore = require("./support/underscore"); +global.Backbone = require("./support/backbone"); +Backbone.$ = $; + +global.Skull = require('./support/skull'); +require('./support/app'); +require("../../app/assets/javascripts/catarse_moip/moip_form"); + +describe("MoipForm", function() { + var view; + + beforeEach(function() { + view = new App.views.MoipForm(); + }); + + it("should be true", function() { + expect(true).toEqual(true); + }); +}); + diff --git a/spec/javascripts/support/app.js b/spec/javascripts/support/app.js new file mode 100644 index 0000000..0ac5040 --- /dev/null +++ b/spec/javascripts/support/app.js @@ -0,0 +1 @@ +global.App = window.App = Skull.View.extend({}); diff --git a/spec/javascripts/support/backbone.js b/spec/javascripts/support/backbone.js new file mode 100644 index 0000000..3512d42 --- /dev/null +++ b/spec/javascripts/support/backbone.js @@ -0,0 +1,1571 @@ +// Backbone.js 1.0.0 + +// (c) 2010-2013 Jeremy Ashkenas, DocumentCloud Inc. +// Backbone may be freely distributed under the MIT license. +// For all details and documentation: +// http://backbonejs.org + +(function(){ + + // Initial Setup + // ------------- + + // Save a reference to the global object (`window` in the browser, `exports` + // on the server). + var root = this; + + // Save the previous value of the `Backbone` variable, so that it can be + // restored later on, if `noConflict` is used. + var previousBackbone = root.Backbone; + + // Create local references to array methods we'll want to use later. + var array = []; + var push = array.push; + var slice = array.slice; + var splice = array.splice; + + // The top-level namespace. All public Backbone classes and modules will + // be attached to this. Exported for both the browser and the server. + var Backbone; + if (typeof exports !== 'undefined') { + Backbone = exports; + } else { + Backbone = root.Backbone = {}; + } + + // Current version of the library. Keep in sync with `package.json`. + Backbone.VERSION = '1.0.0'; + + // Require Underscore, if we're on the server, and it's not already present. + var _ = root._; + if (!_ && (typeof require !== 'undefined')) _ = require('underscore'); + + // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns + // the `$` variable. + Backbone.$ = root.jQuery || root.Zepto || root.ender || root.$; + + // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable + // to its previous owner. Returns a reference to this Backbone object. + Backbone.noConflict = function() { + root.Backbone = previousBackbone; + return this; + }; + + // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option + // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and + // set a `X-Http-Method-Override` header. + Backbone.emulateHTTP = false; + + // Turn on `emulateJSON` to support legacy servers that can't deal with direct + // `application/json` requests ... will encode the body as + // `application/x-www-form-urlencoded` instead and will send the model in a + // form param named `model`. + Backbone.emulateJSON = false; + + // Backbone.Events + // --------------- + + // A module that can be mixed in to *any object* in order to provide it with + // custom events. You may bind with `on` or remove with `off` callback + // functions to an event; `trigger`-ing an event fires all callbacks in + // succession. + // + // var object = {}; + // _.extend(object, Backbone.Events); + // object.on('expand', function(){ alert('expanded'); }); + // object.trigger('expand'); + // + var Events = Backbone.Events = { + + // Bind an event to a `callback` function. Passing `"all"` will bind + // the callback to all events fired. + on: function(name, callback, context) { + if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this; + this._events || (this._events = {}); + var events = this._events[name] || (this._events[name] = []); + events.push({callback: callback, context: context, ctx: context || this}); + return this; + }, + + // Bind an event to only be triggered a single time. After the first time + // the callback is invoked, it will be removed. + once: function(name, callback, context) { + if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this; + var self = this; + var once = _.once(function() { + self.off(name, once); + callback.apply(this, arguments); + }); + once._callback = callback; + return this.on(name, once, context); + }, + + // Remove one or many callbacks. If `context` is null, removes all + // callbacks with that function. If `callback` is null, removes all + // callbacks for the event. If `name` is null, removes all bound + // callbacks for all events. + off: function(name, callback, context) { + var retain, ev, events, names, i, l, j, k; + if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; + if (!name && !callback && !context) { + this._events = {}; + return this; + } + + names = name ? [name] : _.keys(this._events); + for (i = 0, l = names.length; i < l; i++) { + name = names[i]; + if (events = this._events[name]) { + this._events[name] = retain = []; + if (callback || context) { + for (j = 0, k = events.length; j < k; j++) { + ev = events[j]; + if ((callback && callback !== ev.callback && callback !== ev.callback._callback) || + (context && context !== ev.context)) { + retain.push(ev); + } + } + } + if (!retain.length) delete this._events[name]; + } + } + + return this; + }, + + // Trigger one or many events, firing all bound callbacks. Callbacks are + // passed the same arguments as `trigger` is, apart from the event name + // (unless you're listening on `"all"`, which will cause your callback to + // receive the true name of the event as the first argument). + trigger: function(name) { + if (!this._events) return this; + var args = slice.call(arguments, 1); + if (!eventsApi(this, 'trigger', name, args)) return this; + var events = this._events[name]; + var allEvents = this._events.all; + if (events) triggerEvents(events, args); + if (allEvents) triggerEvents(allEvents, arguments); + return this; + }, + + // Tell this object to stop listening to either specific events ... or + // to every object it's currently listening to. + stopListening: function(obj, name, callback) { + var listeners = this._listeners; + if (!listeners) return this; + var deleteListener = !name && !callback; + if (typeof name === 'object') callback = this; + if (obj) (listeners = {})[obj._listenerId] = obj; + for (var id in listeners) { + listeners[id].off(name, callback, this); + if (deleteListener) delete this._listeners[id]; + } + return this; + } + + }; + + // Regular expression used to split event strings. + var eventSplitter = /\s+/; + + // Implement fancy features of the Events API such as multiple event + // names `"change blur"` and jQuery-style event maps `{change: action}` + // in terms of the existing API. + var eventsApi = function(obj, action, name, rest) { + if (!name) return true; + + // Handle event maps. + if (typeof name === 'object') { + for (var key in name) { + obj[action].apply(obj, [key, name[key]].concat(rest)); + } + return false; + } + + // Handle space separated event names. + if (eventSplitter.test(name)) { + var names = name.split(eventSplitter); + for (var i = 0, l = names.length; i < l; i++) { + obj[action].apply(obj, [names[i]].concat(rest)); + } + return false; + } + + return true; + }; + + // A difficult-to-believe, but optimized internal dispatch function for + // triggering events. Tries to keep the usual cases speedy (most internal + // Backbone events have 3 arguments). + var triggerEvents = function(events, args) { + var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; + switch (args.length) { + case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; + case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; + case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; + case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; + default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); + } + }; + + var listenMethods = {listenTo: 'on', listenToOnce: 'once'}; + + // Inversion-of-control versions of `on` and `once`. Tell *this* object to + // listen to an event in another object ... keeping track of what it's + // listening to. + _.each(listenMethods, function(implementation, method) { + Events[method] = function(obj, name, callback) { + var listeners = this._listeners || (this._listeners = {}); + var id = obj._listenerId || (obj._listenerId = _.uniqueId('l')); + listeners[id] = obj; + if (typeof name === 'object') callback = this; + obj[implementation](name, callback, this); + return this; + }; + }); + + // Aliases for backwards compatibility. + Events.bind = Events.on; + Events.unbind = Events.off; + + // Allow the `Backbone` object to serve as a global event bus, for folks who + // want global "pubsub" in a convenient place. + _.extend(Backbone, Events); + + // Backbone.Model + // -------------- + + // Backbone **Models** are the basic data object in the framework -- + // frequently representing a row in a table in a database on your server. + // A discrete chunk of data and a bunch of useful, related methods for + // performing computations and transformations on that data. + + // Create a new model with the specified attributes. A client id (`cid`) + // is automatically generated and assigned for you. + var Model = Backbone.Model = function(attributes, options) { + var defaults; + var attrs = attributes || {}; + options || (options = {}); + this.cid = _.uniqueId('c'); + this.attributes = {}; + _.extend(this, _.pick(options, modelOptions)); + if (options.parse) attrs = this.parse(attrs, options) || {}; + if (defaults = _.result(this, 'defaults')) { + attrs = _.defaults({}, attrs, defaults); + } + this.set(attrs, options); + this.changed = {}; + this.initialize.apply(this, arguments); + }; + + // A list of options to be attached directly to the model, if provided. + var modelOptions = ['url', 'urlRoot', 'collection']; + + // Attach all inheritable methods to the Model prototype. + _.extend(Model.prototype, Events, { + + // A hash of attributes whose current and previous value differ. + changed: null, + + // The value returned during the last failed validation. + validationError: null, + + // The default name for the JSON `id` attribute is `"id"`. MongoDB and + // CouchDB users may want to set this to `"_id"`. + idAttribute: 'id', + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // Return a copy of the model's `attributes` object. + toJSON: function(options) { + return _.clone(this.attributes); + }, + + // Proxy `Backbone.sync` by default -- but override this if you need + // custom syncing semantics for *this* particular model. + sync: function() { + return Backbone.sync.apply(this, arguments); + }, + + // Get the value of an attribute. + get: function(attr) { + return this.attributes[attr]; + }, + + // Get the HTML-escaped value of an attribute. + escape: function(attr) { + return _.escape(this.get(attr)); + }, + + // Returns `true` if the attribute contains a value that is not null + // or undefined. + has: function(attr) { + return this.get(attr) != null; + }, + + // Set a hash of model attributes on the object, firing `"change"`. This is + // the core primitive operation of a model, updating the data and notifying + // anyone who needs to know about the change in state. The heart of the beast. + set: function(key, val, options) { + var attr, attrs, unset, changes, silent, changing, prev, current; + if (key == null) return this; + + // Handle both `"key", value` and `{key: value}` -style arguments. + if (typeof key === 'object') { + attrs = key; + options = val; + } else { + (attrs = {})[key] = val; + } + + options || (options = {}); + + // Run validation. + if (!this._validate(attrs, options)) return false; + + // Extract attributes and options. + unset = options.unset; + silent = options.silent; + changes = []; + changing = this._changing; + this._changing = true; + + if (!changing) { + this._previousAttributes = _.clone(this.attributes); + this.changed = {}; + } + current = this.attributes, prev = this._previousAttributes; + + // Check for changes of `id`. + if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; + + // For each `set` attribute, update or delete the current value. + for (attr in attrs) { + val = attrs[attr]; + if (!_.isEqual(current[attr], val)) changes.push(attr); + if (!_.isEqual(prev[attr], val)) { + this.changed[attr] = val; + } else { + delete this.changed[attr]; + } + unset ? delete current[attr] : current[attr] = val; + } + + // Trigger all relevant attribute changes. + if (!silent) { + if (changes.length) this._pending = true; + for (var i = 0, l = changes.length; i < l; i++) { + this.trigger('change:' + changes[i], this, current[changes[i]], options); + } + } + + // You might be wondering why there's a `while` loop here. Changes can + // be recursively nested within `"change"` events. + if (changing) return this; + if (!silent) { + while (this._pending) { + this._pending = false; + this.trigger('change', this, options); + } + } + this._pending = false; + this._changing = false; + return this; + }, + + // Remove an attribute from the model, firing `"change"`. `unset` is a noop + // if the attribute doesn't exist. + unset: function(attr, options) { + return this.set(attr, void 0, _.extend({}, options, {unset: true})); + }, + + // Clear all attributes on the model, firing `"change"`. + clear: function(options) { + var attrs = {}; + for (var key in this.attributes) attrs[key] = void 0; + return this.set(attrs, _.extend({}, options, {unset: true})); + }, + + // Determine if the model has changed since the last `"change"` event. + // If you specify an attribute name, determine if that attribute has changed. + hasChanged: function(attr) { + if (attr == null) return !_.isEmpty(this.changed); + return _.has(this.changed, attr); + }, + + // Return an object containing all the attributes that have changed, or + // false if there are no changed attributes. Useful for determining what + // parts of a view need to be updated and/or what attributes need to be + // persisted to the server. Unset attributes will be set to undefined. + // You can also pass an attributes object to diff against the model, + // determining if there *would be* a change. + changedAttributes: function(diff) { + if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; + var val, changed = false; + var old = this._changing ? this._previousAttributes : this.attributes; + for (var attr in diff) { + if (_.isEqual(old[attr], (val = diff[attr]))) continue; + (changed || (changed = {}))[attr] = val; + } + return changed; + }, + + // Get the previous value of an attribute, recorded at the time the last + // `"change"` event was fired. + previous: function(attr) { + if (attr == null || !this._previousAttributes) return null; + return this._previousAttributes[attr]; + }, + + // Get all of the attributes of the model at the time of the previous + // `"change"` event. + previousAttributes: function() { + return _.clone(this._previousAttributes); + }, + + // Fetch the model from the server. If the server's representation of the + // model differs from its current attributes, they will be overridden, + // triggering a `"change"` event. + fetch: function(options) { + options = options ? _.clone(options) : {}; + if (options.parse === void 0) options.parse = true; + var model = this; + var success = options.success; + options.success = function(resp) { + if (!model.set(model.parse(resp, options), options)) return false; + if (success) success(model, resp, options); + model.trigger('sync', model, resp, options); + }; + wrapError(this, options); + return this.sync('read', this, options); + }, + + // Set a hash of model attributes, and sync the model to the server. + // If the server returns an attributes hash that differs, the model's + // state will be `set` again. + save: function(key, val, options) { + var attrs, method, xhr, attributes = this.attributes; + + // Handle both `"key", value` and `{key: value}` -style arguments. + if (key == null || typeof key === 'object') { + attrs = key; + options = val; + } else { + (attrs = {})[key] = val; + } + + // If we're not waiting and attributes exist, save acts as `set(attr).save(null, opts)`. + if (attrs && (!options || !options.wait) && !this.set(attrs, options)) return false; + + options = _.extend({validate: true}, options); + + // Do not persist invalid models. + if (!this._validate(attrs, options)) return false; + + // Set temporary attributes if `{wait: true}`. + if (attrs && options.wait) { + this.attributes = _.extend({}, attributes, attrs); + } + + // After a successful server-side save, the client is (optionally) + // updated with the server-side state. + if (options.parse === void 0) options.parse = true; + var model = this; + var success = options.success; + options.success = function(resp) { + // Ensure attributes are restored during synchronous saves. + model.attributes = attributes; + var serverAttrs = model.parse(resp, options); + if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs); + if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) { + return false; + } + if (success) success(model, resp, options); + model.trigger('sync', model, resp, options); + }; + wrapError(this, options); + + method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); + if (method === 'patch') options.attrs = attrs; + xhr = this.sync(method, this, options); + + // Restore attributes. + if (attrs && options.wait) this.attributes = attributes; + + return xhr; + }, + + // Destroy this model on the server if it was already persisted. + // Optimistically removes the model from its collection, if it has one. + // If `wait: true` is passed, waits for the server to respond before removal. + destroy: function(options) { + options = options ? _.clone(options) : {}; + var model = this; + var success = options.success; + + var destroy = function() { + model.trigger('destroy', model, model.collection, options); + }; + + options.success = function(resp) { + if (options.wait || model.isNew()) destroy(); + if (success) success(model, resp, options); + if (!model.isNew()) model.trigger('sync', model, resp, options); + }; + + if (this.isNew()) { + options.success(); + return false; + } + wrapError(this, options); + + var xhr = this.sync('delete', this, options); + if (!options.wait) destroy(); + return xhr; + }, + + // Default URL for the model's representation on the server -- if you're + // using Backbone's restful methods, override this to change the endpoint + // that will be called. + url: function() { + var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError(); + if (this.isNew()) return base; + return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id); + }, + + // **parse** converts a response into the hash of attributes to be `set` on + // the model. The default implementation is just to pass the response along. + parse: function(resp, options) { + return resp; + }, + + // Create a new model with identical attributes to this one. + clone: function() { + return new this.constructor(this.attributes); + }, + + // A model is new if it has never been saved to the server, and lacks an id. + isNew: function() { + return this.id == null; + }, + + // Check if the model is currently in a valid state. + isValid: function(options) { + return this._validate({}, _.extend(options || {}, { validate: true })); + }, + + // Run validation against the next complete set of model attributes, + // returning `true` if all is well. Otherwise, fire an `"invalid"` event. + _validate: function(attrs, options) { + if (!options.validate || !this.validate) return true; + attrs = _.extend({}, this.attributes, attrs); + var error = this.validationError = this.validate(attrs, options) || null; + if (!error) return true; + this.trigger('invalid', this, error, _.extend(options || {}, {validationError: error})); + return false; + } + + }); + + // Underscore methods that we want to implement on the Model. + var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit']; + + // Mix in each Underscore method as a proxy to `Model#attributes`. + _.each(modelMethods, function(method) { + Model.prototype[method] = function() { + var args = slice.call(arguments); + args.unshift(this.attributes); + return _[method].apply(_, args); + }; + }); + + // Backbone.Collection + // ------------------- + + // If models tend to represent a single row of data, a Backbone Collection is + // more analagous to a table full of data ... or a small slice or page of that + // table, or a collection of rows that belong together for a particular reason + // -- all of the messages in this particular folder, all of the documents + // belonging to this particular author, and so on. Collections maintain + // indexes of their models, both in order, and for lookup by `id`. + + // Create a new **Collection**, perhaps to contain a specific type of `model`. + // If a `comparator` is specified, the Collection will maintain + // its models in sort order, as they're added and removed. + var Collection = Backbone.Collection = function(models, options) { + options || (options = {}); + if (options.url) this.url = options.url; + if (options.model) this.model = options.model; + if (options.comparator !== void 0) this.comparator = options.comparator; + this._reset(); + this.initialize.apply(this, arguments); + if (models) this.reset(models, _.extend({silent: true}, options)); + }; + + // Default options for `Collection#set`. + var setOptions = {add: true, remove: true, merge: true}; + var addOptions = {add: true, merge: false, remove: false}; + + // Define the Collection's inheritable methods. + _.extend(Collection.prototype, Events, { + + // The default model for a collection is just a **Backbone.Model**. + // This should be overridden in most cases. + model: Model, + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // The JSON representation of a Collection is an array of the + // models' attributes. + toJSON: function(options) { + return this.map(function(model){ return model.toJSON(options); }); + }, + + // Proxy `Backbone.sync` by default. + sync: function() { + return Backbone.sync.apply(this, arguments); + }, + + // Add a model, or list of models to the set. + add: function(models, options) { + return this.set(models, _.defaults(options || {}, addOptions)); + }, + + // Remove a model, or a list of models from the set. + remove: function(models, options) { + models = _.isArray(models) ? models.slice() : [models]; + options || (options = {}); + var i, l, index, model; + for (i = 0, l = models.length; i < l; i++) { + model = this.get(models[i]); + if (!model) continue; + delete this._byId[model.id]; + delete this._byId[model.cid]; + index = this.indexOf(model); + this.models.splice(index, 1); + this.length--; + if (!options.silent) { + options.index = index; + model.trigger('remove', model, this, options); + } + this._removeReference(model); + } + return this; + }, + + // Update a collection by `set`-ing a new list of models, adding new ones, + // removing models that are no longer present, and merging models that + // already exist in the collection, as necessary. Similar to **Model#set**, + // the core operation for updating the data contained by the collection. + set: function(models, options) { + options = _.defaults(options || {}, setOptions); + if (options.parse) models = this.parse(models, options); + if (!_.isArray(models)) models = models ? [models] : []; + var i, l, model, attrs, existing, sort; + var at = options.at; + var sortable = this.comparator && (at == null) && options.sort !== false; + var sortAttr = _.isString(this.comparator) ? this.comparator : null; + var toAdd = [], toRemove = [], modelMap = {}; + + // Turn bare objects into model references, and prevent invalid models + // from being added. + for (i = 0, l = models.length; i < l; i++) { + if (!(model = this._prepareModel(models[i], options))) continue; + + // If a duplicate is found, prevent it from being added and + // optionally merge it into the existing model. + if (existing = this.get(model)) { + if (options.remove) modelMap[existing.cid] = true; + if (options.merge) { + existing.set(model.attributes, options); + if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true; + } + + // This is a new model, push it to the `toAdd` list. + } else if (options.add) { + toAdd.push(model); + + // Listen to added models' events, and index models for lookup by + // `id` and by `cid`. + model.on('all', this._onModelEvent, this); + this._byId[model.cid] = model; + if (model.id != null) this._byId[model.id] = model; + } + } + + // Remove nonexistent models if appropriate. + if (options.remove) { + for (i = 0, l = this.length; i < l; ++i) { + if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model); + } + if (toRemove.length) this.remove(toRemove, options); + } + + // See if sorting is needed, update `length` and splice in new models. + if (toAdd.length) { + if (sortable) sort = true; + this.length += toAdd.length; + if (at != null) { + splice.apply(this.models, [at, 0].concat(toAdd)); + } else { + push.apply(this.models, toAdd); + } + } + + // Silently sort the collection if appropriate. + if (sort) this.sort({silent: true}); + + if (options.silent) return this; + + // Trigger `add` events. + for (i = 0, l = toAdd.length; i < l; i++) { + (model = toAdd[i]).trigger('add', model, this, options); + } + + // Trigger `sort` if the collection was sorted. + if (sort) this.trigger('sort', this, options); + return this; + }, + + // When you have more items than you want to add or remove individually, + // you can reset the entire set with a new list of models, without firing + // any granular `add` or `remove` events. Fires `reset` when finished. + // Useful for bulk operations and optimizations. + reset: function(models, options) { + options || (options = {}); + for (var i = 0, l = this.models.length; i < l; i++) { + this._removeReference(this.models[i]); + } + options.previousModels = this.models; + this._reset(); + this.add(models, _.extend({silent: true}, options)); + if (!options.silent) this.trigger('reset', this, options); + return this; + }, + + // Add a model to the end of the collection. + push: function(model, options) { + model = this._prepareModel(model, options); + this.add(model, _.extend({at: this.length}, options)); + return model; + }, + + // Remove a model from the end of the collection. + pop: function(options) { + var model = this.at(this.length - 1); + this.remove(model, options); + return model; + }, + + // Add a model to the beginning of the collection. + unshift: function(model, options) { + model = this._prepareModel(model, options); + this.add(model, _.extend({at: 0}, options)); + return model; + }, + + // Remove a model from the beginning of the collection. + shift: function(options) { + var model = this.at(0); + this.remove(model, options); + return model; + }, + + // Slice out a sub-array of models from the collection. + slice: function(begin, end) { + return this.models.slice(begin, end); + }, + + // Get a model from the set by id. + get: function(obj) { + if (obj == null) return void 0; + return this._byId[obj.id != null ? obj.id : obj.cid || obj]; + }, + + // Get the model at the given index. + at: function(index) { + return this.models[index]; + }, + + // Return models with matching attributes. Useful for simple cases of + // `filter`. + where: function(attrs, first) { + if (_.isEmpty(attrs)) return first ? void 0 : []; + return this[first ? 'find' : 'filter'](function(model) { + for (var key in attrs) { + if (attrs[key] !== model.get(key)) return false; + } + return true; + }); + }, + + // Return the first model with matching attributes. Useful for simple cases + // of `find`. + findWhere: function(attrs) { + return this.where(attrs, true); + }, + + // Force the collection to re-sort itself. You don't need to call this under + // normal circumstances, as the set will maintain sort order as each item + // is added. + sort: function(options) { + if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); + options || (options = {}); + + // Run sort based on type of `comparator`. + if (_.isString(this.comparator) || this.comparator.length === 1) { + this.models = this.sortBy(this.comparator, this); + } else { + this.models.sort(_.bind(this.comparator, this)); + } + + if (!options.silent) this.trigger('sort', this, options); + return this; + }, + + // Figure out the smallest index at which a model should be inserted so as + // to maintain order. + sortedIndex: function(model, value, context) { + value || (value = this.comparator); + var iterator = _.isFunction(value) ? value : function(model) { + return model.get(value); + }; + return _.sortedIndex(this.models, model, iterator, context); + }, + + // Pluck an attribute from each model in the collection. + pluck: function(attr) { + return _.invoke(this.models, 'get', attr); + }, + + // Fetch the default set of models for this collection, resetting the + // collection when they arrive. If `reset: true` is passed, the response + // data will be passed through the `reset` method instead of `set`. + fetch: function(options) { + options = options ? _.clone(options) : {}; + if (options.parse === void 0) options.parse = true; + var success = options.success; + var collection = this; + options.success = function(resp) { + var method = options.reset ? 'reset' : 'set'; + collection[method](resp, options); + if (success) success(collection, resp, options); + collection.trigger('sync', collection, resp, options); + }; + wrapError(this, options); + return this.sync('read', this, options); + }, + + // Create a new instance of a model in this collection. Add the model to the + // collection immediately, unless `wait: true` is passed, in which case we + // wait for the server to agree. + create: function(model, options) { + options = options ? _.clone(options) : {}; + if (!(model = this._prepareModel(model, options))) return false; + if (!options.wait) this.add(model, options); + var collection = this; + var success = options.success; + options.success = function(resp) { + if (options.wait) collection.add(model, options); + if (success) success(model, resp, options); + }; + model.save(null, options); + return model; + }, + + // **parse** converts a response into a list of models to be added to the + // collection. The default implementation is just to pass it through. + parse: function(resp, options) { + return resp; + }, + + // Create a new collection with an identical list of models as this one. + clone: function() { + return new this.constructor(this.models); + }, + + // Private method to reset all internal state. Called when the collection + // is first initialized or reset. + _reset: function() { + this.length = 0; + this.models = []; + this._byId = {}; + }, + + // Prepare a hash of attributes (or other model) to be added to this + // collection. + _prepareModel: function(attrs, options) { + if (attrs instanceof Model) { + if (!attrs.collection) attrs.collection = this; + return attrs; + } + options || (options = {}); + options.collection = this; + var model = new this.model(attrs, options); + if (!model._validate(attrs, options)) { + this.trigger('invalid', this, attrs, options); + return false; + } + return model; + }, + + // Internal method to sever a model's ties to a collection. + _removeReference: function(model) { + if (this === model.collection) delete model.collection; + model.off('all', this._onModelEvent, this); + }, + + // Internal method called every time a model in the set fires an event. + // Sets need to update their indexes when models change ids. All other + // events simply proxy through. "add" and "remove" events that originate + // in other collections are ignored. + _onModelEvent: function(event, model, collection, options) { + if ((event === 'add' || event === 'remove') && collection !== this) return; + if (event === 'destroy') this.remove(model, options); + if (model && event === 'change:' + model.idAttribute) { + delete this._byId[model.previous(model.idAttribute)]; + if (model.id != null) this._byId[model.id] = model; + } + this.trigger.apply(this, arguments); + } + + }); + + // Underscore methods that we want to implement on the Collection. + // 90% of the core usefulness of Backbone Collections is actually implemented + // right here: + var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl', + 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select', + 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', + 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest', + 'tail', 'drop', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf', + 'isEmpty', 'chain']; + + // Mix in each Underscore method as a proxy to `Collection#models`. + _.each(methods, function(method) { + Collection.prototype[method] = function() { + var args = slice.call(arguments); + args.unshift(this.models); + return _[method].apply(_, args); + }; + }); + + // Underscore methods that take a property name as an argument. + var attributeMethods = ['groupBy', 'countBy', 'sortBy']; + + // Use attributes instead of properties. + _.each(attributeMethods, function(method) { + Collection.prototype[method] = function(value, context) { + var iterator = _.isFunction(value) ? value : function(model) { + return model.get(value); + }; + return _[method](this.models, iterator, context); + }; + }); + + // Backbone.View + // ------------- + + // Backbone Views are almost more convention than they are actual code. A View + // is simply a JavaScript object that represents a logical chunk of UI in the + // DOM. This might be a single item, an entire list, a sidebar or panel, or + // even the surrounding frame which wraps your whole app. Defining a chunk of + // UI as a **View** allows you to define your DOM events declaratively, without + // having to worry about render order ... and makes it easy for the view to + // react to specific changes in the state of your models. + + // Creating a Backbone.View creates its initial element outside of the DOM, + // if an existing element is not provided... + var View = Backbone.View = function(options) { + this.cid = _.uniqueId('view'); + this._configure(options || {}); + this._ensureElement(); + this.initialize.apply(this, arguments); + this.delegateEvents(); + }; + + // Cached regex to split keys for `delegate`. + var delegateEventSplitter = /^(\S+)\s*(.*)$/; + + // List of view options to be merged as properties. + var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; + + // Set up all inheritable **Backbone.View** properties and methods. + _.extend(View.prototype, Events, { + + // The default `tagName` of a View's element is `"div"`. + tagName: 'div', + + // jQuery delegate for element lookup, scoped to DOM elements within the + // current view. This should be prefered to global lookups where possible. + $: function(selector) { + return this.$el.find(selector); + }, + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // **render** is the core function that your view should override, in order + // to populate its element (`this.el`), with the appropriate HTML. The + // convention is for **render** to always return `this`. + render: function() { + return this; + }, + + // Remove this view by taking the element out of the DOM, and removing any + // applicable Backbone.Events listeners. + remove: function() { + this.$el.remove(); + this.stopListening(); + return this; + }, + + // Change the view's element (`this.el` property), including event + // re-delegation. + setElement: function(element, delegate) { + if (this.$el) this.undelegateEvents(); + this.$el = element instanceof Backbone.$ ? element : Backbone.$(element); + this.el = this.$el[0]; + if (delegate !== false) this.delegateEvents(); + return this; + }, + + // Set callbacks, where `this.events` is a hash of + // + // *{"event selector": "callback"}* + // + // { + // 'mousedown .title': 'edit', + // 'click .button': 'save' + // 'click .open': function(e) { ... } + // } + // + // pairs. Callbacks will be bound to the view, with `this` set properly. + // Uses event delegation for efficiency. + // Omitting the selector binds the event to `this.el`. + // This only works for delegate-able events: not `focus`, `blur`, and + // not `change`, `submit`, and `reset` in Internet Explorer. + delegateEvents: function(events) { + if (!(events || (events = _.result(this, 'events')))) return this; + this.undelegateEvents(); + for (var key in events) { + var method = events[key]; + if (!_.isFunction(method)) method = this[events[key]]; + if (!method) continue; + + var match = key.match(delegateEventSplitter); + var eventName = match[1], selector = match[2]; + method = _.bind(method, this); + eventName += '.delegateEvents' + this.cid; + if (selector === '') { + this.$el.on(eventName, method); + } else { + this.$el.on(eventName, selector, method); + } + } + return this; + }, + + // Clears all callbacks previously bound to the view with `delegateEvents`. + // You usually don't need to use this, but may wish to if you have multiple + // Backbone views attached to the same DOM element. + undelegateEvents: function() { + this.$el.off('.delegateEvents' + this.cid); + return this; + }, + + // Performs the initial configuration of a View with a set of options. + // Keys with special meaning *(e.g. model, collection, id, className)* are + // attached directly to the view. See `viewOptions` for an exhaustive + // list. + _configure: function(options) { + if (this.options) options = _.extend({}, _.result(this, 'options'), options); + _.extend(this, _.pick(options, viewOptions)); + this.options = options; + }, + + // Ensure that the View has a DOM element to render into. + // If `this.el` is a string, pass it through `$()`, take the first + // matching element, and re-assign it to `el`. Otherwise, create + // an element from the `id`, `className` and `tagName` properties. + _ensureElement: function() { + if (!this.el) { + var attrs = _.extend({}, _.result(this, 'attributes')); + if (this.id) attrs.id = _.result(this, 'id'); + if (this.className) attrs['class'] = _.result(this, 'className'); + var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs); + this.setElement($el, false); + } else { + this.setElement(_.result(this, 'el'), false); + } + } + + }); + + // Backbone.sync + // ------------- + + // Override this function to change the manner in which Backbone persists + // models to the server. You will be passed the type of request, and the + // model in question. By default, makes a RESTful Ajax request + // to the model's `url()`. Some possible customizations could be: + // + // * Use `setTimeout` to batch rapid-fire updates into a single request. + // * Send up the models as XML instead of JSON. + // * Persist models via WebSockets instead of Ajax. + // + // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests + // as `POST`, with a `_method` parameter containing the true HTTP method, + // as well as all requests with the body as `application/x-www-form-urlencoded` + // instead of `application/json` with the model in a param named `model`. + // Useful when interfacing with server-side languages like **PHP** that make + // it difficult to read the body of `PUT` requests. + Backbone.sync = function(method, model, options) { + var type = methodMap[method]; + + // Default options, unless specified. + _.defaults(options || (options = {}), { + emulateHTTP: Backbone.emulateHTTP, + emulateJSON: Backbone.emulateJSON + }); + + // Default JSON-request options. + var params = {type: type, dataType: 'json'}; + + // Ensure that we have a URL. + if (!options.url) { + params.url = _.result(model, 'url') || urlError(); + } + + // Ensure that we have the appropriate request data. + if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { + params.contentType = 'application/json'; + params.data = JSON.stringify(options.attrs || model.toJSON(options)); + } + + // For older servers, emulate JSON by encoding the request into an HTML-form. + if (options.emulateJSON) { + params.contentType = 'application/x-www-form-urlencoded'; + params.data = params.data ? {model: params.data} : {}; + } + + // For older servers, emulate HTTP by mimicking the HTTP method with `_method` + // And an `X-HTTP-Method-Override` header. + if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) { + params.type = 'POST'; + if (options.emulateJSON) params.data._method = type; + var beforeSend = options.beforeSend; + options.beforeSend = function(xhr) { + xhr.setRequestHeader('X-HTTP-Method-Override', type); + if (beforeSend) return beforeSend.apply(this, arguments); + }; + } + + // Don't process data on a non-GET request. + if (params.type !== 'GET' && !options.emulateJSON) { + params.processData = false; + } + + // If we're sending a `PATCH` request, and we're in an old Internet Explorer + // that still has ActiveX enabled by default, override jQuery to use that + // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8. + if (params.type === 'PATCH' && window.ActiveXObject && + !(window.external && window.external.msActiveXFilteringEnabled)) { + params.xhr = function() { + return new ActiveXObject("Microsoft.XMLHTTP"); + }; + } + + // Make the request, allowing the user to override any Ajax options. + var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); + model.trigger('request', model, xhr, options); + return xhr; + }; + + // Map from CRUD to HTTP for our default `Backbone.sync` implementation. + var methodMap = { + 'create': 'POST', + 'update': 'PUT', + 'patch': 'PATCH', + 'delete': 'DELETE', + 'read': 'GET' + }; + + // Set the default implementation of `Backbone.ajax` to proxy through to `$`. + // Override this if you'd like to use a different library. + Backbone.ajax = function() { + return Backbone.$.ajax.apply(Backbone.$, arguments); + }; + + // Backbone.Router + // --------------- + + // Routers map faux-URLs to actions, and fire events when routes are + // matched. Creating a new one sets its `routes` hash, if not set statically. + var Router = Backbone.Router = function(options) { + options || (options = {}); + if (options.routes) this.routes = options.routes; + this._bindRoutes(); + this.initialize.apply(this, arguments); + }; + + // Cached regular expressions for matching named param parts and splatted + // parts of route strings. + var optionalParam = /\((.*?)\)/g; + var namedParam = /(\(\?)?:\w+/g; + var splatParam = /\*\w+/g; + var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; + + // Set up all inheritable **Backbone.Router** properties and methods. + _.extend(Router.prototype, Events, { + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // Manually bind a single named route to a callback. For example: + // + // this.route('search/:query/p:num', 'search', function(query, num) { + // ... + // }); + // + route: function(route, name, callback) { + if (!_.isRegExp(route)) route = this._routeToRegExp(route); + if (_.isFunction(name)) { + callback = name; + name = ''; + } + if (!callback) callback = this[name]; + var router = this; + Backbone.history.route(route, function(fragment) { + var args = router._extractParameters(route, fragment); + callback && callback.apply(router, args); + router.trigger.apply(router, ['route:' + name].concat(args)); + router.trigger('route', name, args); + Backbone.history.trigger('route', router, name, args); + }); + return this; + }, + + // Simple proxy to `Backbone.history` to save a fragment into the history. + navigate: function(fragment, options) { + Backbone.history.navigate(fragment, options); + return this; + }, + + // Bind all defined routes to `Backbone.history`. We have to reverse the + // order of the routes here to support behavior where the most general + // routes can be defined at the bottom of the route map. + _bindRoutes: function() { + if (!this.routes) return; + this.routes = _.result(this, 'routes'); + var route, routes = _.keys(this.routes); + while ((route = routes.pop()) != null) { + this.route(route, this.routes[route]); + } + }, + + // Convert a route string into a regular expression, suitable for matching + // against the current location hash. + _routeToRegExp: function(route) { + route = route.replace(escapeRegExp, '\\$&') + .replace(optionalParam, '(?:$1)?') + .replace(namedParam, function(match, optional){ + return optional ? match : '([^\/]+)'; + }) + .replace(splatParam, '(.*?)'); + return new RegExp('^' + route + '$'); + }, + + // Given a route, and a URL fragment that it matches, return the array of + // extracted decoded parameters. Empty or unmatched parameters will be + // treated as `null` to normalize cross-browser behavior. + _extractParameters: function(route, fragment) { + var params = route.exec(fragment).slice(1); + return _.map(params, function(param) { + return param ? decodeURIComponent(param) : null; + }); + } + + }); + + // Backbone.History + // ---------------- + + // Handles cross-browser history management, based on either + // [pushState](http://diveintohtml5.info/history.html) and real URLs, or + // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) + // and URL fragments. If the browser supports neither (old IE, natch), + // falls back to polling. + var History = Backbone.History = function() { + this.handlers = []; + _.bindAll(this, 'checkUrl'); + + // Ensure that `History` can be used outside of the browser. + if (typeof window !== 'undefined') { + this.location = window.location; + this.history = window.history; + } + }; + + // Cached regex for stripping a leading hash/slash and trailing space. + var routeStripper = /^[#\/]|\s+$/g; + + // Cached regex for stripping leading and trailing slashes. + var rootStripper = /^\/+|\/+$/g; + + // Cached regex for detecting MSIE. + var isExplorer = /msie [\w.]+/; + + // Cached regex for removing a trailing slash. + var trailingSlash = /\/$/; + + // Has the history handling already been started? + History.started = false; + + // Set up all inheritable **Backbone.History** properties and methods. + _.extend(History.prototype, Events, { + + // The default interval to poll for hash changes, if necessary, is + // twenty times a second. + interval: 50, + + // Gets the true hash value. Cannot use location.hash directly due to bug + // in Firefox where location.hash will always be decoded. + getHash: function(window) { + var match = (window || this).location.href.match(/#(.*)$/); + return match ? match[1] : ''; + }, + + // Get the cross-browser normalized URL fragment, either from the URL, + // the hash, or the override. + getFragment: function(fragment, forcePushState) { + if (fragment == null) { + if (this._hasPushState || !this._wantsHashChange || forcePushState) { + fragment = this.location.pathname; + var root = this.root.replace(trailingSlash, ''); + if (!fragment.indexOf(root)) fragment = fragment.substr(root.length); + } else { + fragment = this.getHash(); + } + } + return fragment.replace(routeStripper, ''); + }, + + // Start the hash change handling, returning `true` if the current URL matches + // an existing route, and `false` otherwise. + start: function(options) { + if (History.started) throw new Error("Backbone.history has already been started"); + History.started = true; + + // Figure out the initial configuration. Do we need an iframe? + // Is pushState desired ... is it available? + this.options = _.extend({}, {root: '/'}, this.options, options); + this.root = this.options.root; + this._wantsHashChange = this.options.hashChange !== false; + this._wantsPushState = !!this.options.pushState; + this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState); + var fragment = this.getFragment(); + var docMode = document.documentMode; + var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); + + // Normalize root to always include a leading and trailing slash. + this.root = ('/' + this.root + '/').replace(rootStripper, '/'); + + if (oldIE && this._wantsHashChange) { + this.iframe = Backbone.$('