From 075646cea07b0b7b3630c9dc5fc2e58b56b4b72f Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Mon, 15 Jan 2024 10:42:01 +0000 Subject: [PATCH 001/241] remove quiet_assets gem; Deprecated gem and not work with Rails 5. --- Gemfile | 1 - Gemfile.lock | 3 --- 2 files changed, 4 deletions(-) diff --git a/Gemfile b/Gemfile index 1237c5559..2a757f960 100644 --- a/Gemfile +++ b/Gemfile @@ -105,7 +105,6 @@ group :development do gem 'slackistrano', '0.1.9', require: false gem 'brightbox', '2.3.9' gem 'rack-cors', '0.3.0' ,:require => 'rack/cors' # TODO: remove when upgrade Rails. - gem 'quiet_assets', '1.1.0' gem 'webrick', '1.3.1' gem 'jslint_on_rails', '1.1.1' gem 'rubocop', '0.40.0', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 49f95f034..335a535c3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -345,8 +345,6 @@ GEM coderay (~> 1.1) method_source (~> 1.0) public_suffix (4.0.7) - quiet_assets (1.1.0) - railties (>= 3.1, < 5.0) racc (1.7.3) rack (1.6.13) rack-attack (5.4.2) @@ -614,7 +612,6 @@ DEPENDENCIES pg_search (= 1.0.6) prawn (= 0.13.2) protected_attributes (= 1.1.4) - quiet_assets (= 1.1.0) rack-cors (= 0.3.0) rack-livereload (= 0.3.11) rails (= 4.2.11.3) From df9b841e9d9a2a35f2b9969781b1542e4697419e Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Mon, 15 Jan 2024 11:00:04 +0000 Subject: [PATCH 002/241] replace protected_attributes with protected_attributes_continued for Rails 5+ --- Gemfile | 2 +- Gemfile.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 2a757f960..3fb3f5e8d 100644 --- a/Gemfile +++ b/Gemfile @@ -20,7 +20,7 @@ gem 'nokogiri', '1.12.5' # TODO: 1.12.5 is the last version support 2.5. New ver gem 'inherited_resources', '1.7.2' # TODO: need upgrade when upgrade to Rails 5 gem 'traco', '~> 5.3', '>= 5.3.3' # TODO: latest version @ 2021. Suggest migrate to Mobility gem. # gem 'strong_parameters' -gem 'protected_attributes', '1.1.4' # TODO: Only support Rails version < 5 (https://github.com/rails/protected_attributes) +gem 'protected_attributes_continued', '1.2.4' # TODO: upgrade to latest after we successfully upgrade Rails to 5. gem 'devise', '4.4.3' # TODO: version 4.4.3 work under <=Rails 5.3 and <=Ruby 2.6 gem 'cancancan', '1.17.0' # TODO, need upgrade to 2.0 for Rails 5 gem 'ahoy_matey', '1.6.1' # TODO: latest 5.0.2. Can't upgrade to 2.0 until upgrade to Rails 5 diff --git a/Gemfile.lock b/Gemfile.lock index 335a535c3..1c2083a88 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -339,8 +339,8 @@ GEM pdf-reader (~> 1.2) ruby-rc4 ttfunk (~> 1.0.3) - protected_attributes (1.1.4) - activemodel (>= 4.0.1, < 5.0) + protected_attributes_continued (1.2.4) + activemodel (>= 4.0.1, < 6.0) pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) @@ -611,7 +611,7 @@ DEPENDENCIES pg_array_parser (= 0.0.9) pg_search (= 1.0.6) prawn (= 0.13.2) - protected_attributes (= 1.1.4) + protected_attributes_continued (= 1.2.4) rack-cors (= 0.3.0) rack-livereload (= 0.3.11) rails (= 4.2.11.3) From 375cbd91ad4caab1447b3dd5632cb4193921642a Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Mon, 15 Jan 2024 11:20:29 +0000 Subject: [PATCH 003/241] upgrade gem inherited_resources --- Gemfile | 2 +- Gemfile.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index 3fb3f5e8d..6931863f2 100644 --- a/Gemfile +++ b/Gemfile @@ -17,7 +17,7 @@ gem 'nested-hstore', '0.1.2' # TODO: latest 0.1.2 @ 2015 gem 'pg_search', '1.0.6' # TODO: update to newer version when upgrade to Rails 5 gem 'oj', '3.14.2' # optimised JSON (picked by multi_json) # TODO: to upgrade to newer version, need >=Ruby 2.7 gem 'nokogiri', '1.12.5' # TODO: 1.12.5 is the last version support 2.5. New version need Ruby 2.6+ -gem 'inherited_resources', '1.7.2' # TODO: need upgrade when upgrade to Rails 5 +gem 'inherited_resources', '1.9.0' # TODO: need upgrade when upgrade to Rails 6 gem 'traco', '~> 5.3', '>= 5.3.3' # TODO: latest version @ 2021. Suggest migrate to Mobility gem. # gem 'strong_parameters' gem 'protected_attributes_continued', '1.2.4' # TODO: upgrade to latest after we successfully upgrade Rails to 5. diff --git a/Gemfile.lock b/Gemfile.lock index 1c2083a88..c08325e82 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -234,10 +234,10 @@ GEM multi_xml (>= 0.5.2) i18n (0.9.5) concurrent-ruby (~> 1.0) - inherited_resources (1.7.2) - actionpack (>= 3.2, < 5.2.x) + inherited_resources (1.9.0) + actionpack (>= 4.2, < 5.3) has_scope (~> 0.6) - railties (>= 3.2, < 5.2.x) + railties (>= 4.2, < 5.3) responders io-wait (0.3.1) jmespath (1.6.2) @@ -596,7 +596,7 @@ DEPENDENCIES guard-livereload (= 1.1.3) handlebars-source (= 1.0.12) httparty (~> 0.21.0) - inherited_resources (= 1.7.2) + inherited_resources (= 1.9.0) jslint_on_rails (= 1.1.1) json_spec (= 1.1.5) kaminari (= 1.2.2) From a2434fff21c2959e6011e91dfc1a917aa3a13576 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Mon, 15 Jan 2024 14:24:23 +0000 Subject: [PATCH 004/241] upgrade ahoy to 2.2.1 from 1.6.1 --- Gemfile | 6 +- Gemfile.lock | 19 +++-- config/initializers/ahoy.rb | 76 ++++++++----------- ...0240115115459_ahoy_1_4_0_upgrade_step_2.rb | 2 +- 4 files changed, 46 insertions(+), 57 deletions(-) diff --git a/Gemfile b/Gemfile index 6931863f2..a6a2bd073 100644 --- a/Gemfile +++ b/Gemfile @@ -3,6 +3,7 @@ source 'https://rubygems.org' ruby '2.5.9' gem 'rails', '4.2.11.3' +# gem 'rails', '5.0.7.2' # Bundle edge Rails instead: # gem 'rails', :git => 'git://github.com/rails/rails.git' @@ -22,8 +23,9 @@ gem 'traco', '~> 5.3', '>= 5.3.3' # TODO: latest version @ 2021. Suggest migrate # gem 'strong_parameters' gem 'protected_attributes_continued', '1.2.4' # TODO: upgrade to latest after we successfully upgrade Rails to 5. gem 'devise', '4.4.3' # TODO: version 4.4.3 work under <=Rails 5.3 and <=Ruby 2.6 -gem 'cancancan', '1.17.0' # TODO, need upgrade to 2.0 for Rails 5 -gem 'ahoy_matey', '1.6.1' # TODO: latest 5.0.2. Can't upgrade to 2.0 until upgrade to Rails 5 +gem 'cancancan', '1.17.0' # TODO, can upgrade to 2.0 after Rails 5 +gem 'ahoy_matey', '2.2.1' # TODO: latest 5.0.2. Can't upgrade to 3.0 until upgrade to Rails 5 +gem 'uuidtools', '~> 2.2' # For Ahoy. (https://github.com/ankane/ahoy/blob/v2.2.1/docs/Ahoy-2-Upgrade.md#activerecordstore) gem 'browser', '2.5.3' # Latest 5.3.1 @ 2021, doesn't work with this project, maybe try again after upgrade ruby > 2.5 and rails >= 5 # TODO: starting from v1.4, it break our test due to redirection changes: diff --git a/Gemfile.lock b/Gemfile.lock index c08325e82..87baa1145 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -47,17 +47,16 @@ GEM addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) afm (0.2.2) - ahoy_matey (1.6.1) + ahoy_matey (2.2.1) addressable browser (~> 2.0) - geocoder - rack-attack (< 6) - railties - referer-parser (>= 0.3.0) + device_detector + geocoder (>= 1.4.5) + railties (>= 4.2) + referer-parser (>= 0.3) request_store - safely_block (>= 0.1.1) + safely_block (>= 0.2.1) user_agent_parser - uuidtools airbrussh (1.5.1) sshkit (>= 1.6.1, != 1.7.0) annotate (2.5.0) @@ -154,6 +153,7 @@ GEM dalli (2.7.10) database_cleaner (1.2.0) debug_inspector (1.2.0) + device_detector (1.0.7) devise (4.4.3) bcrypt (~> 3.0) orm_adapter (~> 0.1) @@ -347,8 +347,6 @@ GEM public_suffix (4.0.7) racc (1.7.3) rack (1.6.13) - rack-attack (5.4.2) - rack (>= 1.0, < 3) rack-cors (0.3.0) rack-livereload (0.3.11) rack @@ -555,7 +553,7 @@ DEPENDENCIES actionpack-page_caching (= 1.1.1) active_model_serializers (= 0.8.4) acts-as-taggable-on (= 5.0.0) - ahoy_matey (= 1.6.1) + ahoy_matey (= 2.2.1) annotate (= 2.5.0) appsignal (= 1.3.3) aws-sdk (~> 2) @@ -639,6 +637,7 @@ DEPENDENCIES test-unit (= 3.1.5) traco (~> 5.3, >= 5.3.3) uglifier (= 2.7.2) + uuidtools (~> 2.2) web-console (~> 2.0) webrick (= 1.3.1) whenever (= 0.11.0) diff --git a/config/initializers/ahoy.rb b/config/initializers/ahoy.rb index 9145c6926..168640987 100644 --- a/config/initializers/ahoy.rb +++ b/config/initializers/ahoy.rb @@ -1,56 +1,44 @@ require 'sapi/geoip' -class Ahoy::Store < Ahoy::Stores::ActiveRecordStore - def report_exception(exception) - Appsignal.add_exception(exception) if defined? Appsignal - end +class Ahoy::Store < Ahoy::DatabaseStore + UUID_NAMESPACE = UUIDTools::UUID.parse("dcd74c26-8fc9-453a-a9c2-afc445c3258d") def visit_model Ahoy::Visit end - def track_visit(options) - visit = visit_model.new( - { - id: ahoy.visit_id, - visitor_id: ahoy.visitor_id, - user: user, - started_at: options[:started_at] - }, - :without_protection => true - ) - visit_properties.keys.each do |key| - visit.send(:"#{key}=", visit_properties[key]) if visit.respond_to?(:"#{key}=") - end + def visit + @visit ||= visit_model.find_by(id: ensure_uuid(ahoy.visit_token)) if ahoy.visit_token + end + + def track_visit(data) + data[:id] = ensure_uuid(data.delete(:visit_token)) + data[:visitor_id] = ensure_uuid(data.delete(:visitor_token)) + geo_ip_data = Sapi::GeoIP.instance.resolve(request.ip) - visit.country = geo_ip_data[:country] - visit.city = geo_ip_data[:city] - visit.organization = geo_ip_data[:organization] - - begin - visit.save! - rescue ActiveRecord::RecordNotUnique - # do nothing - end + data[:country] = geo_ip_data[:country] + data[:city] = geo_ip_data[:city] + data[:organization] = geo_ip_data[:organization] + + super(data) + end + + def track_event(data) + data[:id] = ensure_uuid(data.delete(:event_id)) + super(data) end - def track_event(name, properties, options) - event = event_model.new( - { - id: options[:id], - visit_id: ahoy.visit_id, - user: user, - name: name, - properties: properties, - time: options[:time] - }, - :without_protection => true - ) - - begin - event.save! - rescue ActiveRecord::RecordNotUnique - # do nothing - end + def ensure_uuid(id) + UUIDTools::UUID.parse(id).to_s + rescue + UUIDTools::UUID.sha1_create(UUID_NAMESPACE, id).to_s end end Ahoy.quiet = false +Ahoy.api = true +Ahoy.server_side_visits = :when_needed +Ahoy.user_agent_parser = :device_detector +Ahoy.bot_detection_version = 2 +Ahoy.track_bots = Rails.env.test? + +# https://github.com/ankane/ahoy/tree/v2.2.1#exceptions +Safely.report_exception_method = ->(e) { Appsignal.add_exception(exception) if defined? Appsignal } diff --git a/db/migrate/20240115115459_ahoy_1_4_0_upgrade_step_2.rb b/db/migrate/20240115115459_ahoy_1_4_0_upgrade_step_2.rb index f7060fc32..2848f2dc3 100644 --- a/db/migrate/20240115115459_ahoy_1_4_0_upgrade_step_2.rb +++ b/db/migrate/20240115115459_ahoy_1_4_0_upgrade_step_2.rb @@ -2,7 +2,7 @@ # https://stackoverflow.com/a/31672314/556780 class Ahoy140UpgradeStep2 < ActiveRecord::Migration disable_ddl_transaction! - def change + def up Ahoy::Event.where(properties: nil).select(:id).find_in_batches do |events| Ahoy::Event.where(id: events.map(&:id)).update_all("properties = (regexp_replace(properties_json::text, '\\\\u0000', '', 'g'))::json::jsonb") end From 1e1a8e0875c81a6adfb1ced91f47a1caa2552904 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Mon, 15 Jan 2024 14:50:01 +0000 Subject: [PATCH 005/241] release sprockets version lock. Rails 5 need sprockets 3+ --- Gemfile | 7 +++---- Gemfile.lock | 20 ++++++++------------ 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/Gemfile b/Gemfile index a6a2bd073..d8c5873ec 100644 --- a/Gemfile +++ b/Gemfile @@ -20,7 +20,6 @@ gem 'oj', '3.14.2' # optimised JSON (picked by multi_json) # TODO: to upgrade to gem 'nokogiri', '1.12.5' # TODO: 1.12.5 is the last version support 2.5. New version need Ruby 2.6+ gem 'inherited_resources', '1.9.0' # TODO: need upgrade when upgrade to Rails 6 gem 'traco', '~> 5.3', '>= 5.3.3' # TODO: latest version @ 2021. Suggest migrate to Mobility gem. -# gem 'strong_parameters' gem 'protected_attributes_continued', '1.2.4' # TODO: upgrade to latest after we successfully upgrade Rails to 5. gem 'devise', '4.4.3' # TODO: version 4.4.3 work under <=Rails 5.3 and <=Ruby 2.6 gem 'cancancan', '1.17.0' # TODO, can upgrade to 2.0 after Rails 5 @@ -46,11 +45,11 @@ gem 'redis-rails', '5.0.2' # TODO: latest, may remove this Gem when upgrade to R gem 'whenever', '0.11.0', :require => false # TODO: latest version 1.0 @ 2019. Should migrate to sidekiq-cron. gem 'httparty', '~> 0.21.0' # TODO: latest. -gem 'sprockets', '2.12.5' # upgrading to 3 breaks handlebars/tilt +# gem 'sprockets', '2.12.5' # upgrading to 3 breaks handlebars/tilt gem 'kaminari', '1.2.2' # TODO: latest @ 2021. Suggest migrate to pagy gem. -gem 'acts-as-taggable-on', '5.0.0' # TODO: latest v10 @ 2023. Need upgrade when upgrade to Rails 5. -gem 'carrierwave', '1.3.1' # TODO: latest is 3.0.5 @ 2023. v2 support Rails 5 +gem 'acts-as-taggable-on', '5.0.0' # TODO: latest v10 @ 2023. Need upgrade after upgrade to Rails 5. +gem 'carrierwave', '1.3.1' # TODO: latest is 3.0.5 @ 2023. can upgrade to v2 after Rails 5 # PDF gem 'prawn', '0.13.2' diff --git a/Gemfile.lock b/Gemfile.lock index 87baa1145..53153177a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -225,7 +225,6 @@ GEM actionpack (>= 4.1) activesupport (>= 4.1) hashery (2.1.2) - hike (1.2.3) http-cookie (1.0.5) domain_name (~> 0.5) http_parser.rb (0.8.0) @@ -485,15 +484,13 @@ GEM capistrano (>= 3.0.1) json spring (3.1.1) - sprockets (2.12.5) - hike (~> 1.2) - multi_json (~> 1.0) - rack (~> 1.0) - tilt (~> 1.1, != 1.3.0) - sprockets-rails (2.3.3) - actionpack (>= 3.0) - activesupport (>= 3.0) - sprockets (>= 2.8, < 4.0) + sprockets (3.7.2) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + sprockets-rails (3.2.2) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) sshkit (1.22.0) mutex_m net-scp (>= 1.1.2) @@ -511,7 +508,7 @@ GEM power_assert thor (1.2.2) thread_safe (0.3.6) - tilt (1.4.1) + tilt (2.3.0) timeout (0.4.0) tins (1.32.1) sync @@ -631,7 +628,6 @@ DEPENDENCIES sitemap_generator (~> 6.3) slackistrano (= 0.1.9) spring - sprockets (= 2.12.5) strong_migrations (~> 0.3.1) susy (= 2.2.14) test-unit (= 3.1.5) From 11e1019f8c9732ee13ab7def774195f64f26c740 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Mon, 15 Jan 2024 16:13:20 +0000 Subject: [PATCH 006/241] upgrade ember (broken) --- Gemfile | 14 +++++++++++--- Gemfile.lock | 40 +++++++++++++++++++++++++++------------- 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/Gemfile b/Gemfile index d8c5873ec..0a7406024 100644 --- a/Gemfile +++ b/Gemfile @@ -168,8 +168,16 @@ gem 'gon', '~> 6.4' # TODO: latest gem "chartkick", '2.3.5' # TODO: latest 5.0.5 @ 2023. Should upgrade to v4 once we upgrade to Rails 5.2+ and Ruby 2.6+ gem 'nested_form', '0.3.2' # TODO: latest @ 2013. Project is public archived on github. No longer maintained. gem 'bootstrap-sass', '2.3.2.2' # TODO: latest 3.4.1 @ 2019. Can't upgrade unless we sure bootstrap v3 backward compatible with boostrap v2 + # Ember -gem 'ember-rails', '0.14.1' # Not support Sprockets 3+ unless upgrade. Latest version support Rails 5.1 -gem 'ember-source', '1.6.1' # TODO: just a wrapwrapper. Any update will change the ember.js version. -gem 'ember-data-source', '0.14' # TODO: just a wrapwrapper. Any update will change the JS ember-data version. +gem 'ember-rails', '~> 0.21.0' # Latest @ 2017 +# gem 'ember-rails', '0.14.1' # TODO: upgrade to latest after Rails 5 + +gem 'ember-source', '1.8.0' # TODO: just a wrapwrapper. Any update will change the ember.js version. +# gem 'ember-source', '1.6.1' # TODO: just a wrapwrapper. Any update will change the ember.js version. + +gem 'ember-data-source', '1.13.0' # TODO: just a wrapwrapper. Any update will change the JS ember-data version. +# gem 'ember-data-source', '0.14' # TODO: just a wrapwrapper. Any update will change the JS ember-data version. + gem 'handlebars-source', '1.0.12' # TODO: just a wrapwrapper. Any update will change the handlebars.js version. +# gem 'handlebars-source', '1.0.12' # TODO: just a wrapwrapper. Any update will change the handlebars.js version. diff --git a/Gemfile.lock b/Gemfile.lock index 53153177a..0b6f2a129 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -25,6 +25,8 @@ GEM erubis (~> 2.7.0) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.3) + active-model-adapter-source (2.1.1) + ember-data-source (>= 1.13, < 3.0) active_model_serializers (0.8.4) activemodel (>= 3.0) activejob (4.2.11.3) @@ -76,6 +78,10 @@ GEM aws-sdk-core (= 2.11.632) aws-sigv4 (1.8.0) aws-eventstream (~> 1, >= 1.0.2) + babel-source (5.8.35) + babel-transpiler (0.7.0) + babel-source (>= 4.0, < 6) + execjs (~> 2.0) barber (0.12.2) ember-source (>= 1.0, < 3.1) execjs (>= 1.2, < 3) @@ -172,18 +178,26 @@ GEM em-websocket (0.5.3) eventmachine (>= 0.12.9) http_parser.rb (~> 0) - ember-data-source (0.14) - ember-source - ember-rails (0.14.1) + ember-cli-assets (0.0.37) + ember-data-source (1.13.0) + ember-source (>= 1.8, < 3.0) + ember-es6_template (0.6.0) + babel-transpiler (>= 0.6.0, < 0.8) + sprockets (>= 2.2, < 4.1) + ember-handlebars-template (0.8.0) + barber (>= 0.11.0) + sprockets (>= 3.3, < 4.1) + ember-rails (0.21.0) + active-model-adapter-source (>= 1.13.0) active_model_serializers - barber (>= 0.4.1) - ember-data-source - ember-source - execjs (>= 1.2) - handlebars-source + ember-cli-assets (~> 0.0.1) + ember-data-source (>= 1.13.0) + ember-es6_template (>= 0.4.0, < 0.7) + ember-handlebars-template (>= 0.1.1, < 1.0) + ember-source (>= 1.8.0) jquery-rails (>= 1.0.17) - railties (>= 3.1) - ember-source (1.6.1) + railties (>= 4.2) + ember-source (1.8.0) handlebars-source (~> 1.0) errbase (0.2.2) erubis (2.7.0) @@ -580,9 +594,9 @@ DEPENDENCIES devise (= 4.4.3) dotenv-rails (= 2.0.1) ed25519 (= 1.2.4) - ember-data-source (= 0.14) - ember-rails (= 0.14.1) - ember-source (= 1.6.1) + ember-data-source (= 1.13.0) + ember-rails (~> 0.21.0) + ember-source (= 1.8.0) factory_girl_rails (= 4.2.1) geoip (= 1.3.5) gon (~> 6.4) From 57fb3024f1faf3c513c41463d7d190e77ce4feb8 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Mon, 15 Jan 2024 16:50:36 +0000 Subject: [PATCH 007/241] upgrade pg_search --- Gemfile | 4 ++-- Gemfile.lock | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Gemfile b/Gemfile index 0a7406024..a8a9a8477 100644 --- a/Gemfile +++ b/Gemfile @@ -9,13 +9,13 @@ gem 'rails', '4.2.11.3' # gem 'rails', :git => 'git://github.com/rails/rails.git' gem 'actionpack-action_caching', '~> 1.2', '>= 1.2.2' # A feature that removed from core in Rails 4.0, maybe be better migrate away from this. -gem 'actionpack-page_caching', '1.1.1' # A feature that removed from core in Rails 4.0, maybe be better migrate away from this. # TODO, need update when upgrade to rails 5 +gem 'actionpack-page_caching', '1.1.1' # A feature that removed from core in Rails 4.0, maybe be better migrate away from this. # TODO, can update after upgrade to rails 5 gem 'active_model_serializers', '0.8.4' # Deprecated gem 'dalli', '2.7.10' # TODO: latest is 3.2.6. I believe should be fine to upgrade but we have no way to test. gem 'pg', '0.21.0' # TODO: latest 1.5.4, need Rails 5 to upgrade to 1.0.0 gem 'pg_array_parser', '0.0.9' # TODO: latest 0.0.9 gem 'nested-hstore', '0.1.2' # TODO: latest 0.1.2 @ 2015 -gem 'pg_search', '1.0.6' # TODO: update to newer version when upgrade to Rails 5 +gem 'pg_search', '2.3.0' # TODO: can upgrade to newer version after Rails 5 gem 'oj', '3.14.2' # optimised JSON (picked by multi_json) # TODO: to upgrade to newer version, need >=Ruby 2.7 gem 'nokogiri', '1.12.5' # TODO: 1.12.5 is the last version support 2.5. New version need Ruby 2.6+ gem 'inherited_resources', '1.9.0' # TODO: need upgrade when upgrade to Rails 6 diff --git a/Gemfile.lock b/Gemfile.lock index 0b6f2a129..bb2db1fd9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -342,10 +342,9 @@ GEM pdfkit (0.8.7.3) pg (0.21.0) pg_array_parser (0.0.9) - pg_search (1.0.6) - activerecord (>= 3.1) - activesupport (>= 3.1) - arel + pg_search (2.3.0) + activerecord (>= 4.2) + activesupport (>= 4.2) power_assert (2.0.3) powerpack (0.1.3) prawn (0.13.2) @@ -618,7 +617,7 @@ DEPENDENCIES pdfkit (~> 0.8.7.3) pg (= 0.21.0) pg_array_parser (= 0.0.9) - pg_search (= 1.0.6) + pg_search (= 2.3.0) prawn (= 0.13.2) protected_attributes_continued (= 1.2.4) rack-cors (= 0.3.0) From 995f592adda1d8fc70ef92b9aa57039527cf6f0c Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Mon, 15 Jan 2024 16:55:37 +0000 Subject: [PATCH 008/241] upgrade groupdate gem --- Gemfile | 2 +- Gemfile.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index a8a9a8477..307529374 100644 --- a/Gemfile +++ b/Gemfile @@ -32,7 +32,7 @@ gem 'browser', '2.5.3' # Latest 5.3.1 @ 2021, doesn't work with this project, ma # rspec ./spec/controllers/admin/nomenclature_changes/split_controller_spec.rb:191 gem 'wicked', '1.3.4' -gem 'groupdate', '2.4.0' # TODO: seems only ApiRequest#recent_requests using this. Suggest rewrite and remove dependencies. +gem 'groupdate', '4.1.2' # TODO: can upgrade after rails 5 and newer ruby gem 'rubyzip', '~> 2.3', '>= 2.3.2' # TODO: latest gem 'responders', '~> 2.0' # https://guides.rubyonrails.org/v4.2/upgrading_ruby_on_rails.html#responders diff --git a/Gemfile.lock b/Gemfile.lock index bb2db1fd9..09bdc1d50 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -219,8 +219,8 @@ GEM i18n (>= 0.7) multi_json request_store (>= 1.0) - groupdate (2.4.0) - activesupport (>= 3) + groupdate (4.1.2) + activesupport (>= 4.2) guard (1.8.3) formatador (>= 0.2.4) listen (~> 1.3) @@ -599,7 +599,7 @@ DEPENDENCIES factory_girl_rails (= 4.2.1) geoip (= 1.3.5) gon (~> 6.4) - groupdate (= 2.4.0) + groupdate (= 4.1.2) guard-bundler (= 1.0.0) guard-livereload (= 1.1.3) handlebars-source (= 1.0.12) From 14ff1f40c10e19ca08b083542ef95b45c4411d87 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Mon, 15 Jan 2024 17:49:49 +0000 Subject: [PATCH 009/241] clean up gemfile --- Gemfile | 32 +++++++++++++++++--------------- Gemfile.lock | 16 ++++++++-------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/Gemfile b/Gemfile index 307529374..a3ac9a727 100644 --- a/Gemfile +++ b/Gemfile @@ -13,8 +13,8 @@ gem 'actionpack-page_caching', '1.1.1' # A feature that removed from core in Rai gem 'active_model_serializers', '0.8.4' # Deprecated gem 'dalli', '2.7.10' # TODO: latest is 3.2.6. I believe should be fine to upgrade but we have no way to test. gem 'pg', '0.21.0' # TODO: latest 1.5.4, need Rails 5 to upgrade to 1.0.0 -gem 'pg_array_parser', '0.0.9' # TODO: latest 0.0.9 -gem 'nested-hstore', '0.1.2' # TODO: latest 0.1.2 @ 2015 +gem 'pg_array_parser', '~> 0.0.9' +gem 'nested-hstore', '~> 0.1.2' gem 'pg_search', '2.3.0' # TODO: can upgrade to newer version after Rails 5 gem 'oj', '3.14.2' # optimised JSON (picked by multi_json) # TODO: to upgrade to newer version, need >=Ruby 2.7 gem 'nokogiri', '1.12.5' # TODO: 1.12.5 is the last version support 2.5. New version need Ruby 2.6+ @@ -34,7 +34,7 @@ gem 'wicked', '1.3.4' gem 'groupdate', '4.1.2' # TODO: can upgrade after rails 5 and newer ruby -gem 'rubyzip', '~> 2.3', '>= 2.3.2' # TODO: latest +gem 'rubyzip', '~> 2.3', '>= 2.3.2' gem 'responders', '~> 2.0' # https://guides.rubyonrails.org/v4.2/upgrading_ruby_on_rails.html#responders gem 'sidekiq', '4.2.10' # TODO, Ruby 2.7 need version 6.0.5 sidekiq @@ -43,10 +43,10 @@ gem 'sidekiq-unique-jobs', '4.0.18' # TODO: latest is 8.0.5 @ 2023 gem 'redis-rails', '5.0.2' # TODO: latest, may remove this Gem when upgrade to Rails 5.2. (https://github.com/redis-store/redis-rails/tree/master#a-quick-note-about-rails-52) gem 'whenever', '0.11.0', :require => false # TODO: latest version 1.0 @ 2019. Should migrate to sidekiq-cron. -gem 'httparty', '~> 0.21.0' # TODO: latest. +gem 'httparty', '~> 0.21.0' # gem 'sprockets', '2.12.5' # upgrading to 3 breaks handlebars/tilt -gem 'kaminari', '1.2.2' # TODO: latest @ 2021. Suggest migrate to pagy gem. +gem 'kaminari', '~> 1.2', '>= 1.2.2' # TODO: Suggest migrate to pagy gem. gem 'acts-as-taggable-on', '5.0.0' # TODO: latest v10 @ 2023. Need upgrade after upgrade to Rails 5. gem 'carrierwave', '1.3.1' # TODO: latest is 3.0.5 @ 2023. can upgrade to v2 after Rails 5 @@ -59,14 +59,16 @@ gem 'wkhtmltopdf-binary', '~> 0.12.6.6' gem 'aws-sdk', '~> 2' # TODO: v2 Deprecated, need to upgrade to v3 gem 'rails-observers', '~> 0.1.5' # A feature that removed from core in Rails 4.0, maybe be better migrate away from this. -# Gems used for assets -gem 'sass-rails', '5.0.7' # TODO: may need to upgrade when upgrade to Rails 5 or 6 (https://github.com/rails/sass-rails/releases) -gem 'coffee-rails', '4.2.2' # TODO: v5 support Rails 6 +# Use SCSS for stylesheets +gem 'sass-rails', '~> 5.0' +# Use Uglifier as compressor for JavaScript assets +gem 'uglifier', '>= 1.3.0' +# Use CoffeeScript for .coffee assets and views +gem 'coffee-rails', '~> 4.2' # See https://github.com/sstephenson/execjs#readme for more supported runtimes # gem 'therubyracer', :platforms => :ruby -gem 'uglifier', '2.7.2' # TODO: Only works with ES5. Latest version 4.2.0 @ 2019 gem 'strong_migrations', '~> 0.3.1' # TODO: should upgrade when we upgrade to rails 5 @@ -138,13 +140,13 @@ gem 'geoip', '1.3.5' # TODO: no change logs, no idea if safe to update. Latest v # track who created or edited a given object gem 'clerk', '0.2.3' # TODO: Need update to 1.0.0 when upgrade to Rails 5. I would say should update our code and just use paper_trail. This gem last update at 2018. -gem 'paper_trail', '4.2.0' # TODO: latest is 15.1.0. Need upgrade to v5 for Rails 5. +gem 'paper_trail', '4.2.0' # TODO: latest is 15.1.0. Need upgrade to v6 for Rails 5.1; v9 for Rails 5.2 gem 'dotenv-rails', '2.0.1' -gem 'sitemap_generator', '~> 6.3' # TODO: latest +gem 'sitemap_generator', '~> 6.3' -gem 'appsignal', '1.3.3' +gem 'appsignal', '1.3.3' # TODO: should upgrade to latest after all upgrade. gem 'test-unit', '3.1.5' # annoyingly, rails console won't start without it in staging / production ### GEM for frontend ### @@ -163,10 +165,10 @@ gem 'test-unit', '3.1.5' # annoyingly, rails console won't start without it in s # gem 'jquery-mousewheel-rails', '~> 0.0.9' # gem "font-awesome-rails", '4.5.0.1' -gem 'susy', '2.2.14' # TODO: Deprecated. 2.2.14 is the latest version @ 2018 -gem 'gon', '~> 6.4' # TODO: latest +gem 'susy', '~> 2.2', '>= 2.2.14' # TODO: Deprecated. (https://github.com/oddbird/susy#power-tools-for-the-web-deprecated) +gem 'gon', '~> 6.4' gem "chartkick", '2.3.5' # TODO: latest 5.0.5 @ 2023. Should upgrade to v4 once we upgrade to Rails 5.2+ and Ruby 2.6+ -gem 'nested_form', '0.3.2' # TODO: latest @ 2013. Project is public archived on github. No longer maintained. +gem 'nested_form', '~> 0.3.2' # TODO: Deprecated. (https://github.com/ryanb/nested_form#unmaintained) gem 'bootstrap-sass', '2.3.2.2' # TODO: latest 3.4.1 @ 2019. Can't upgrade unless we sure bootstrap v3 backward compatible with boostrap v2 # Ember diff --git a/Gemfile.lock b/Gemfile.lock index 09bdc1d50..3bd8caf4c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -586,7 +586,7 @@ DEPENDENCIES chartkick (= 2.3.5) clerk (= 0.2.3) codeclimate-test-reporter (= 0.1.1) - coffee-rails (= 4.2.2) + coffee-rails (~> 4.2) coveralls (= 0.7.1) dalli (= 2.7.10) database_cleaner (= 1.2.0) @@ -607,16 +607,16 @@ DEPENDENCIES inherited_resources (= 1.9.0) jslint_on_rails (= 1.1.1) json_spec (= 1.1.5) - kaminari (= 1.2.2) + kaminari (~> 1.2, >= 1.2.2) launchy (= 2.4.3) - nested-hstore (= 0.1.2) - nested_form (= 0.3.2) + nested-hstore (~> 0.1.2) + nested_form (~> 0.3.2) nokogiri (= 1.12.5) oj (= 3.14.2) paper_trail (= 4.2.0) pdfkit (~> 0.8.7.3) pg (= 0.21.0) - pg_array_parser (= 0.0.9) + pg_array_parser (~> 0.0.9) pg_search (= 2.3.0) prawn (= 0.13.2) protected_attributes_continued (= 1.2.4) @@ -633,7 +633,7 @@ DEPENDENCIES rspec-rails (= 3.9.1) rubocop (= 0.40.0) rubyzip (~> 2.3, >= 2.3.2) - sass-rails (= 5.0.7) + sass-rails (~> 5.0) sidekiq (= 4.2.10) sidekiq-status (= 1.1.4) sidekiq-unique-jobs (= 4.0.18) @@ -642,10 +642,10 @@ DEPENDENCIES slackistrano (= 0.1.9) spring strong_migrations (~> 0.3.1) - susy (= 2.2.14) + susy (~> 2.2, >= 2.2.14) test-unit (= 3.1.5) traco (~> 5.3, >= 5.3.3) - uglifier (= 2.7.2) + uglifier (>= 1.3.0) uuidtools (~> 2.2) web-console (~> 2.0) webrick (= 1.3.1) From 7774b25a7e45a8d716290e60566830c90a1e95f4 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Mon, 15 Jan 2024 20:10:21 +0000 Subject: [PATCH 010/241] upgrade paper_trail to v5 latest --- Gemfile | 2 +- Gemfile.lock | 5 ++--- app/controllers/application_controller.rb | 1 + 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index a3ac9a727..96b5b9c61 100644 --- a/Gemfile +++ b/Gemfile @@ -140,7 +140,7 @@ gem 'geoip', '1.3.5' # TODO: no change logs, no idea if safe to update. Latest v # track who created or edited a given object gem 'clerk', '0.2.3' # TODO: Need update to 1.0.0 when upgrade to Rails 5. I would say should update our code and just use paper_trail. This gem last update at 2018. -gem 'paper_trail', '4.2.0' # TODO: latest is 15.1.0. Need upgrade to v6 for Rails 5.1; v9 for Rails 5.2 +gem 'paper_trail', '5.2.3' # TODO: latest is 15.1.0. Need upgrade to v6 for Rails 5.1; v9 for Rails 5.2 gem 'dotenv-rails', '2.0.1' diff --git a/Gemfile.lock b/Gemfile.lock index 3bd8caf4c..85978d3cc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -327,9 +327,8 @@ GEM numerizer (0.1.1) oj (3.14.2) orm_adapter (0.5.0) - paper_trail (4.2.0) + paper_trail (5.2.3) activerecord (>= 3.0, < 6.0) - activesupport (>= 3.0, < 6.0) request_store (~> 1.1) parser (2.7.2.0) ast (~> 2.4.1) @@ -613,7 +612,7 @@ DEPENDENCIES nested_form (~> 0.3.2) nokogiri (= 1.12.5) oj (= 3.14.2) - paper_trail (= 4.2.0) + paper_trail (= 5.2.3) pdfkit (~> 0.8.7.3) pg (= 0.21.0) pg_array_parser (~> 0.0.9) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 204d41073..977700ac7 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -3,6 +3,7 @@ class ApplicationController < ActionController::Base include SentientController before_filter :set_locale before_filter :configure_permitted_parameters, if: :devise_controller? + before_filter :set_paper_trail_whodunnit rescue_from CanCan::AccessDenied, with: :access_denied_error From 1916ae27c80868c80901f8b812913176a572347f Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Mon, 15 Jan 2024 20:27:48 +0000 Subject: [PATCH 011/241] upgrade pg_search --- app/models/document.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/document.rb b/app/models/document.rb index 92a571a97..31de6644b 100644 --- a/app/models/document.rb +++ b/app/models/document.rb @@ -25,7 +25,7 @@ # class Document < ActiveRecord::Base - include PgSearch + include PgSearch::Model pg_search_scope :search_by_title, :against => :title, :using => { :tsearch => { :prefix => true } }, :order_within_rank => "documents.date, documents.title, documents.id" From a30061ade811e7e237d94bfb0425dd33beb9e07e Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Tue, 16 Jan 2024 15:23:59 +0000 Subject: [PATCH 012/241] We use Sentient in User model, so don't relay on clerk's dependencies, we should add this to our Gemfile. --- Gemfile | 3 ++- Gemfile.lock | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 96b5b9c61..a96eebd19 100644 --- a/Gemfile +++ b/Gemfile @@ -18,7 +18,7 @@ gem 'nested-hstore', '~> 0.1.2' gem 'pg_search', '2.3.0' # TODO: can upgrade to newer version after Rails 5 gem 'oj', '3.14.2' # optimised JSON (picked by multi_json) # TODO: to upgrade to newer version, need >=Ruby 2.7 gem 'nokogiri', '1.12.5' # TODO: 1.12.5 is the last version support 2.5. New version need Ruby 2.6+ -gem 'inherited_resources', '1.9.0' # TODO: need upgrade when upgrade to Rails 6 +gem 'inherited_resources', '1.9.0' # Deprecated (https://github.com/activeadmin/inherited_resources#notice) # TODO: need upgrade when upgrade to Rails 6 gem 'traco', '~> 5.3', '>= 5.3.3' # TODO: latest version @ 2021. Suggest migrate to Mobility gem. gem 'protected_attributes_continued', '1.2.4' # TODO: upgrade to latest after we successfully upgrade Rails to 5. gem 'devise', '4.4.3' # TODO: version 4.4.3 work under <=Rails 5.3 and <=Ruby 2.6 @@ -140,6 +140,7 @@ gem 'geoip', '1.3.5' # TODO: no change logs, no idea if safe to update. Latest v # track who created or edited a given object gem 'clerk', '0.2.3' # TODO: Need update to 1.0.0 when upgrade to Rails 5. I would say should update our code and just use paper_trail. This gem last update at 2018. +gem 'sentient_user', '0.4.0' gem 'paper_trail', '5.2.3' # TODO: latest is 15.1.0. Need upgrade to v6 for Rails 5.1; v9 for Rails 5.2 gem 'dotenv-rails', '2.0.1' diff --git a/Gemfile.lock b/Gemfile.lock index 85978d3cc..fefa655c7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -633,6 +633,7 @@ DEPENDENCIES rubocop (= 0.40.0) rubyzip (~> 2.3, >= 2.3.2) sass-rails (~> 5.0) + sentient_user (= 0.4.0) sidekiq (= 4.2.10) sidekiq-status (= 1.1.4) sidekiq-unique-jobs (= 4.0.18) From 736ae76028d53e8c39ad842c91081036769ffd27 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Tue, 16 Jan 2024 15:26:51 +0000 Subject: [PATCH 013/241] add deprecated message for `update_attributes` --- .../admin/taxon_concept_comments_controller.rb | 2 +- app/controllers/admin/users_controller.rb | 2 +- app/controllers/trade/sandbox_shipments_controller.rb | 2 +- app/controllers/trade/shipments_controller.rb | 2 +- app/controllers/trade/validation_errors_controller.rb | 2 +- app/models/event.rb | 4 ++-- app/models/iucn_mapping_manager.rb | 2 +- app/models/nomenclature_change/full_reassignment.rb | 6 +++--- .../nomenclature_change/input_taxon_concept_processor.rb | 2 +- .../to_accepted_name_transformation.rb | 2 +- .../nomenclature_change/to_synonym_transformation.rb | 2 +- app/models/trade/annual_report_upload.rb | 2 +- app/models/trade/validation_rule.rb | 2 +- app/workers/changes_history_generator_worker.rb | 2 +- app/workers/submission_worker.rb | 4 ++-- lib/tasks/import_countries.rake | 2 +- lib/tasks/import_orchids_T_to_A.rake | 2 +- lib/tasks/import_trade_codes.rake | 2 +- lib/tasks/import_trade_names.rake | 4 ++-- lib/tasks/trade_db_resolve_ip_to_country.rake | 2 +- .../trade/annual_report_uploads_controller_spec.rb | 2 +- spec/models/designation_spec.rb | 8 ++++---- spec/models/document_search_spec.rb | 4 ++-- spec/models/document_spec.rb | 4 ++-- spec/models/geo_entity_search_spec.rb | 2 +- spec/models/taxonomy_spec.rb | 4 ++-- spec/models/trade/inclusion_validation_rule_spec.rb | 6 +++--- spec/models/trade/sandbox_template_spec.rb | 2 +- spec/models/trade/validation_rule_spec.rb | 6 +++--- 29 files changed, 44 insertions(+), 44 deletions(-) diff --git a/app/controllers/admin/taxon_concept_comments_controller.rb b/app/controllers/admin/taxon_concept_comments_controller.rb index e9a5aab1d..fd925381f 100644 --- a/app/controllers/admin/taxon_concept_comments_controller.rb +++ b/app/controllers/admin/taxon_concept_comments_controller.rb @@ -23,7 +23,7 @@ def create def update @taxon_concept = TaxonConcept.find(params[:taxon_concept_id]) @comment = @taxon_concept.comments.find(params[:id]) - @comment.update_attributes(params[:comment]) + @comment.update_attributes(params[:comment]) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. redirect_to admin_taxon_concept_comments_url(@taxon_concept), notice: 'Operation succeeded' end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 899ac1cee..a5de1871c 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -21,7 +21,7 @@ def update if params[:user][:password].blank? @user.update_without_password(params[:user]) else - @user.update_attributes(params[:user]) + @user.update_attributes(params[:user]) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. end respond_to do |format| format.js { diff --git a/app/controllers/trade/sandbox_shipments_controller.rb b/app/controllers/trade/sandbox_shipments_controller.rb index ecbd3d865..1620fd683 100644 --- a/app/controllers/trade/sandbox_shipments_controller.rb +++ b/app/controllers/trade/sandbox_shipments_controller.rb @@ -16,7 +16,7 @@ def update aru = Trade::AnnualReportUpload.find(params[:annual_report_upload_id]) sandbox_klass = Trade::SandboxTemplate.ar_klass(aru.sandbox.table_name) @sandbox_shipment = sandbox_klass.find(params[:id]) - @sandbox_shipment.update_attributes(sandbox_shipment_params) + @sandbox_shipment.update_attributes(sandbox_shipment_params) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. head :no_content end diff --git a/app/controllers/trade/shipments_controller.rb b/app/controllers/trade/shipments_controller.rb index 2aa8d485f..818678ce1 100644 --- a/app/controllers/trade/shipments_controller.rb +++ b/app/controllers/trade/shipments_controller.rb @@ -20,7 +20,7 @@ def create def update @shipment = Trade::Shipment.find(params[:id]) update_params = populate_accepted_taxon_concept(shipment_params) - if @shipment.update_attributes(update_params) + if @shipment.update_attributes(update_params) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. render :json => @shipment, :status => :ok else render :json => { "errors" => @shipment.errors }, :status => :unprocessable_entity diff --git a/app/controllers/trade/validation_errors_controller.rb b/app/controllers/trade/validation_errors_controller.rb index 5ec095a5b..6a14e82e1 100644 --- a/app/controllers/trade/validation_errors_controller.rb +++ b/app/controllers/trade/validation_errors_controller.rb @@ -8,7 +8,7 @@ def show def update @validation_error = Trade::ValidationError.find(params[:id]) - if @validation_error.update_attributes(validation_error_params) + if @validation_error.update_attributes(validation_error_params) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. render json: @validation_error, status: :ok else render json: { 'errors' => @validation_error.errors }, diff --git a/app/models/event.rb b/app/models/event.rb index 5d0bd51eb..6cc8c9b46 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -106,11 +106,11 @@ def self.search(query) end def activate! - update_attributes(:is_current => true) + update_attributes(:is_current => true) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. end def deactivate! - update_attributes(:is_current => false) + update_attributes(:is_current => false) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. end protected diff --git a/app/models/iucn_mapping_manager.rb b/app/models/iucn_mapping_manager.rb index ba6aaeab8..c847d1d5a 100644 --- a/app/models/iucn_mapping_manager.rb +++ b/app/models/iucn_mapping_manager.rb @@ -42,7 +42,7 @@ def map_taxon_concept(taxon_concept, map, data) begin match = data["result"].first puts "#{taxon_concept.full_name} #{taxon_concept.author_year} <=> #{match["scientific_name"]} #{match["authority"]}" - map.update_attributes( + map.update_attributes( # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. :iucn_taxon_name => match['scientific_name'], :iucn_taxon_id => match['taxonid'], :iucn_author => match['authority'], diff --git a/app/models/nomenclature_change/full_reassignment.rb b/app/models/nomenclature_change/full_reassignment.rb index b03e1cc67..88ea5aa7e 100644 --- a/app/models/nomenclature_change/full_reassignment.rb +++ b/app/models/nomenclature_change/full_reassignment.rb @@ -41,14 +41,14 @@ def process Rails.logger.debug "FULL REASSIGNMENT Document Citations (#{@old_taxon_concept.document_citation_taxon_concepts.count})" # need validations to be applied to avoid duplicates exception @old_taxon_concept.document_citation_taxon_concepts.each do |dctc| - dctc.update_attributes(update_attrs) + dctc.update_attributes(update_attrs) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. end # shipments Rails.logger.debug "FULL REASSIGNMENT Shipments" Trade::Shipment.where(taxon_concept_id: @old_taxon_concept.id).update_all(update_attrs) - @old_taxon_concept.update_attributes(dependents_updated_at: update_timestamp) + @old_taxon_concept.update_attributes(dependents_updated_at: update_timestamp) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. @old_taxon_concept.reload - @new_taxon_concept.update_attributes(dependents_updated_at: update_timestamp) + @new_taxon_concept.update_attributes(dependents_updated_at: update_timestamp) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. end end diff --git a/app/models/nomenclature_change/input_taxon_concept_processor.rb b/app/models/nomenclature_change/input_taxon_concept_processor.rb index ef0616ecf..c23e1c10a 100644 --- a/app/models/nomenclature_change/input_taxon_concept_processor.rb +++ b/app/models/nomenclature_change/input_taxon_concept_processor.rb @@ -8,7 +8,7 @@ def run return false unless @input.taxon_concept Rails.logger.debug("Processing input #{@input.taxon_concept.full_name}") tc = @input.taxon_concept - tc.update_attributes( + tc.update_attributes( # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. nomenclature_note_en: "#{tc.nomenclature_note_en} #{@input.note_en}", nomenclature_note_es: "#{tc.nomenclature_note_es} #{@input.note_es}", nomenclature_note_fr: "#{tc.nomenclature_note_fr} #{@input.note_fr}" diff --git a/app/models/nomenclature_change/to_accepted_name_transformation.rb b/app/models/nomenclature_change/to_accepted_name_transformation.rb index f6a195573..716392896 100644 --- a/app/models/nomenclature_change/to_accepted_name_transformation.rb +++ b/app/models/nomenclature_change/to_accepted_name_transformation.rb @@ -13,7 +13,7 @@ def process @non_accepted_taxon_concept.inverse_trade_name_relationships ) - @non_accepted_taxon_concept.update_attributes( + @non_accepted_taxon_concept.update_attributes( # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. parent_id: @new_parent.id, name_status: 'A' ) diff --git a/app/models/nomenclature_change/to_synonym_transformation.rb b/app/models/nomenclature_change/to_synonym_transformation.rb index 5ce40788a..4edc3da2f 100644 --- a/app/models/nomenclature_change/to_synonym_transformation.rb +++ b/app/models/nomenclature_change/to_synonym_transformation.rb @@ -27,7 +27,7 @@ def process def relink_relationships(relationships, new_taxon_concept) relationships.includes(:taxon_concept, :taxon_relationship_type).each do |rel| Rails.logger.debug "Relinking #{rel.taxon_relationship_type.name} relationship from #{rel.taxon_concept.full_name} to #{new_taxon_concept.full_name}" - rel.update_attributes(taxon_concept_id: new_taxon_concept.id) + rel.update_attributes(taxon_concept_id: new_taxon_concept.id) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. end end diff --git a/app/models/trade/annual_report_upload.rb b/app/models/trade/annual_report_upload.rb index 5a7f3f6a7..456a1c910 100644 --- a/app/models/trade/annual_report_upload.rb +++ b/app/models/trade/annual_report_upload.rb @@ -94,7 +94,7 @@ def submit(submitter) DownloadsCacheCleanupWorker.perform_async(:shipments) # flag as submitted - update_attributes({ + update_attributes({ # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. submitted_at: DateTime.now, submitted_by_id: submitter.id, number_of_records_submitted: records_submitted diff --git a/app/models/trade/validation_rule.rb b/app/models/trade/validation_rule.rb index 7b8d5d52c..957b8aec9 100644 --- a/app/models/trade/validation_rule.rb +++ b/app/models/trade/validation_rule.rb @@ -105,7 +105,7 @@ def update_or_create_error_record(annual_report_upload, existing_record, error_c if error_count == 0 existing_record.destroy else - existing_record.update_attributes( + existing_record.update_attributes( # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. error_count: error_count ) end diff --git a/app/workers/changes_history_generator_worker.rb b/app/workers/changes_history_generator_worker.rb index c26e72d57..426bd039f 100644 --- a/app/workers/changes_history_generator_worker.rb +++ b/app/workers/changes_history_generator_worker.rb @@ -23,7 +23,7 @@ def perform(aru_id, user_id) obj = s3.bucket(bucket_name).object(filename) obj.upload_file(tempfile.path) - aru.update_attributes(aws_storage_path: obj.public_url) + aru.update_attributes(aws_storage_path: obj.public_url) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. rescue Aws::S3::Errors::ServiceError => e Rails.logger.warn "Something went wrong while uploading #{aru.id} to S3" Appsignal.add_exception(e) if defined? Appsignal diff --git a/app/workers/submission_worker.rb b/app/workers/submission_worker.rb index ee3e38374..e1503d0d4 100644 --- a/app/workers/submission_worker.rb +++ b/app/workers/submission_worker.rb @@ -41,7 +41,7 @@ def perform(aru_id, submitter_id) aru.sandbox.destroy # flag as submitted - aru.update_attributes({ + aru.update_attributes({ # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. submitted_at: DateTime.now, submitted_by_id: submitter.id, number_of_records_submitted: records_submitted @@ -62,7 +62,7 @@ def upload_on_S3(aru, tempfile) obj = s3.bucket(bucket_name).object(filename) obj.upload_file(tempfile.path) - aru.update_attributes(aws_storage_path: obj.public_url) + aru.update_attributes(aws_storage_path: obj.public_url) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. rescue Aws::S3::Errors::ServiceError => e Rails.logger.warn "Something went wrong while uploading #{aru.id} to S3" Appsignal.add_exception(e) if defined? Appsignal diff --git a/lib/tasks/import_countries.rake b/lib/tasks/import_countries.rake index dfbd0e552..835c31d7a 100644 --- a/lib/tasks/import_countries.rake +++ b/lib/tasks/import_countries.rake @@ -44,7 +44,7 @@ namespace :import do CSV.foreach("lib/files/country_codes_en_es_fr_utf8.csv") do |row| country = GeoEntity.find_or_initialize_by(iso_code2: row[0].strip.upcase) unless country.id.nil? - country.update_attributes( + country.update_attributes( # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. :name_fr => row[1].strip, :name_es => row[2].strip ) end diff --git a/lib/tasks/import_orchids_T_to_A.rake b/lib/tasks/import_orchids_T_to_A.rake index b02a56a56..babb04cbc 100644 --- a/lib/tasks/import_orchids_T_to_A.rake +++ b/lib/tasks/import_orchids_T_to_A.rake @@ -12,7 +12,7 @@ namespace :import do puts "There was a problem with this Taxon Concept #{row['ID'].strip}" next end - @nomenclature_change.update_attributes(:status => NomenclatureChange::SUBMITTED) + @nomenclature_change.update_attributes(:status => NomenclatureChange::SUBMITTED) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. end end end diff --git a/lib/tasks/import_trade_codes.rake b/lib/tasks/import_trade_codes.rake index 727600541..4abf22e0a 100644 --- a/lib/tasks/import_trade_codes.rake +++ b/lib/tasks/import_trade_codes.rake @@ -6,7 +6,7 @@ namespace :import do current_count = klass.count CSV.foreach("lib/files/#{klass.to_s.downcase}_codes_utf8.csv") do |row| code = klass.find_or_initialize_by(code: row[0].strip.upcase) - code.update_attributes(:name_en => row[1].strip, + code.update_attributes(:name_en => row[1].strip, # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. :name_fr => row[2].strip, :name_es => row[3].strip) end diff --git a/lib/tasks/import_trade_names.rake b/lib/tasks/import_trade_names.rake index 669683a2c..ca068f11b 100644 --- a/lib/tasks/import_trade_names.rake +++ b/lib/tasks/import_trade_names.rake @@ -146,7 +146,7 @@ namespace :import do puts "Updating #{tc.full_name}" unless tc.accepted_names.any? puts "from synonym to trade_name" - tc.update_attributes(:name_status => "T", :parent_id => nil, + tc.update_attributes(:name_status => "T", :parent_id => nil, # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. :legacy_trade_code => cites_code) end puts "Update its children's taxon_name_id" @@ -154,7 +154,7 @@ namespace :import do if child.accepted_names.any? puts "looking at #{child.full_name} scientific_name" taxon_name = TaxonName.find_or_create_by(scientific_name: child.full_name) - child.update_attributes(:parent_id => nil, :taxon_name_id => taxon_name.id) + child.update_attributes(:parent_id => nil, :taxon_name_id => taxon_name.id) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. end end end diff --git a/lib/tasks/trade_db_resolve_ip_to_country.rake b/lib/tasks/trade_db_resolve_ip_to_country.rake index c5dec65c8..66b61d885 100644 --- a/lib/tasks/trade_db_resolve_ip_to_country.rake +++ b/lib/tasks/trade_db_resolve_ip_to_country.rake @@ -5,6 +5,6 @@ task :trade_db_resolve_ip_to_country => :environment do trade_downloads.each do |td| puts "Updating Trade download #{td.id}" geo_ip_data = Sapi::GeoIP.instance.resolve(td.user_ip) - td.update_attributes!(geo_ip_data) + td.update_attributes!(geo_ip_data) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. end end diff --git a/spec/controllers/trade/annual_report_uploads_controller_spec.rb b/spec/controllers/trade/annual_report_uploads_controller_spec.rb index 2572de7de..ac6896cb3 100644 --- a/spec/controllers/trade/annual_report_uploads_controller_spec.rb +++ b/spec/controllers/trade/annual_report_uploads_controller_spec.rb @@ -30,7 +30,7 @@ def exporter_csv @aru.save(:validate => false) @completed_aru = build(:annual_report_upload) @completed_aru.save(:validate => false) - @completed_aru.update_attributes(submitted_at: Time.now) + @completed_aru.update_attributes(submitted_at: Time.now) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. end it "should return all annual report uploads" do get :index, format: :json diff --git a/spec/models/designation_spec.rb b/spec/models/designation_spec.rb index 2c881229e..5c001216c 100644 --- a/spec/models/designation_spec.rb +++ b/spec/models/designation_spec.rb @@ -33,14 +33,14 @@ context "when updating a non-protected name" do let(:designation) { create(:designation) } specify { - expect(designation.update_attributes( + expect(designation.update_attributes( # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. { :name => 'RULES OF INTERGALACTIC TRADE' } )).to be_truthy } end context "when updating a protected name" do specify { - expect(cites.update_attributes( + expect(cites.update_attributes( # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. { :name => 'RULES OF INTERGALACTIC TRADE' } )).to be_falsey } @@ -48,13 +48,13 @@ context "when updating taxonomy with no dependent objects attached" do let(:designation) { create(:designation) } let(:taxonomy) { create(:taxonomy) } - specify { expect(designation.update_attributes(:taxonomy_id => taxonomy.id)).to be_truthy } + specify { expect(designation.update_attributes(:taxonomy_id => taxonomy.id)).to be_truthy } # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. end context "when updating taxonomy with dependent objects attached" do let(:designation) { create(:designation) } let!(:change_type) { create(:change_type, :designation => designation) } let(:taxonomy) { create(:taxonomy) } - specify { expect(designation.update_attributes(:taxonomy_id => taxonomy.id)).to be_falsey } + specify { expect(designation.update_attributes(:taxonomy_id => taxonomy.id)).to be_falsey } # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. end end describe :destroy do diff --git a/spec/models/document_search_spec.rb b/spec/models/document_search_spec.rb index 87ae859b4..244dfb2ad 100644 --- a/spec/models/document_search_spec.rb +++ b/spec/models/document_search_spec.rb @@ -318,7 +318,7 @@ context "when document updated in last #{DocumentSearch::REFRESH_INTERVAL} minutes" do specify do travel_to(Time.now - (DocumentSearch::REFRESH_INTERVAL - 1).minutes) do - @d.update_attributes(is_public: true) + @d.update_attributes(is_public: true) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. end expect(DocumentSearch.documents_need_refreshing?).to be_truthy end @@ -357,7 +357,7 @@ context "when citation updated in last #{DocumentSearch::REFRESH_INTERVAL} minutes" do specify do travel_to(Time.now - (DocumentSearch::REFRESH_INTERVAL - 1).minutes) do - @c_tc.update_attributes(taxon_concept_id: create_cites_eu_species.id) + @c_tc.update_attributes(taxon_concept_id: create_cites_eu_species.id) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. end expect(DocumentSearch.citations_need_refreshing?).to be_truthy end diff --git a/spec/models/document_spec.rb b/spec/models/document_spec.rb index 01e612542..9c7a9b099 100644 --- a/spec/models/document_spec.rb +++ b/spec/models/document_spec.rb @@ -81,13 +81,13 @@ } context "when primary document sort_index_updated" do specify "secondary document sort_index is in sync" do - primary_document.update_attributes(sort_index: 3) + primary_document.update_attributes(sort_index: 3) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. expect(secondary_document.reload.sort_index).to eq(3) end end context "when secondary document sort_index_updated" do specify "primary document sort_index is in sync" do - secondary_document.update_attributes(sort_index: 3) + secondary_document.update_attributes(sort_index: 3) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. expect(primary_document.reload.sort_index).to eq(3) end end diff --git a/spec/models/geo_entity_search_spec.rb b/spec/models/geo_entity_search_spec.rb index 61c4a8419..9ab78db4b 100644 --- a/spec/models/geo_entity_search_spec.rb +++ b/spec/models/geo_entity_search_spec.rb @@ -108,7 +108,7 @@ subject { GeoEntitySearch.new({ geo_entity_types_set: '3' }) } specify do subject.cached_results - @burma.update_attributes({ is_current: false }) + @burma.update_attributes({ is_current: false }) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. expect(subject.cached_results).not_to include(@burma) end end diff --git a/spec/models/taxonomy_spec.rb b/spec/models/taxonomy_spec.rb index f2fd94008..8d55efc45 100644 --- a/spec/models/taxonomy_spec.rb +++ b/spec/models/taxonomy_spec.rb @@ -31,10 +31,10 @@ describe :update do context "when updating a non-protected name" do let(:taxonomy) { create(:taxonomy) } - specify { expect(taxonomy.update_attributes({ :name => 'WORLD OF LOLCATS' })).to be_truthy } + specify { expect(taxonomy.update_attributes({ :name => 'WORLD OF LOLCATS' })).to be_truthy } # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. end context "when updating a protected name" do - specify { expect(cites_eu.update_attributes({ :name => 'WORLD OF LOLCATS' })).to be_falsey } + specify { expect(cites_eu.update_attributes({ :name => 'WORLD OF LOLCATS' })).to be_falsey } # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. end end describe :destroy do diff --git a/spec/models/trade/inclusion_validation_rule_spec.rb b/spec/models/trade/inclusion_validation_rule_spec.rb index e7612dc86..f7c4676a1 100644 --- a/spec/models/trade/inclusion_validation_rule_spec.rb +++ b/spec/models/trade/inclusion_validation_rule_spec.rb @@ -111,8 +111,8 @@ context "when updates and error fixed for all records" do specify "error record is destroyed" do travel_to(Time.now + 1) do - @shipment2.update_attributes(taxon_name: 'Canis lupus') - @shipment3.update_attributes(taxon_name: 'Canis lupus') + @shipment2.update_attributes(taxon_name: 'Canis lupus') # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + @shipment3.update_attributes(taxon_name: 'Canis lupus') # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. expect { validation_rule.refresh_errors_if_needed(annual_report_upload) }.to change { Trade::ValidationError.count }.by(-1) @@ -123,7 +123,7 @@ context "when updates and error fixed for some records" do specify "error record is updated to reflect new error_count" do travel_to(Time.now + 1) do - @shipment2.update_attributes(taxon_name: 'Canis lupus') + @shipment2.update_attributes(taxon_name: 'Canis lupus') # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. expect { validation_rule.refresh_errors_if_needed(annual_report_upload) }.to change { @validation_error.reload.error_count }.by(-1) diff --git a/spec/models/trade/sandbox_template_spec.rb b/spec/models/trade/sandbox_template_spec.rb index 6be016098..dbfb09c2d 100644 --- a/spec/models/trade/sandbox_template_spec.rb +++ b/spec/models/trade/sandbox_template_spec.rb @@ -54,7 +54,7 @@ @shipment1 = sandbox_klass.create(:taxon_name => canis_lupus.full_name) end specify { - @shipment1.update_attributes(:taxon_name => canis_aureus.full_name) + @shipment1.update_attributes(:taxon_name => canis_aureus.full_name) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. expect(@shipment1.reload.taxon_concept_id).to eq(canis_aureus.id) } end diff --git a/spec/models/trade/validation_rule_spec.rb b/spec/models/trade/validation_rule_spec.rb index b236d5859..ed62b7997 100644 --- a/spec/models/trade/validation_rule_spec.rb +++ b/spec/models/trade/validation_rule_spec.rb @@ -101,8 +101,8 @@ context "when updates and error fixed for all records" do specify "error record is destroyed" do travel_to(Time.now + 1) do - @shipment2.update_attributes(taxon_name: 'Canis lupus') - @shipment3.update_attributes(taxon_name: 'Canis lupus') + @shipment2.update_attributes(taxon_name: 'Canis lupus') # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + @shipment3.update_attributes(taxon_name: 'Canis lupus') # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. expect { validation_rule.refresh_errors_if_needed(annual_report_upload) }.to change { Trade::ValidationError.count }.by(-1) @@ -113,7 +113,7 @@ context "when updates and error fixed for some records" do specify "error record is updated to reflect new error_count" do travel_to(Time.now + 1) do - @shipment2.update_attributes(taxon_name: 'Canis lupus') + @shipment2.update_attributes(taxon_name: 'Canis lupus') # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. expect { validation_rule.refresh_errors_if_needed(annual_report_upload) }.to change { @validation_error.reload.error_count }.by(-1) From 0e5f639e8fcf2afb3844b71d5c6f7133b2ea3515 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Wed, 17 Jan 2024 10:03:52 +0000 Subject: [PATCH 014/241] strong params (round 1) --- .../checklist/downloads_controller.rb | 12 ++++++-- app/controllers/registrations_controller.rb | 28 ++++++++++--------- app/controllers/sessions_controller.rb | 10 +++++-- app/controllers/trade/shipments_controller.rb | 2 +- app/workers/download_worker.rb | 2 +- .../registrations_controller_spec.rb | 5 +++- 6 files changed, 39 insertions(+), 20 deletions(-) diff --git a/app/controllers/checklist/downloads_controller.rb b/app/controllers/checklist/downloads_controller.rb index b2dfbeaf4..897d080c5 100644 --- a/app/controllers/checklist/downloads_controller.rb +++ b/app/controllers/checklist/downloads_controller.rb @@ -17,8 +17,8 @@ def index # POST downloads/ def create - @download = Download.create(params[:download]) - if params[:download][:doc_type] == 'citesidmanual' + @download = Download.create(download_params) + if download_params[:doc_type] == 'citesidmanual' ManualDownloadWorker.perform_async(@download.id, params) else DownloadWorker.perform_async(@download.id, params) @@ -93,4 +93,12 @@ def not_found render :json => { error: "No downloads available" } end + def download_params + params.require(:download).permit( + # attributes used in this controller. + :doc_typ, + # other attributes were in model `attr_accessible`. + :format + ) + end end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 86faad5aa..bb931b991 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -3,17 +3,10 @@ def update @user = User.find(current_user.id) successfully_updated = - if needs_password?(@user, params) - # TODO: not sure if this still work, need test. - # TODO: deprecations https://github.com/heartcombo/devise/blob/main/CHANGELOG.md#400rc1---2016-02-01 - @user.update_with_password(devise_parameter_sanitizer.sanitize(:account_update)) + if needs_password? + @user.update_with_password(user_params) else - # remove the virtual current_password attribute - # update_without_password doesn't know how to ignore it - params[:user].delete(:current_password) - # TODO: not sure if this still work, need test. - # TODO: deprecations https://github.com/heartcombo/devise/blob/main/CHANGELOG.md#400rc1---2016-02-01 - @user.update_without_password(devise_parameter_sanitizer.sanitize(:account_update)) + @user.update_without_password(user_params) end if successfully_updated @@ -39,8 +32,17 @@ def after_update_path_for(resource) # check if we need password to update user data # ie if password or email was changed # extend this as needed - def needs_password?(user, params) - user.email != params[:user][:email] || - params[:user][:password].present? + def needs_password? + @user.email != user_params[:email] || user_params[:password].present? + end + + def user_params + params.require(:user).permit( + # attributes needed by Devise and/or used in this controller. + :name, :email, :password, :password_confirmation, :current_password, + # other attributes were in model `attr_accessible`. + :remember_me, :role, :terms_and_conditions, :is_cites_authority, + :organisation, :geo_entity_id, :is_active + ) end end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 769429210..472513568 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -2,10 +2,10 @@ class SessionsController < Devise::SessionsController respond_to :html, :json def create - resource = User.find_for_database_authentication(email: params[:user][:email]) + resource = User.find_for_database_authentication(email: user_params[:email]) return invalid_login_attempt unless resource - if resource.valid_password?(params[:user][:password]) + if resource.valid_password?(user_params[:password]) sign_in :user, resource set_flash_message(:notice, :signed_in) return respond_with resource, location: after_sign_in_path_for(resource) @@ -24,4 +24,10 @@ def invalid_login_attempt format.json { render json: flash[:error], status: 401 } end end + + private + + def user_params + params.require(:user).permit(:email, :password) + end end diff --git a/app/controllers/trade/shipments_controller.rb b/app/controllers/trade/shipments_controller.rb index 818678ce1..87b7f13b6 100644 --- a/app/controllers/trade/shipments_controller.rb +++ b/app/controllers/trade/shipments_controller.rb @@ -37,7 +37,7 @@ def update_batch def destroy @shipment = Trade::Shipment.find(params[:id]) - @shipment.destroy + @shipment.destroy # TODO: didn't check if the destory is successful. render :json => nil, :status => :ok end diff --git a/app/workers/download_worker.rb b/app/workers/download_worker.rb index d095a09ea..b97a61c7c 100644 --- a/app/workers/download_worker.rb +++ b/app/workers/download_worker.rb @@ -22,7 +22,7 @@ def perform(download_id, params) } params = params.symbolize_keys - I18n.locale = params[:locale] # because we're outside of request scope + I18n.locale = params[:locale] # because we're outside of request scope # TODO: should use with_locale with block. document_module = document_modules[@download.doc_type].new(params) @download.path = document_module.generate diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index e28bd4ab9..1bf92dfab 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -16,6 +16,7 @@ :email => @u1.email, :name => 'ZZ' } expect(response).to redirect_to(admin_root_url) + expect(@u1.reload.name).to eq('ZZ') end it "should update password" do sign_in(@u1) @@ -25,13 +26,15 @@ :current_password => '11111111' } expect(response).to redirect_to(admin_root_url) + expect(@u1.reload.valid_password?('22222222')).to eq(true) end it "should not update that account if not valid" do sign_in(@u1) put :update, :id => @u1.id, :user => { - :email => @u1.email, :name => nil + :email => 'another_email@example.com', :name => nil } expect(response).to render_template("edit") + expect(@u1.reload.email).not_to eq('another_email@example.com') end end From 539e879033382b01681193694817e8ea27191b62 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Wed, 17 Jan 2024 10:04:34 +0000 Subject: [PATCH 015/241] Remove broken code --- app/controllers/api/geo_relationships_controller.rb | 12 ------------ config/routes.rb | 2 -- 2 files changed, 14 deletions(-) delete mode 100644 app/controllers/api/geo_relationships_controller.rb diff --git a/app/controllers/api/geo_relationships_controller.rb b/app/controllers/api/geo_relationships_controller.rb deleted file mode 100644 index 600938945..000000000 --- a/app/controllers/api/geo_relationships_controller.rb +++ /dev/null @@ -1,12 +0,0 @@ -class Api::GeoRelationshipsController < ApplicationController - respond_to :json - inherit_resources - - protected - - def collection - @geo_relationship_types ||= end_of_association_chain.order(:name). - select([:id, :name]). - map { |d| { :value => d.id, :text => d.name } } - end -end diff --git a/config/routes.rb b/config/routes.rb index f2787adb4..f2ab7f643 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -55,8 +55,6 @@ resources :species_listings, :only => [:index] resources :change_types, :only => [:index] resources :ranks, :only => [:index] - resources :geo_entities, :only => [:index] - resources :geo_relationship_types, :only => [:index] resources :trade_downloads_cache_cleanup, only: [:index] end namespace :admin do From a1aaded5bdfa8826c0028134e96405eb15520547 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Wed, 17 Jan 2024 12:05:44 +0000 Subject: [PATCH 016/241] found include SentientUser and track_who_does_it in Clerk gem README, so safe to remove the dependancy gem in our Gemfile. --- Gemfile | 1 - Gemfile.lock | 1 - 2 files changed, 2 deletions(-) diff --git a/Gemfile b/Gemfile index a96eebd19..f4f25bb45 100644 --- a/Gemfile +++ b/Gemfile @@ -140,7 +140,6 @@ gem 'geoip', '1.3.5' # TODO: no change logs, no idea if safe to update. Latest v # track who created or edited a given object gem 'clerk', '0.2.3' # TODO: Need update to 1.0.0 when upgrade to Rails 5. I would say should update our code and just use paper_trail. This gem last update at 2018. -gem 'sentient_user', '0.4.0' gem 'paper_trail', '5.2.3' # TODO: latest is 15.1.0. Need upgrade to v6 for Rails 5.1; v9 for Rails 5.2 gem 'dotenv-rails', '2.0.1' diff --git a/Gemfile.lock b/Gemfile.lock index fefa655c7..85978d3cc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -633,7 +633,6 @@ DEPENDENCIES rubocop (= 0.40.0) rubyzip (~> 2.3, >= 2.3.2) sass-rails (~> 5.0) - sentient_user (= 0.4.0) sidekiq (= 4.2.10) sidekiq-status (= 1.1.4) sidekiq-unique-jobs (= 4.0.18) From 82c04aa858b301d137f45cdd60685de63e8316c5 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Wed, 17 Jan 2024 18:25:03 +0000 Subject: [PATCH 017/241] strong params (round 2) --- .../admin/change_types_controller.rb | 8 +++++++ app/controllers/admin/cites_acs_controller.rb | 11 +++++++++ .../cites_captivity_processes_controller.rb | 11 +++++++++ .../admin/cites_cops_controller.rb | 11 +++++++++ ...cites_extraordinary_meetings_controller.rb | 11 +++++++++ .../cites_hash_annotations_controller.rb | 10 ++++++++ app/controllers/admin/cites_pcs_controller.rb | 11 +++++++++ ...tes_suspension_notifications_controller.rb | 11 +++++++++ .../admin/cites_suspensions_controller.rb | 17 ++++++++++++++ app/controllers/admin/cites_tcs_controller.rb | 11 +++++++++ .../admin/designations_controller.rb | 9 ++++++++ .../admin/distributions_controller.rb | 10 ++++++++ app/controllers/admin/documents_controller.rb | 16 +++++++++++++ app/controllers/admin/ec_srgs_controller.rb | 11 +++++++++ .../eu_council_regulations_controller.rb | 11 +++++++++ .../admin/eu_decision_types_controller.rb | 9 ++++++++ .../admin/eu_hash_annotations_controller.rb | 10 ++++++++ .../eu_implementing_regulations_controller.rb | 11 +++++++++ .../admin/eu_opinions_controller.rb | 13 +++++++++++ .../admin/eu_regulations_controller.rb | 11 +++++++++ .../eu_suspension_regulations_controller.rb | 11 +++++++++ app/controllers/admin/events_controller.rb | 11 +++++++++ .../admin/geo_entities_controller.rb | 11 +++++++++ .../admin/geo_relationships_controller.rb | 8 +++++++ .../admin/hybrid_relationships_controller.rb | 9 ++++++++ .../admin/instruments_controller.rb | 9 ++++++++ app/controllers/admin/languages_controller.rb | 9 ++++++++ app/controllers/admin/purposes_controller.rb | 9 ++++++++ app/controllers/admin/ranks_controller.rb | 9 ++++++++ .../admin/references_controller.rb | 9 ++++++++ app/controllers/admin/sources_controller.rb | 9 ++++++++ .../admin/species_listings_controller.rb | 8 +++++++ .../admin/srg_histories_controller.rb | 9 ++++++++ .../admin/synonym_relationships_controller.rb | 9 ++++++++ app/controllers/admin/tags_controller.rb | 9 ++++++++ .../taxon_cites_suspensions_controller.rb | 18 +++++++++++++++ .../admin/taxon_commons_controller.rb | 10 ++++++++ .../taxon_concept_comments_controller.rb | 14 +++++++++-- .../taxon_concept_references_controller.rb | 11 +++++++++ .../taxon_concept_term_pairs_controller.rb | 9 ++++++++ .../admin/taxon_concepts_controller.rb | 14 +++++++++++ .../admin/taxon_eu_suspensions_controller.rb | 13 +++++++++++ .../admin/taxon_instruments_controller.rb | 11 ++++++++- .../admin/taxon_listing_changes_controller.rb | 23 ++++++++++++++++++- .../admin/taxon_quotas_controller.rb | 13 +++++++++++ .../admin/taxon_relationships_controller.rb | 11 ++++++++- .../admin/taxonomies_controller.rb | 6 +++++ .../term_trade_codes_pairs_controller.rb | 8 +++++++ app/controllers/admin/terms_controller.rb | 6 +++++ .../trade_name_relationships_controller.rb | 9 ++++++++ app/controllers/admin/units_controller.rb | 8 +++++++ app/controllers/admin/users_controller.rb | 10 ++++++++ app/models/annotation.rb | 6 ++--- app/models/change_type.rb | 4 ++-- app/models/cites_ac.rb | 2 +- app/models/cites_cop.rb | 2 +- app/models/cites_extraordinary_meeting.rb | 2 +- app/models/cites_pc.rb | 2 +- app/models/cites_process.rb | 6 ++--- app/models/cites_suspension.rb | 6 ++--- app/models/cites_suspension_confirmation.rb | 2 +- app/models/cites_suspension_notification.rb | 2 +- app/models/cites_tc.rb | 2 +- app/models/cms_mapping.rb | 2 +- app/models/comment.rb | 4 ++-- app/models/designation.rb | 2 +- app/models/distribution.rb | 4 ++-- app/models/document.rb | 10 ++++---- app/models/document_citation.rb | 2 +- app/models/ec_srg.rb | 2 +- app/models/eu_decision.rb | 10 ++++---- app/models/eu_decision_type.rb | 2 +- app/models/eu_opinion.rb | 2 +- app/models/eu_regulation.rb | 2 +- app/models/eu_suspension_regulation.rb | 2 +- app/models/event.rb | 8 ++++--- app/models/geo_entity.rb | 6 ++--- app/models/geo_relationship.rb | 2 +- app/models/instrument.rb | 2 +- app/models/language.rb | 2 +- app/models/listing_change.rb | 14 +++++------ app/models/listing_distribution.rb | 2 +- app/models/preset_tag.rb | 2 +- app/models/quota.rb | 2 +- app/models/rank.rb | 4 ++-- app/models/reference.rb | 2 +- app/models/species_listing.rb | 2 +- app/models/srg_history.rb | 2 +- app/models/taxon_common.rb | 4 ++-- app/models/taxon_concept.rb | 14 +++++------ app/models/taxon_concept_reference.rb | 8 +++---- app/models/taxon_instrument.rb | 2 +- app/models/taxon_relationship.rb | 4 ++-- app/models/taxonomy.rb | 2 +- app/models/term_trade_codes_pair.rb | 2 +- app/models/trade/taxon_concept_term_pair.rb | 2 +- app/models/trade_code.rb | 3 +-- app/models/trade_restriction.rb | 12 +++++----- app/models/user.rb | 6 ++--- .../_form.html.erb | 2 +- app/views/admin/cites_pcs/_form.html.erb | 2 +- app/views/admin/cites_tcs/_form.html.erb | 2 +- app/views/admin/taxon_commons/_form.html.erb | 4 ++-- .../admin/taxon_instruments/_list.html.erb | 2 +- .../admin/taxon_relationships/_form.html.erb | 2 +- config/routes.rb | 2 +- 106 files changed, 654 insertions(+), 107 deletions(-) diff --git a/app/controllers/admin/change_types_controller.rb b/app/controllers/admin/change_types_controller.rb index b48233155..212358747 100644 --- a/app/controllers/admin/change_types_controller.rb +++ b/app/controllers/admin/change_types_controller.rb @@ -12,4 +12,12 @@ def load_associations @designations = Designation.order(:name) end + private + + def change_type_params + params.require(:change_type).permit( + # attributes were in model `attr_accessible`. + :name, :display_name_en, :display_name_es, :display_name_fr, :designation_id + ) + end end diff --git a/app/controllers/admin/cites_acs_controller.rb b/app/controllers/admin/cites_acs_controller.rb index 6ea81d424..1a228163b 100644 --- a/app/controllers/admin/cites_acs_controller.rb +++ b/app/controllers/admin/cites_acs_controller.rb @@ -12,4 +12,15 @@ def collection page(params[:page]). search(params[:query]) end + + private + + def cites_ac_params + params.require(:cites_ac).permit( + # attributes were in model `attr_accessible`. + :is_current, :name, :designation_id, :description, :extended_description, + :url, :private_url, :multilingual_url, :published_at, :effective_at, :is_current, :end_date, + :created_by_id, :updated_by_id + ) + end end diff --git a/app/controllers/admin/cites_captivity_processes_controller.rb b/app/controllers/admin/cites_captivity_processes_controller.rb index 1f3e86caf..956df325a 100644 --- a/app/controllers/admin/cites_captivity_processes_controller.rb +++ b/app/controllers/admin/cites_captivity_processes_controller.rb @@ -48,4 +48,15 @@ def collection geo_entities.name_en ASC'). page(params[:page]) end + + private + + def cites_captivity_process_params + params.require(:cites_captivity_process).permit( + # attributes were in model `attr_accessible`. + :start_event_id, :geo_entity_id, :resolution, :start_date, + :taxon_concept_id, :notes, :status, :document, :document_title, + :created_by_id, :updated_by_id + ) + end end diff --git a/app/controllers/admin/cites_cops_controller.rb b/app/controllers/admin/cites_cops_controller.rb index d20a8ab00..72bde8fbe 100644 --- a/app/controllers/admin/cites_cops_controller.rb +++ b/app/controllers/admin/cites_cops_controller.rb @@ -11,4 +11,15 @@ def collection page(params[:page]). search(params[:query]) end + + private + + def cites_cop_params + params.require(:cites_cop).permit( + # attributes were in model `attr_accessible`. + :is_current, :name, :designation_id, :description, :extended_description, + :url, :private_url, :multilingual_url, :published_at, :effective_at, :is_current, :end_date, + :created_by_id, :updated_by_id + ) + end end diff --git a/app/controllers/admin/cites_extraordinary_meetings_controller.rb b/app/controllers/admin/cites_extraordinary_meetings_controller.rb index 66ee461d4..11a613a35 100644 --- a/app/controllers/admin/cites_extraordinary_meetings_controller.rb +++ b/app/controllers/admin/cites_extraordinary_meetings_controller.rb @@ -12,4 +12,15 @@ def collection page(params[:page]). search(params[:query]) end + + private + + def cites_extraordinary_meeting_params + params.require(:cites_extraordinary_meeting).permit( + # attributes were in model `attr_accessible`. + :is_current, :name, :designation_id, :description, :extended_description, + :url, :private_url, :multilingual_url, :published_at, :effective_at, :is_current, :end_date, + :created_by_id, :updated_by_id + ) + end end diff --git a/app/controllers/admin/cites_hash_annotations_controller.rb b/app/controllers/admin/cites_hash_annotations_controller.rb index 8af4f2ef7..622362d32 100644 --- a/app/controllers/admin/cites_hash_annotations_controller.rb +++ b/app/controllers/admin/cites_hash_annotations_controller.rb @@ -9,4 +9,14 @@ def load_associations @events = CitesCop.order(:effective_at) end + private + + def cites_hash_annotation_params + params.require(:annotation).permit( + # attributes were in model `attr_accessible`. + :listing_change_id, :symbol, :parent_symbol, :short_note_en, + :full_note_en, :short_note_fr, :full_note_fr, :short_note_es, :full_note_es, + :display_in_index, :display_in_footnote, :event_id + ) + end end diff --git a/app/controllers/admin/cites_pcs_controller.rb b/app/controllers/admin/cites_pcs_controller.rb index 846208760..d15b3059d 100644 --- a/app/controllers/admin/cites_pcs_controller.rb +++ b/app/controllers/admin/cites_pcs_controller.rb @@ -12,4 +12,15 @@ def collection page(params[:page]). search(params[:query]) end + + private + + def cites_pc_params + params.require(:cites_pc).permit( + # attributes were in model `attr_accessible`. + :is_current, :name, :designation_id, :description, :extended_description, + :url, :private_url, :multilingual_url, :published_at, :effective_at, :is_current, :end_date, + :created_by_id, :updated_by_id + ) + end end diff --git a/app/controllers/admin/cites_suspension_notifications_controller.rb b/app/controllers/admin/cites_suspension_notifications_controller.rb index ff373999b..769a1f9bb 100644 --- a/app/controllers/admin/cites_suspension_notifications_controller.rb +++ b/app/controllers/admin/cites_suspension_notifications_controller.rb @@ -12,4 +12,15 @@ def collection page(params[:page]). search(params[:query]) end + + private + + def cites_suspension_notification_params + params.require(:cites_suspension_notification).permit( + # attributes were in model `attr_accessible`. + :subtype, :new_subtype, :end_date, :name, :designation_id, :description, :extended_description, + :url, :private_url, :multilingual_url, :published_at, :effective_at, :is_current, :end_date, + :created_by_id, :updated_by_id + ) + end end diff --git a/app/controllers/admin/cites_suspensions_controller.rb b/app/controllers/admin/cites_suspensions_controller.rb index da0e7af9b..4bcce98eb 100644 --- a/app/controllers/admin/cites_suspensions_controller.rb +++ b/app/controllers/admin/cites_suspensions_controller.rb @@ -49,4 +49,21 @@ def collection @cites_suspensions ||= end_of_association_chain.order('start_date DESC'). page(params[:page]).search(params[:query]) end + + private + + def cites_suspension_params + params.require(:cites_suspension).permit( + # attributes were in model `attr_accessible`. + :start_notification_id, :end_notification_id, + :applies_to_import, :end_date, :geo_entity_id, :is_current, + :notes, :publication_date, :purpose_ids, :quota, :type, + :source_ids, :start_date, :term_ids, :unit_id, :internal_notes, + :nomenclature_note_en, :nomenclature_note_es, :nomenclature_note_fr, + :created_by_id, :updated_by_id, :url, + :taxon_concept_id, cites_suspension_confirmations_attributes: [ + :id, :cites_suspension_notification_id, :_destroy + ] + ) + end end diff --git a/app/controllers/admin/cites_tcs_controller.rb b/app/controllers/admin/cites_tcs_controller.rb index 005ecc658..938597ae1 100644 --- a/app/controllers/admin/cites_tcs_controller.rb +++ b/app/controllers/admin/cites_tcs_controller.rb @@ -12,4 +12,15 @@ def collection page(params[:page]). search(params[:query]) end + + private + + def cites_tc_params + params.require(:cites_tc).permit( + # attributes were in model `attr_accessible`. + :is_current, :name, :designation_id, :description, :extended_description, + :url, :private_url, :multilingual_url, :published_at, :effective_at, :is_current, :end_date, + :created_by_id, :updated_by_id + ) + end end diff --git a/app/controllers/admin/designations_controller.rb b/app/controllers/admin/designations_controller.rb index 0860b30a3..63ed83673 100644 --- a/app/controllers/admin/designations_controller.rb +++ b/app/controllers/admin/designations_controller.rb @@ -23,4 +23,13 @@ def collection def load_associations @taxonomies = Taxonomy.order(:name) end + + private + + def designation_params + params.require(:designation).permit( + # attributes were in model `attr_accessible`. + :name, :taxonomy_id + ) + end end diff --git a/app/controllers/admin/distributions_controller.rb b/app/controllers/admin/distributions_controller.rb index 20b3930bd..75672b1e6 100644 --- a/app/controllers/admin/distributions_controller.rb +++ b/app/controllers/admin/distributions_controller.rb @@ -72,4 +72,14 @@ def load_distributions @distributions = @taxon_concept.distributions. joins(:geo_entity).order('geo_entities.name_en ASC') end + + private + + def distribution_params + params.require(:distribution).permit( + # attributes were in model `attr_accessible`. + :geo_entity_id, :taxon_concept_id, :tag_list, :internal_notes, :created_by_id, :updated_by_id, + references_attributes: [:citation, :created_by_id, :updated_by_id, :id, :_destroy] + ) + end end diff --git a/app/controllers/admin/documents_controller.rb b/app/controllers/admin/documents_controller.rb index 4b034a59e..283eab48f 100644 --- a/app/controllers/admin/documents_controller.rb +++ b/app/controllers/admin/documents_controller.rb @@ -138,4 +138,20 @@ def redirect_url admin_documents_url end end + + private + + def document_params + params.require(:document).permit( + # attributes were in model `attr_accessible`. + :event_id, :filename, :date, :type, :title, :is_public, + :language_id, + :sort_index, :discussion_id, :discussion_sort_index, + :primary_language_document_id, + :designation_id, + citations_attributes: [ + :id, :_destroy, :document_id, :stringy_taxon_concept_ids, :geo_entity_ids + ] + ) + end end diff --git a/app/controllers/admin/ec_srgs_controller.rb b/app/controllers/admin/ec_srgs_controller.rb index 9a057d3ac..0bf191e3c 100644 --- a/app/controllers/admin/ec_srgs_controller.rb +++ b/app/controllers/admin/ec_srgs_controller.rb @@ -12,4 +12,15 @@ def collection page(params[:page]). search(params[:query]) end + + private + + def ec_srg_params + params.require(:ec_srg).permit( + # attributes were in model `attr_accessible`. + :is_current, :name, :designation_id, :description, :extended_description, + :url, :private_url, :multilingual_url, :published_at, :effective_at, :is_current, :end_date, + :created_by_id, :updated_by_id + ) + end end diff --git a/app/controllers/admin/eu_council_regulations_controller.rb b/app/controllers/admin/eu_council_regulations_controller.rb index 7d602fea8..9f2c97f38 100644 --- a/app/controllers/admin/eu_council_regulations_controller.rb +++ b/app/controllers/admin/eu_council_regulations_controller.rb @@ -15,4 +15,15 @@ def collection def list_template 'admin/eu_regulations_common/list' end + + private + + def eu_council_regulation_params + params.require(:eu_council_regulation).permit( + # attributes were in model `attr_accessible`. + :name, :designation_id, :description, :extended_description, + :url, :private_url, :multilingual_url, :published_at, :effective_at, :is_current, :end_date, + :created_by_id, :updated_by_id + ) + end end diff --git a/app/controllers/admin/eu_decision_types_controller.rb b/app/controllers/admin/eu_decision_types_controller.rb index a287115ae..49bd5574a 100644 --- a/app/controllers/admin/eu_decision_types_controller.rb +++ b/app/controllers/admin/eu_decision_types_controller.rb @@ -14,4 +14,13 @@ def collection @eu_decision_types ||= end_of_association_chain.page(params[:page]). order('UPPER(name) ASC') end + + private + + def eu_decision_type_params + params.require(:eu_decision_type).permit( + # attributes were in model `attr_accessible`. + :name, :tooltip, :decision_type + ) + end end diff --git a/app/controllers/admin/eu_hash_annotations_controller.rb b/app/controllers/admin/eu_hash_annotations_controller.rb index 77ee7e6c3..c961f488f 100644 --- a/app/controllers/admin/eu_hash_annotations_controller.rb +++ b/app/controllers/admin/eu_hash_annotations_controller.rb @@ -9,4 +9,14 @@ def load_associations @events = EuRegulation.order(:effective_at) end + private + + def eu_hash_annotation_params + params.require(:annotation).permit( + # attributes were in model `attr_accessible`. + :listing_change_id, :symbol, :parent_symbol, :short_note_en, + :full_note_en, :short_note_fr, :full_note_fr, :short_note_es, :full_note_es, + :display_in_index, :display_in_footnote, :event_id + ) + end end diff --git a/app/controllers/admin/eu_implementing_regulations_controller.rb b/app/controllers/admin/eu_implementing_regulations_controller.rb index fcce342ef..fe0d458ee 100644 --- a/app/controllers/admin/eu_implementing_regulations_controller.rb +++ b/app/controllers/admin/eu_implementing_regulations_controller.rb @@ -15,4 +15,15 @@ def collection def list_template 'admin/eu_regulations_common/list' end + + private + + def eu_implementing_regulation_params + params.require(:eu_implementing_regulation).permit( + # attributes were in model `attr_accessible`. + :name, :designation_id, :description, :extended_description, + :url, :private_url, :multilingual_url, :published_at, :effective_at, :is_current, :end_date, + :created_by_id, :updated_by_id + ) + end end diff --git a/app/controllers/admin/eu_opinions_controller.rb b/app/controllers/admin/eu_opinions_controller.rb index a6232b181..f191d1fd9 100644 --- a/app/controllers/admin/eu_opinions_controller.rb +++ b/app/controllers/admin/eu_opinions_controller.rb @@ -56,4 +56,17 @@ def collection geo_entities.name_en ASC'). page(params[:page]) end + + private + + def eu_opinion_params + params.require(:eu_opinion).permit( + # attributes were in model `attr_accessible`. + :document_id, :end_date, :end_event_id, :geo_entity_id, :internal_notes, + :is_current, :notes, :start_date, :start_event_id, :eu_decision_type_id, + :taxon_concept_id, :type, :conditions_apply, :term_id, :source_id, + :nomenclature_note_en, :nomenclature_note_es, :nomenclature_note_fr, + :created_by_id, :updated_by_id, :srg_history_id + ) + end end diff --git a/app/controllers/admin/eu_regulations_controller.rb b/app/controllers/admin/eu_regulations_controller.rb index 185c8cd96..4aac69d13 100644 --- a/app/controllers/admin/eu_regulations_controller.rb +++ b/app/controllers/admin/eu_regulations_controller.rb @@ -28,4 +28,15 @@ def load_associations @eu_regulations_for_dropdown = EuRegulation. order('effective_at DESC, name ASC') end + + private + + def eu_regulation_params + params.require(:eu_regulation).permit( + # attributes were in model `attr_accessible`. + :listing_changes_event_id, :name, :designation_id, :description, :extended_description, + :url, :private_url, :multilingual_url, :published_at, :effective_at, :is_current, :end_date, + :created_by_id, :updated_by_id + ) + end end diff --git a/app/controllers/admin/eu_suspension_regulations_controller.rb b/app/controllers/admin/eu_suspension_regulations_controller.rb index 1715fd091..cf326ced2 100644 --- a/app/controllers/admin/eu_suspension_regulations_controller.rb +++ b/app/controllers/admin/eu_suspension_regulations_controller.rb @@ -31,4 +31,15 @@ def load_associations @eu_suspension_regulations_for_dropdown = EuSuspensionRegulation. order('effective_at DESC, name ASC') end + + private + + def eu_suspension_regulation_params + params.require(:eu_suspension_regulation).permit( + # attributes were in model `attr_accessible`. + :eu_suspensions_event_id, :name, :designation_id, :description, :extended_description, + :url, :private_url, :multilingual_url, :published_at, :effective_at, :is_current, :end_date, + :created_by_id, :updated_by_id + ) + end end diff --git a/app/controllers/admin/events_controller.rb b/app/controllers/admin/events_controller.rb index 2e88f79bb..e1db9f5f4 100644 --- a/app/controllers/admin/events_controller.rb +++ b/app/controllers/admin/events_controller.rb @@ -43,4 +43,15 @@ def collection def load_associations @designations = Designation.order(:name) end + + private + + def event_params + params.require(:event).permit( + # attributes were in model `attr_accessible`. + :name, :designation_id, :description, :extended_description, + :url, :private_url, :multilingual_url, :published_at, :effective_at, :is_current, :end_date, + :created_by_id, :updated_by_id + ) + end end diff --git a/app/controllers/admin/geo_entities_controller.rb b/app/controllers/admin/geo_entities_controller.rb index 9d38052fa..20b687fe9 100644 --- a/app/controllers/admin/geo_entities_controller.rb +++ b/app/controllers/admin/geo_entities_controller.rb @@ -28,4 +28,15 @@ def collection page(params[:page]). search(params[:query]) end + + private + + def geo_entity_params + params.require(:geo_entity).permit( + # attributes were in model `attr_accessible`. + :geo_entity_type_id, :iso_code2, :iso_code3, + :legacy_id, :legacy_type, :long_name, :name_en, :name_es, :name_fr, + :is_current + ) + end end diff --git a/app/controllers/admin/geo_relationships_controller.rb b/app/controllers/admin/geo_relationships_controller.rb index 750a72bf3..649888d57 100644 --- a/app/controllers/admin/geo_relationships_controller.rb +++ b/app/controllers/admin/geo_relationships_controller.rb @@ -36,4 +36,12 @@ def collection page(params[:page]) end + private + + def geo_relationship_params + params.require(:geo_relationship).permit( + # attributes were in model `attr_accessible`. + :geo_entity_id, :geo_relationship_type_id, :other_geo_entity_id + ) + end end diff --git a/app/controllers/admin/hybrid_relationships_controller.rb b/app/controllers/admin/hybrid_relationships_controller.rb index 0d4ddb168..97c690169 100644 --- a/app/controllers/admin/hybrid_relationships_controller.rb +++ b/app/controllers/admin/hybrid_relationships_controller.rb @@ -63,4 +63,13 @@ def load_hybrid_relationship_type find_by_name(TaxonRelationshipType::HAS_HYBRID) end + private + + def hybrid_relationship_params + params.require(:taxon_relationship).permit( + # attributes were in model `attr_accessible`. + :taxon_concept_id, :other_taxon_concept_id, :taxon_relationship_type_id, + :created_by_id, :updated_by_id + ) + end end diff --git a/app/controllers/admin/instruments_controller.rb b/app/controllers/admin/instruments_controller.rb index 746c93f7c..ad2cd48d3 100644 --- a/app/controllers/admin/instruments_controller.rb +++ b/app/controllers/admin/instruments_controller.rb @@ -22,4 +22,13 @@ def collection def load_associations @designations = Designation.order(:name) end + + private + + def instrument_params + params.require(:instrument).permit( + # attributes were in model `attr_accessible`. + :designation_id, :name + ) + end end diff --git a/app/controllers/admin/languages_controller.rb b/app/controllers/admin/languages_controller.rb index e109c77dc..5d8c2cef6 100644 --- a/app/controllers/admin/languages_controller.rb +++ b/app/controllers/admin/languages_controller.rb @@ -7,4 +7,13 @@ def collection page(params[:page]). search(params[:query]) end + + private + + def language_params + params.require(:language).permit( + # attributes were in model `attr_accessible`. + :iso_code1, :iso_code3, :name_en, :name_fr, :name_es + ) + end end diff --git a/app/controllers/admin/purposes_controller.rb b/app/controllers/admin/purposes_controller.rb index ae88f84eb..a597f6025 100644 --- a/app/controllers/admin/purposes_controller.rb +++ b/app/controllers/admin/purposes_controller.rb @@ -22,4 +22,13 @@ def collection page(params[:page]). search(params[:query]) end + + private + + def purpose_params + params.require(:purpose).permit( + # attributes were in model `attr_accessible`. + :code, :type, :name_en, :name_es, :name_fr + ) + end end diff --git a/app/controllers/admin/ranks_controller.rb b/app/controllers/admin/ranks_controller.rb index da5009b65..f2ed178dc 100644 --- a/app/controllers/admin/ranks_controller.rb +++ b/app/controllers/admin/ranks_controller.rb @@ -5,4 +5,13 @@ class Admin::RanksController < Admin::StandardAuthorizationController def collection @ranks ||= end_of_association_chain.order(:taxonomic_position).page(params[:page]) end + + private + + def rank_params + params.require(:rank).permit( + # attributes were in model `attr_accessible`. + :name, :display_name_en, :display_name_es, :display_name_fr, :taxonomic_position, :fixed_order + ) + end end diff --git a/app/controllers/admin/references_controller.rb b/app/controllers/admin/references_controller.rb index d9db02c3d..a6bca7e54 100644 --- a/app/controllers/admin/references_controller.rb +++ b/app/controllers/admin/references_controller.rb @@ -30,4 +30,13 @@ def collection page(params[:page]). search(params[:query]) end + + private + + def reference_params + params.require(:reference).permit( + # attributes were in model `attr_accessible`. + :citation, :created_by_id, :updated_by_id + ) + end end diff --git a/app/controllers/admin/sources_controller.rb b/app/controllers/admin/sources_controller.rb index 82bff479f..f8546d6a2 100644 --- a/app/controllers/admin/sources_controller.rb +++ b/app/controllers/admin/sources_controller.rb @@ -22,4 +22,13 @@ def collection page(params[:page]). search(params[:query]) end + + private + + def source_params + params.require(:source).permit( + # attributes were in model `attr_accessible`. + :code, :type, :name_en, :name_es, :name_fr + ) + end end diff --git a/app/controllers/admin/species_listings_controller.rb b/app/controllers/admin/species_listings_controller.rb index 158c28e07..491ffa401 100644 --- a/app/controllers/admin/species_listings_controller.rb +++ b/app/controllers/admin/species_listings_controller.rb @@ -13,4 +13,12 @@ def load_associations @designations = Designation.order(:name) end + private + + def species_listing_params + params.require(:species_listing).permit( + # attributes were in model `attr_accessible`. + :designation_id, :name, :abbreviation + ) + end end diff --git a/app/controllers/admin/srg_histories_controller.rb b/app/controllers/admin/srg_histories_controller.rb index 239362257..7298860a6 100644 --- a/app/controllers/admin/srg_histories_controller.rb +++ b/app/controllers/admin/srg_histories_controller.rb @@ -5,4 +5,13 @@ def collection @srg_histories ||= end_of_association_chain.page(params[:page]). order('UPPER(name) ASC') end + + private + + def srg_history_params + params.require(:srg_history).permit( + # attributes were in model `attr_accessible`. + :name, :tooltip + ) + end end diff --git a/app/controllers/admin/synonym_relationships_controller.rb b/app/controllers/admin/synonym_relationships_controller.rb index da6f07f2a..943a533ca 100644 --- a/app/controllers/admin/synonym_relationships_controller.rb +++ b/app/controllers/admin/synonym_relationships_controller.rb @@ -63,4 +63,13 @@ def load_synonym_relationship_type find_by_name(TaxonRelationshipType::HAS_SYNONYM) end + private + + def synonym_relationship_params + params.require(:taxon_relationship).permit( + # attributes were in model `attr_accessible`. + :taxon_concept_id, :other_taxon_concept_id, :taxon_relationship_type_id, + :created_by_id, :updated_by_id + ) + end end diff --git a/app/controllers/admin/tags_controller.rb b/app/controllers/admin/tags_controller.rb index 0ba405df3..ab2eb2b47 100644 --- a/app/controllers/admin/tags_controller.rb +++ b/app/controllers/admin/tags_controller.rb @@ -10,4 +10,13 @@ def collection order('UPPER(name) ASC, model ASC'). search(params[:query]) end + + private + + def tag_params + params.require(:tag).permit( + # attributes were in model `attr_accessible`. + :model, :name + ) + end end diff --git a/app/controllers/admin/taxon_cites_suspensions_controller.rb b/app/controllers/admin/taxon_cites_suspensions_controller.rb index ff38b9747..df0486d59 100644 --- a/app/controllers/admin/taxon_cites_suspensions_controller.rb +++ b/app/controllers/admin/taxon_cites_suspensions_controller.rb @@ -60,4 +60,22 @@ def load_lib_objects def collection @cites_suspensions ||= end_of_association_chain.page(params[:page]) end + + private + + def cites_suspension_params + params.require(:cites_suspension).permit( + # attributes were in model `attr_accessible`. + :start_notification_id, :end_notification_id, + :applies_to_import, :end_date, :geo_entity_id, :is_current, + :notes, :publication_date, :purpose_ids, :quota, :type, + :source_ids, :start_date, :term_ids, :unit_id, :internal_notes, + :nomenclature_note_en, :nomenclature_note_es, :nomenclature_note_fr, + :created_by_id, :updated_by_id, :url, + :taxon_concept_id, + cites_suspension_confirmations_attributes: [ + :cites_suspension_notification_id, :id, :_destroy + ] + ) + end end diff --git a/app/controllers/admin/taxon_commons_controller.rb b/app/controllers/admin/taxon_commons_controller.rb index 5acf7be29..0fd0a01cd 100644 --- a/app/controllers/admin/taxon_commons_controller.rb +++ b/app/controllers/admin/taxon_commons_controller.rb @@ -61,4 +61,14 @@ def destroy def load_associations @languages = Language.order(:name_en) end + + private + + def taxon_common_params + params.require(:taxon_common).permit( + # attributes were in model `attr_accessible`. + :common_name_id, :taxon_concept_id, :created_by_id, + :updated_by_id, :name, :language_id + ) + end end diff --git a/app/controllers/admin/taxon_concept_comments_controller.rb b/app/controllers/admin/taxon_concept_comments_controller.rb index fd925381f..4dbeb5632 100644 --- a/app/controllers/admin/taxon_concept_comments_controller.rb +++ b/app/controllers/admin/taxon_concept_comments_controller.rb @@ -15,7 +15,7 @@ def index def create @taxon_concept = TaxonConcept.find(params[:taxon_concept_id]) - @comment = @taxon_concept.comments.create(params[:comment]) + @comment = @taxon_concept.comments.create(comment_params) redirect_to admin_taxon_concept_comments_url(@taxon_concept), notice: 'Operation succeeded' end @@ -23,8 +23,18 @@ def create def update @taxon_concept = TaxonConcept.find(params[:taxon_concept_id]) @comment = @taxon_concept.comments.find(params[:id]) - @comment.update_attributes(params[:comment]) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + @comment.update_attributes(comment_params) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. redirect_to admin_taxon_concept_comments_url(@taxon_concept), notice: 'Operation succeeded' end + + private + + def comment_params + params.require(:comment).permit( + # attributes were in model `attr_accessible`. + :comment_type, :commentable_id, :commentable_type, :note, + :created_by_id, :updated_by_id + ) + end end diff --git a/app/controllers/admin/taxon_concept_references_controller.rb b/app/controllers/admin/taxon_concept_references_controller.rb index c5aa4d091..48d2e4d40 100644 --- a/app/controllers/admin/taxon_concept_references_controller.rb +++ b/app/controllers/admin/taxon_concept_references_controller.rb @@ -70,4 +70,15 @@ def destroy } end end + + private + + def taxon_concept_reference_params + params.require(:taxon_concept_reference).permit( + # attributes were in model `attr_accessible`. + :reference_id, :taxon_concept_id, :is_standard, :is_cascaded, + :excluded_taxon_concepts_ids, :created_by_id, :updated_by_id, + reference_attributes: [:citation, :created_by_id, :updated_by_id, :id, :_destroy] + ) + end end diff --git a/app/controllers/admin/taxon_concept_term_pairs_controller.rb b/app/controllers/admin/taxon_concept_term_pairs_controller.rb index 03d264271..4325d0249 100644 --- a/app/controllers/admin/taxon_concept_term_pairs_controller.rb +++ b/app/controllers/admin/taxon_concept_term_pairs_controller.rb @@ -18,4 +18,13 @@ def collection page(params[:page]). search(params[:query]) end + + private + + def taxon_concept_term_pair_params + params.require(:taxon_concept_term_pair).permit( + # attributes were in model `attr_accessible`. + :taxon_concept_id, :term_id + ) + end end diff --git a/app/controllers/admin/taxon_concepts_controller.rb b/app/controllers/admin/taxon_concepts_controller.rb index 9dae748f3..dc276da91 100644 --- a/app/controllers/admin/taxon_concepts_controller.rb +++ b/app/controllers/admin/taxon_concepts_controller.rb @@ -134,4 +134,18 @@ def render_new_by_name_status end end + private + + def taxon_concept_params + params.require(:taxon_concept).permit( + # attributes were in model `attr_accessible`. + :parent_id, :taxonomy_id, :rank_id, + :parent_id, :author_year, :taxon_name_id, :taxonomic_position, + :legacy_id, :legacy_type, :scientific_name, :name_status, + :tag_list, :legacy_trade_code, :hybrid_parents_ids, + :accepted_names_ids, :accepted_names_for_trade_name_ids, + :nomenclature_note_en, :nomenclature_note_es, :nomenclature_note_fr, + :created_by_id, :updated_by_id, :dependents_updated_at, :kew_id + ) + end end diff --git a/app/controllers/admin/taxon_eu_suspensions_controller.rb b/app/controllers/admin/taxon_eu_suspensions_controller.rb index 2d61c9aa2..5b7adfd8a 100644 --- a/app/controllers/admin/taxon_eu_suspensions_controller.rb +++ b/app/controllers/admin/taxon_eu_suspensions_controller.rb @@ -75,4 +75,17 @@ def collection geo_entities.name_en ASC'). page(params[:page]) end + + private + + def eu_suspension_params + params.require(:eu_suspension).permit( + # attributes were in model `attr_accessible`. + :end_date, :end_event_id, :geo_entity_id, :internal_notes, + :is_current, :notes, :start_date, :start_event_id, :eu_decision_type_id, + :taxon_concept_id, :type, :conditions_apply, :term_id, :source_id, + :nomenclature_note_en, :nomenclature_note_es, :nomenclature_note_fr, + :created_by_id, :updated_by_id, :srg_history_id + ) + end end diff --git a/app/controllers/admin/taxon_instruments_controller.rb b/app/controllers/admin/taxon_instruments_controller.rb index 8313c0e7d..ce0b9a433 100644 --- a/app/controllers/admin/taxon_instruments_controller.rb +++ b/app/controllers/admin/taxon_instruments_controller.rb @@ -18,7 +18,7 @@ def new def create @taxon_concept = TaxonConcept.find(params[:taxon_concept_id]) - @taxon_instrument = TaxonInstrument.new(params[:taxon_instrument]) + @taxon_instrument = TaxonInstrument.new(taxon_instrument_params) if @taxon_concept.taxon_instruments << @taxon_instrument load_taxon_instruments render 'index' @@ -63,4 +63,13 @@ def load_taxon_instruments includes(:instrument). page(params[:page]) end + + private + + def taxon_instrument_params + params.require(:taxon_instrument).permit( + # attributes were in model `attr_accessible`. + :effective_from, :instrument_id, :taxon_concept_id + ) + end end diff --git a/app/controllers/admin/taxon_listing_changes_controller.rb b/app/controllers/admin/taxon_listing_changes_controller.rb index cb13ddd8b..ffd3a269a 100644 --- a/app/controllers/admin/taxon_listing_changes_controller.rb +++ b/app/controllers/admin/taxon_listing_changes_controller.rb @@ -28,7 +28,7 @@ def new def create @taxon_concept = TaxonConcept.find(params[:taxon_concept_id]) @designation = Designation.find(params[:designation_id]) - @listing_change = ListingChange.new(params[:listing_change]) + @listing_change = ListingChange.new(listing_change_params) if @taxon_concept.listing_changes << @listing_change redirect_to admin_taxon_concept_designation_listing_changes_url(@taxon_concept, @designation) else @@ -129,4 +129,25 @@ def load_listing_changes order('listing_changes.effective_at DESC'). page(params[:page]).where(:parent_id => nil) end + + private + + def listing_change_params + params.require(:listing_change).permit( + :taxon_concept_id, :species_listing_id, :change_type_id, + :effective_at, :is_current, :parent_id, :geo_entity_ids, + :inclusion_taxon_concept_id, :hash_annotation_id, :event_id, + :excluded_geo_entities_ids, :excluded_taxon_concepts_ids, :internal_notes, + :nomenclature_note_en, :nomenclature_note_es, :nomenclature_note_fr, + :created_by_id, :updated_by_id, + annotation_attributes: [ + :listing_change_id, :symbol, :parent_symbol, :short_note_en, + :full_note_en, :short_note_fr, :full_note_fr, :short_note_es, :full_note_es, + :display_in_index, :display_in_footnote, :event_id, :id, :_destroy + ], + party_listing_distribution_attributes: [ + :id, :_destroy, :geo_entity_id, :listing_change_id, :is_party + ] + ) + end end diff --git a/app/controllers/admin/taxon_quotas_controller.rb b/app/controllers/admin/taxon_quotas_controller.rb index bac429999..9e9a91a32 100644 --- a/app/controllers/admin/taxon_quotas_controller.rb +++ b/app/controllers/admin/taxon_quotas_controller.rb @@ -66,4 +66,17 @@ def collection notes ASC'). page(params[:page]) end + + private + + def quota_params + params.require(:quota).permit( + :public_display, :end_date, :geo_entity_id, :is_current, + :notes, :publication_date, :purpose_ids, :quota, :type, + :source_ids, :start_date, :term_ids, :unit_id, :internal_notes, + :nomenclature_note_en, :nomenclature_note_es, :nomenclature_note_fr, + :created_by_id, :updated_by_id, :url, + :taxon_concept_id + ) + end end diff --git a/app/controllers/admin/taxon_relationships_controller.rb b/app/controllers/admin/taxon_relationships_controller.rb index 4a44e418a..8ce083af4 100644 --- a/app/controllers/admin/taxon_relationships_controller.rb +++ b/app/controllers/admin/taxon_relationships_controller.rb @@ -24,7 +24,7 @@ def create # we want it to use the information that comes from the form in the params hash. # We still want the @taxon_concept variable to be instantiated, so we keep using # the *belongs_to* helper for that. - @taxon_relationship = TaxonRelationship.new(params[:taxon_relationship]) + @taxon_relationship = TaxonRelationship.new(taxon_relationship_params) create! do |success, failure| success.js { render 'create' } @@ -70,4 +70,13 @@ def collection where(:"taxon_relationship_types.name" => @taxon_relationship_type.name). page(params[:page]) end + + private + + def taxon_relationship_params + params.require(:taxon_relationship).permit( + :taxon_concept_id, :other_taxon_concept_id, :taxon_relationship_type_id, + :created_by_id, :updated_by_id + ) + end end diff --git a/app/controllers/admin/taxonomies_controller.rb b/app/controllers/admin/taxonomies_controller.rb index 33d1ef7ec..06aac0c58 100644 --- a/app/controllers/admin/taxonomies_controller.rb +++ b/app/controllers/admin/taxonomies_controller.rb @@ -17,4 +17,10 @@ def collection page(params[:page]). search(params[:query]) end + + private + + def taxonomy_params + params.require(:taxonomy).permit(:name) + end end diff --git a/app/controllers/admin/term_trade_codes_pairs_controller.rb b/app/controllers/admin/term_trade_codes_pairs_controller.rb index a4de66fd5..25a22df32 100644 --- a/app/controllers/admin/term_trade_codes_pairs_controller.rb +++ b/app/controllers/admin/term_trade_codes_pairs_controller.rb @@ -51,4 +51,12 @@ def collection page(params[:page]). search(params[:query]) end + + private + + def term_trade_codes_pair_params + params.require(:term_trade_codes_pair).permit( + :trade_code_id, :trade_code_type, :term_id + ) + end end diff --git a/app/controllers/admin/terms_controller.rb b/app/controllers/admin/terms_controller.rb index 13a4567b8..16ec47534 100644 --- a/app/controllers/admin/terms_controller.rb +++ b/app/controllers/admin/terms_controller.rb @@ -22,4 +22,10 @@ def collection page(params[:page]). search(params[:query]) end + + private + + def term_params + params.require(:term).permit(:code, :type, :name_en, :name_es, :name_fr) + end end diff --git a/app/controllers/admin/trade_name_relationships_controller.rb b/app/controllers/admin/trade_name_relationships_controller.rb index 2eb5cb360..f8ba53d73 100644 --- a/app/controllers/admin/trade_name_relationships_controller.rb +++ b/app/controllers/admin/trade_name_relationships_controller.rb @@ -63,4 +63,13 @@ def load_trade_name_relationship_type find_by_name(TaxonRelationshipType::HAS_TRADE_NAME) end + private + + def trade_name_relationship_params + params.require(:taxon_relationship).permit( + :taxon_concept_id, :other_taxon_concept_id, :taxon_relationship_type_id, + :created_by_id, :updated_by_id + ) + end + end diff --git a/app/controllers/admin/units_controller.rb b/app/controllers/admin/units_controller.rb index 8d6ca8f53..ccf0a4690 100644 --- a/app/controllers/admin/units_controller.rb +++ b/app/controllers/admin/units_controller.rb @@ -22,4 +22,12 @@ def collection page(params[:page]). search(params[:query]) end + + private + + def unit_params + params.require(:unit).permit( + :code, :type, :name_en, :name_es, :name_fr + ) + end end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index a5de1871c..578ee0654 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -50,4 +50,14 @@ def load_associations ). order('name_en') end + + private + + def user_params + params.require(:user).permit( + :email, :name, :password, :password_confirmation, + :remember_me, :role, :terms_and_conditions, :is_cites_authority, + :organisation, :geo_entity_id, :is_active + ) + end end diff --git a/app/models/annotation.rb b/app/models/annotation.rb index 600b0f2d0..ad0cf1bd9 100644 --- a/app/models/annotation.rb +++ b/app/models/annotation.rb @@ -24,9 +24,9 @@ class Annotation < ActiveRecord::Base track_who_does_it - attr_accessible :listing_change_id, :symbol, :parent_symbol, :short_note_en, - :full_note_en, :short_note_fr, :full_note_fr, :short_note_es, :full_note_es, - :display_in_index, :display_in_footnote, :event_id + # attr_accessible :listing_change_id, :symbol, :parent_symbol, :short_note_en, + # :full_note_en, :short_note_fr, :full_note_fr, :short_note_es, :full_note_es, + # :display_in_index, :display_in_footnote, :event_id has_many :listing_changes has_many :hashed_listing_changes, diff --git a/app/models/change_type.rb b/app/models/change_type.rb index 09c0c64ae..602378679 100644 --- a/app/models/change_type.rb +++ b/app/models/change_type.rb @@ -13,8 +13,8 @@ # class ChangeType < ActiveRecord::Base - attr_accessible :name, :display_name_en, :display_name_es, :display_name_fr, - :designation_id + # attr_accessible :name, :display_name_en, :display_name_es, :display_name_fr, + # :designation_id include Dictionary build_dictionary :addition, :deletion, :reservation, :reservation_withdrawal, :exception diff --git a/app/models/cites_ac.rb b/app/models/cites_ac.rb index 672479eaa..e6a65b556 100644 --- a/app/models/cites_ac.rb +++ b/app/models/cites_ac.rb @@ -26,7 +26,7 @@ # Cites Animal Committee class CitesAc < Event - attr_accessible :is_current + # attr_accessible :is_current validates :effective_at, :presence => true diff --git a/app/models/cites_cop.rb b/app/models/cites_cop.rb index 35fee61b4..4e129ba0e 100644 --- a/app/models/cites_cop.rb +++ b/app/models/cites_cop.rb @@ -24,7 +24,7 @@ # class CitesCop < Event - attr_accessible :is_current + # attr_accessible :is_current has_many :listing_changes, :foreign_key => :event_id has_many :hash_annotations, :class_name => 'Annotation', :foreign_key => :event_id diff --git a/app/models/cites_extraordinary_meeting.rb b/app/models/cites_extraordinary_meeting.rb index 693a7e685..fb5ff452f 100644 --- a/app/models/cites_extraordinary_meeting.rb +++ b/app/models/cites_extraordinary_meeting.rb @@ -24,7 +24,7 @@ # class CitesExtraordinaryMeeting < Event - attr_accessible :is_current + # attr_accessible :is_current validates :effective_at, :presence => true diff --git a/app/models/cites_pc.rb b/app/models/cites_pc.rb index ceb289105..b17133806 100644 --- a/app/models/cites_pc.rb +++ b/app/models/cites_pc.rb @@ -26,7 +26,7 @@ # Cites Plant Committee class CitesPc < Event - attr_accessible :is_current + # attr_accessible :is_current validates :effective_at, :presence => true diff --git a/app/models/cites_process.rb b/app/models/cites_process.rb index b9a96dcc4..13ede34dd 100644 --- a/app/models/cites_process.rb +++ b/app/models/cites_process.rb @@ -1,8 +1,8 @@ class CitesProcess < ActiveRecord::Base track_who_does_it - attr_accessible :start_event_id, :geo_entity_id, :resolution, :start_date, - :taxon_concept_id, :notes, :status, :document, :document_title, - :created_by_id, :updated_by_id + # attr_accessible :start_event_id, :geo_entity_id, :resolution, :start_date, + # :taxon_concept_id, :notes, :status, :document, :document_title, + # :created_by_id, :updated_by_id belongs_to :taxon_concept belongs_to :geo_entity belongs_to :start_event, :class_name => 'Event' diff --git a/app/models/cites_suspension.rb b/app/models/cites_suspension.rb index 9eefb70bf..f68cf27d1 100644 --- a/app/models/cites_suspension.rb +++ b/app/models/cites_suspension.rb @@ -31,9 +31,9 @@ # class CitesSuspension < TradeRestriction - attr_accessible :start_notification_id, :end_notification_id, - :cites_suspension_confirmations_attributes, - :applies_to_import + # attr_accessible :start_notification_id, :end_notification_id, + # :cites_suspension_confirmations_attributes, + # :applies_to_import belongs_to :taxon_concept belongs_to :start_notification, :class_name => 'CitesSuspensionNotification' belongs_to :end_notification, :class_name => 'CitesSuspensionNotification' diff --git a/app/models/cites_suspension_confirmation.rb b/app/models/cites_suspension_confirmation.rb index 81c7c0404..bbabaffb3 100644 --- a/app/models/cites_suspension_confirmation.rb +++ b/app/models/cites_suspension_confirmation.rb @@ -10,7 +10,7 @@ # class CitesSuspensionConfirmation < ActiveRecord::Base - attr_accessible :cites_suspension_notification_id + # attr_accessible :cites_suspension_notification_id belongs_to :confirmation_notification, :class_name => 'CitesSuspensionNotification', :foreign_key => :cites_suspension_notification_id belongs_to :confirmed_suspension, :class_name => 'CitesSuspension', diff --git a/app/models/cites_suspension_notification.rb b/app/models/cites_suspension_notification.rb index 7844ee949..ce4703ff5 100644 --- a/app/models/cites_suspension_notification.rb +++ b/app/models/cites_suspension_notification.rb @@ -24,7 +24,7 @@ # class CitesSuspensionNotification < Event - attr_accessible :subtype, :new_subtype, :end_date + # attr_accessible :subtype, :new_subtype, :end_date attr_accessor :new_subtype has_many :started_suspensions, :foreign_key => :start_notification_id, :class_name => 'CitesSuspension' has_many :ended_suspensions, :foreign_key => :end_notification_id, :class_name => 'CitesSuspension' diff --git a/app/models/cites_tc.rb b/app/models/cites_tc.rb index 7321c6968..31fa43cbc 100644 --- a/app/models/cites_tc.rb +++ b/app/models/cites_tc.rb @@ -26,7 +26,7 @@ # Cites Tech Committee class CitesTc < Event - attr_accessible :is_current + # attr_accessible :is_current validates :effective_at, :presence => true diff --git a/app/models/cms_mapping.rb b/app/models/cms_mapping.rb index 558c78001..21d6622ee 100644 --- a/app/models/cms_mapping.rb +++ b/app/models/cms_mapping.rb @@ -14,7 +14,7 @@ # class CmsMapping < ActiveRecord::Base - attr_accessible :accepted_name_id, :cms_author, :cms_taxon_name, :cms_uuid, :details, :taxon_concept_id + # attr_accessible :accepted_name_id, :cms_author, :cms_taxon_name, :cms_uuid, :details, :taxon_concept_id # serialize :details, ActiveRecord::Coders::Hstore belongs_to :taxon_concept diff --git a/app/models/comment.rb b/app/models/comment.rb index 9fdd19dba..19cb9ba33 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -15,7 +15,7 @@ class Comment < ActiveRecord::Base track_who_does_it - attr_accessible :comment_type, :commentable_id, :commentable_type, :note, - :created_by_id, :updated_by_id + # attr_accessible :comment_type, :commentable_id, :commentable_type, :note, + # :created_by_id, :updated_by_id belongs_to :commentable, polymorphic: true end diff --git a/app/models/designation.rb b/app/models/designation.rb index 21284a3ce..138c75110 100644 --- a/app/models/designation.rb +++ b/app/models/designation.rb @@ -10,7 +10,7 @@ # class Designation < ActiveRecord::Base - attr_accessible :name, :taxonomy_id + # attr_accessible :name, :taxonomy_id include Dictionary build_dictionary :cites, :eu, :cms diff --git a/app/models/distribution.rb b/app/models/distribution.rb index 7d61ccb33..c26edf7f2 100644 --- a/app/models/distribution.rb +++ b/app/models/distribution.rb @@ -14,8 +14,8 @@ class Distribution < ActiveRecord::Base track_who_does_it - attr_accessible :geo_entity_id, :taxon_concept_id, :tag_list, - :references_attributes, :internal_notes, :created_by_id, :updated_by_id + # attr_accessible :geo_entity_id, :taxon_concept_id, :tag_list, + # :references_attributes, :internal_notes, :created_by_id, :updated_by_id acts_as_taggable normalise_blank_values diff --git a/app/models/document.rb b/app/models/document.rb index 31de6644b..276499e12 100644 --- a/app/models/document.rb +++ b/app/models/document.rb @@ -30,11 +30,11 @@ class Document < ActiveRecord::Base :using => { :tsearch => { :prefix => true } }, :order_within_rank => "documents.date, documents.title, documents.id" track_who_does_it - attr_accessible :event_id, :filename, :date, :type, :title, :is_public, - :language_id, :citations_attributes, - :sort_index, :discussion_id, :discussion_sort_index, - :primary_language_document_id, - :designation_id + # attr_accessible :event_id, :filename, :date, :type, :title, :is_public, + # :language_id, :citations_attributes, + # :sort_index, :discussion_id, :discussion_sort_index, + # :primary_language_document_id, + # :designation_id belongs_to :designation belongs_to :event belongs_to :language diff --git a/app/models/document_citation.rb b/app/models/document_citation.rb index 424f5f495..cc3c473ac 100644 --- a/app/models/document_citation.rb +++ b/app/models/document_citation.rb @@ -13,7 +13,7 @@ class DocumentCitation < ActiveRecord::Base track_who_does_it - attr_accessible :document_id, :stringy_taxon_concept_ids, :geo_entity_ids + # attr_accessible :document_id, :stringy_taxon_concept_ids, :geo_entity_ids has_many :document_citation_taxon_concepts, dependent: :destroy, autosave: true has_many :taxon_concepts, through: :document_citation_taxon_concepts has_many :document_citation_geo_entities, dependent: :destroy, autosave: true diff --git a/app/models/ec_srg.rb b/app/models/ec_srg.rb index 0d6c0116c..bea212d04 100644 --- a/app/models/ec_srg.rb +++ b/app/models/ec_srg.rb @@ -26,7 +26,7 @@ # European Commission Scientific Review Group class EcSrg < Event - attr_accessible :is_current + # attr_accessible :is_current has_many :eu_opinions, :foreign_key => :start_event_id, :dependent => :nullify diff --git a/app/models/eu_decision.rb b/app/models/eu_decision.rb index e138c0dc5..936348e08 100644 --- a/app/models/eu_decision.rb +++ b/app/models/eu_decision.rb @@ -30,11 +30,11 @@ require 'csv' class EuDecision < ActiveRecord::Base track_who_does_it - attr_accessible :end_date, :end_event_id, :geo_entity_id, :internal_notes, - :is_current, :notes, :start_date, :start_event_id, :eu_decision_type_id, - :taxon_concept_id, :type, :conditions_apply, :term_id, :source_id, - :nomenclature_note_en, :nomenclature_note_es, :nomenclature_note_fr, - :created_by_id, :updated_by_id, :srg_history_id + # attr_accessible :end_date, :end_event_id, :geo_entity_id, :internal_notes, + # :is_current, :notes, :start_date, :start_event_id, :eu_decision_type_id, + # :taxon_concept_id, :type, :conditions_apply, :term_id, :source_id, + # :nomenclature_note_en, :nomenclature_note_es, :nomenclature_note_fr, + # :created_by_id, :updated_by_id, :srg_history_id belongs_to :taxon_concept belongs_to :m_taxon_concept, :foreign_key => :taxon_concept_id diff --git a/app/models/eu_decision_type.rb b/app/models/eu_decision_type.rb index c6a13a14d..94dff7df9 100644 --- a/app/models/eu_decision_type.rb +++ b/app/models/eu_decision_type.rb @@ -11,7 +11,7 @@ # class EuDecisionType < ActiveRecord::Base - attr_accessible :name, :tooltip, :decision_type + # attr_accessible :name, :tooltip, :decision_type include Dictionary build_dictionary :negative_opinion, :positive_opinion, :no_opinion, :suspension, :srg_referral diff --git a/app/models/eu_opinion.rb b/app/models/eu_opinion.rb index 1808be801..0913297df 100644 --- a/app/models/eu_opinion.rb +++ b/app/models/eu_opinion.rb @@ -27,7 +27,7 @@ # class EuOpinion < EuDecision - attr_accessible :document_id + # attr_accessible :document_id belongs_to :document diff --git a/app/models/eu_regulation.rb b/app/models/eu_regulation.rb index 0510d3c85..c53cb154b 100644 --- a/app/models/eu_regulation.rb +++ b/app/models/eu_regulation.rb @@ -24,7 +24,7 @@ # class EuRegulation < Event - attr_accessible :listing_changes_event_id, :end_date + # attr_accessible :listing_changes_event_id, :end_date attr_accessor :listing_changes_event_id has_many :listing_changes, :foreign_key => :event_id, diff --git a/app/models/eu_suspension_regulation.rb b/app/models/eu_suspension_regulation.rb index 2d0710be9..39e300d1a 100644 --- a/app/models/eu_suspension_regulation.rb +++ b/app/models/eu_suspension_regulation.rb @@ -24,7 +24,7 @@ # class EuSuspensionRegulation < Event - attr_accessible :eu_suspensions_event_id + # attr_accessible :eu_suspensions_event_id attr_accessor :eu_suspensions_event_id has_many :eu_suspensions, :foreign_key => :start_event_id, diff --git a/app/models/event.rb b/app/models/event.rb index 6cc8c9b46..16dd1e5dd 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -25,9 +25,11 @@ class Event < ActiveRecord::Base track_who_does_it - attr_accessible :name, :designation_id, :description, :extended_description, - :url, :private_url, :multilingual_url, :published_at, :effective_at, :is_current, :end_date, - :created_by_id, :updated_by_id + + # attr_accessible :name, :designation_id, :description, :extended_description, + # :url, :private_url, :multilingual_url, :published_at, :effective_at, :is_current, :end_date, + # :created_by_id, :updated_by_id + attr_reader :effective_at_formatted belongs_to :designation diff --git a/app/models/geo_entity.rb b/app/models/geo_entity.rb index ccc0d5715..d21b9d9ba 100644 --- a/app/models/geo_entity.rb +++ b/app/models/geo_entity.rb @@ -18,9 +18,9 @@ # class GeoEntity < ActiveRecord::Base - attr_accessible :geo_entity_type_id, :iso_code2, :iso_code3, - :legacy_id, :legacy_type, :long_name, :name_en, :name_es, :name_fr, - :is_current + # attr_accessible :geo_entity_type_id, :iso_code2, :iso_code3, + # :legacy_id, :legacy_type, :long_name, :name_en, :name_es, :name_fr, + # :is_current translates :name belongs_to :geo_entity_type has_many :geo_relationships diff --git a/app/models/geo_relationship.rb b/app/models/geo_relationship.rb index c20c7c907..e1f3028f0 100644 --- a/app/models/geo_relationship.rb +++ b/app/models/geo_relationship.rb @@ -11,7 +11,7 @@ # class GeoRelationship < ActiveRecord::Base - attr_accessible :geo_entity_id, :geo_relationship_type_id, :other_geo_entity_id + # attr_accessible :geo_entity_id, :geo_relationship_type_id, :other_geo_entity_id belongs_to :geo_relationship_type belongs_to :geo_entity belongs_to :related_geo_entity, :class_name => 'GeoEntity', :foreign_key => :other_geo_entity_id diff --git a/app/models/instrument.rb b/app/models/instrument.rb index 8651c1d2a..a8e52a58c 100644 --- a/app/models/instrument.rb +++ b/app/models/instrument.rb @@ -10,7 +10,7 @@ # class Instrument < ActiveRecord::Base - attr_accessible :designation_id, :name + # attr_accessible :designation_id, :name validates :name, :presence => true, :uniqueness => { :scope => :designation_id } diff --git a/app/models/language.rb b/app/models/language.rb index 22b0358f5..234d21134 100644 --- a/app/models/language.rb +++ b/app/models/language.rb @@ -13,7 +13,7 @@ # class Language < ActiveRecord::Base - attr_accessible :iso_code1, :iso_code3, :name_en, :name_fr, :name_es + # attr_accessible :iso_code1, :iso_code3, :name_en, :name_fr, :name_es translates :name has_many :common_names diff --git a/app/models/listing_change.rb b/app/models/listing_change.rb index 7373e8645..1b1ac9c51 100644 --- a/app/models/listing_change.rb +++ b/app/models/listing_change.rb @@ -28,13 +28,13 @@ class ListingChange < ActiveRecord::Base track_who_does_it - attr_accessible :taxon_concept_id, :species_listing_id, :change_type_id, - :effective_at, :is_current, :parent_id, :geo_entity_ids, - :party_listing_distribution_attributes, :inclusion_taxon_concept_id, - :annotation_attributes, :hash_annotation_id, :event_id, - :excluded_geo_entities_ids, :excluded_taxon_concepts_ids, :internal_notes, - :nomenclature_note_en, :nomenclature_note_es, :nomenclature_note_fr, - :created_by_id, :updated_by_id + # attr_accessible :taxon_concept_id, :species_listing_id, :change_type_id, + # :effective_at, :is_current, :parent_id, :geo_entity_ids, + # :party_listing_distribution_attributes, :inclusion_taxon_concept_id, + # :annotation_attributes, :hash_annotation_id, :event_id, + # :excluded_geo_entities_ids, :excluded_taxon_concepts_ids, :internal_notes, + # :nomenclature_note_en, :nomenclature_note_es, :nomenclature_note_fr, + # :created_by_id, :updated_by_id attr_accessor :excluded_geo_entities_ids, :excluded_taxon_concepts_ids diff --git a/app/models/listing_distribution.rb b/app/models/listing_distribution.rb index 1d5550933..a69275486 100644 --- a/app/models/listing_distribution.rb +++ b/app/models/listing_distribution.rb @@ -15,7 +15,7 @@ class ListingDistribution < ActiveRecord::Base track_who_does_it - attr_accessible :geo_entity_id, :listing_change_id, :is_party + # attr_accessible :geo_entity_id, :listing_change_id, :is_party belongs_to :geo_entity belongs_to :listing_change diff --git a/app/models/preset_tag.rb b/app/models/preset_tag.rb index 4a2c9d6e9..276d370a0 100644 --- a/app/models/preset_tag.rb +++ b/app/models/preset_tag.rb @@ -10,7 +10,7 @@ # class PresetTag < ActiveRecord::Base - attr_accessible :model, :name + # attr_accessible :model, :name TYPES = { :Distribution => 'Distribution', diff --git a/app/models/quota.rb b/app/models/quota.rb index 0f389ad5a..5c4b752bc 100644 --- a/app/models/quota.rb +++ b/app/models/quota.rb @@ -32,7 +32,7 @@ class Quota < TradeRestriction - attr_accessible :public_display + # attr_accessible :public_display validates :quota, :presence => true validates :quota, :numericality => { :greater_than_or_equal_to => -1.0 } diff --git a/app/models/rank.rb b/app/models/rank.rb index 736e767b8..8a5f372c2 100644 --- a/app/models/rank.rb +++ b/app/models/rank.rb @@ -14,8 +14,8 @@ # class Rank < ActiveRecord::Base - attr_accessible :name, :display_name_en, :display_name_es, :display_name_fr, - :taxonomic_position, :fixed_order + # attr_accessible :name, :display_name_en, :display_name_es, :display_name_fr, + # :taxonomic_position, :fixed_order include Dictionary build_dictionary :kingdom, :phylum, :class, :order, :family, :subfamily, :genus, :species, :subspecies, :variety diff --git a/app/models/reference.rb b/app/models/reference.rb index 9f82c49ab..dd2e21215 100644 --- a/app/models/reference.rb +++ b/app/models/reference.rb @@ -18,7 +18,7 @@ class Reference < ActiveRecord::Base track_who_does_it - attr_accessible :citation, :created_by_id, :updated_by_id + # attr_accessible :citation, :created_by_id, :updated_by_id validates :citation, :presence => true has_many :taxon_concept_references diff --git a/app/models/species_listing.rb b/app/models/species_listing.rb index 7830327f2..6c1676cca 100644 --- a/app/models/species_listing.rb +++ b/app/models/species_listing.rb @@ -11,7 +11,7 @@ # class SpeciesListing < ActiveRecord::Base - attr_accessible :designation_id, :name, :abbreviation + # attr_accessible :designation_id, :name, :abbreviation belongs_to :designation has_many :listing_changes diff --git a/app/models/srg_history.rb b/app/models/srg_history.rb index 85d6c426c..faabb9de0 100644 --- a/app/models/srg_history.rb +++ b/app/models/srg_history.rb @@ -1,5 +1,5 @@ class SrgHistory < ActiveRecord::Base - attr_accessible :name, :tooltip + # attr_accessible :name, :tooltip has_many :eu_decisions diff --git a/app/models/taxon_common.rb b/app/models/taxon_common.rb index fb7c4dea2..22d74e487 100644 --- a/app/models/taxon_common.rb +++ b/app/models/taxon_common.rb @@ -13,8 +13,8 @@ class TaxonCommon < ActiveRecord::Base track_who_does_it - attr_accessible :common_name_id, :taxon_concept_id, :created_by_id, - :updated_by_id, :name, :language_id + # attr_accessible :common_name_id, :taxon_concept_id, :created_by_id, + # :updated_by_id, :name, :language_id attr_accessor :name, :language_id belongs_to :common_name belongs_to :taxon_concept diff --git a/app/models/taxon_concept.rb b/app/models/taxon_concept.rb index 9eb2da679..045b90cce 100644 --- a/app/models/taxon_concept.rb +++ b/app/models/taxon_concept.rb @@ -42,13 +42,13 @@ class TaxonConcept < ActiveRecord::Base rank_name: :rank_name } - attr_accessible :parent_id, :taxonomy_id, :rank_id, - :parent_id, :author_year, :taxon_name_id, :taxonomic_position, - :legacy_id, :legacy_type, :scientific_name, :name_status, - :tag_list, :legacy_trade_code, :hybrid_parents_ids, - :accepted_names_ids, :accepted_names_for_trade_name_ids, - :nomenclature_note_en, :nomenclature_note_es, :nomenclature_note_fr, - :created_by_id, :updated_by_id, :dependents_updated_at, :kew_id + # attr_accessible :parent_id, :taxonomy_id, :rank_id, + # :parent_id, :author_year, :taxon_name_id, :taxonomic_position, + # :legacy_id, :legacy_type, :scientific_name, :name_status, + # :tag_list, :legacy_trade_code, :hybrid_parents_ids, + # :accepted_names_ids, :accepted_names_for_trade_name_ids, + # :nomenclature_note_en, :nomenclature_note_es, :nomenclature_note_fr, + # :created_by_id, :updated_by_id, :dependents_updated_at, :kew_id attr_writer :accepted_names_ids, :accepted_names_for_trade_name_ids, diff --git a/app/models/taxon_concept_reference.rb b/app/models/taxon_concept_reference.rb index 06d5fbc7d..cce24be17 100644 --- a/app/models/taxon_concept_reference.rb +++ b/app/models/taxon_concept_reference.rb @@ -16,9 +16,9 @@ class TaxonConceptReference < ActiveRecord::Base track_who_does_it - attr_accessible :reference_id, :taxon_concept_id, :is_standard, :is_cascaded, - :excluded_taxon_concepts_ids, :reference_attributes, - :created_by_id, :updated_by_id + # attr_accessible :reference_id, :taxon_concept_id, :is_standard, :is_cascaded, + # :excluded_taxon_concepts_ids, :reference_attributes, + # :created_by_id, :updated_by_id belongs_to :reference belongs_to :taxon_concept @@ -30,7 +30,7 @@ class TaxonConceptReference < ActiveRecord::Base validates :reference_id, :uniqueness => { :scope => [:taxon_concept_id] } def excluded_taxon_concepts - ids = excluded_taxon_concepts_ids.try(:split, ",") + ids = excluded_taxon_concepts_ids.try(:split, ",")&.flatten ids.flatten.present? ? TaxonConcept.where(id: ids).order(:full_name) : [] end diff --git a/app/models/taxon_instrument.rb b/app/models/taxon_instrument.rb index 3d5667ada..57cfaca40 100644 --- a/app/models/taxon_instrument.rb +++ b/app/models/taxon_instrument.rb @@ -14,7 +14,7 @@ class TaxonInstrument < ActiveRecord::Base track_who_does_it - attr_accessible :effective_from, :instrument_id, :taxon_concept_id + # attr_accessible :effective_from, :instrument_id, :taxon_concept_id belongs_to :instrument belongs_to :taxon_concept diff --git a/app/models/taxon_relationship.rb b/app/models/taxon_relationship.rb index 6a96cc205..09eeaa932 100644 --- a/app/models/taxon_relationship.rb +++ b/app/models/taxon_relationship.rb @@ -14,8 +14,8 @@ class TaxonRelationship < ActiveRecord::Base track_who_does_it - attr_accessible :taxon_concept_id, :other_taxon_concept_id, :taxon_relationship_type_id, - :created_by_id, :updated_by_id + # attr_accessible :taxon_concept_id, :other_taxon_concept_id, :taxon_relationship_type_id, + # :created_by_id, :updated_by_id belongs_to :taxon_relationship_type belongs_to :taxon_concept belongs_to :other_taxon_concept, :class_name => 'TaxonConcept', diff --git a/app/models/taxonomy.rb b/app/models/taxonomy.rb index 43e640ecc..96faee418 100644 --- a/app/models/taxonomy.rb +++ b/app/models/taxonomy.rb @@ -12,7 +12,7 @@ class Taxonomy < ActiveRecord::Base include Dictionary build_dictionary :cites_eu, :cms - attr_accessible :name + # attr_accessible :name has_many :designations has_many :taxon_concepts diff --git a/app/models/term_trade_codes_pair.rb b/app/models/term_trade_codes_pair.rb index e3fa1cace..c1a4cf5ef 100644 --- a/app/models/term_trade_codes_pair.rb +++ b/app/models/term_trade_codes_pair.rb @@ -11,7 +11,7 @@ # class TermTradeCodesPair < ActiveRecord::Base - attr_accessible :trade_code_id, :trade_code_type, :term_id + # attr_accessible :trade_code_id, :trade_code_type, :term_id belongs_to :term, :class_name => "TradeCode" belongs_to :trade_code diff --git a/app/models/trade/taxon_concept_term_pair.rb b/app/models/trade/taxon_concept_term_pair.rb index 336089686..50b2421a8 100644 --- a/app/models/trade/taxon_concept_term_pair.rb +++ b/app/models/trade/taxon_concept_term_pair.rb @@ -10,7 +10,7 @@ # class Trade::TaxonConceptTermPair < ActiveRecord::Base - attr_accessible :taxon_concept_id, :term_id + # attr_accessible :taxon_concept_id, :term_id validates :taxon_concept_id, :presence => true validates :term_id, :presence => true validates_uniqueness_of :taxon_concept_id, scope: :term_id diff --git a/app/models/trade_code.rb b/app/models/trade_code.rb index a3ce201fc..f179bbe0b 100644 --- a/app/models/trade_code.rb +++ b/app/models/trade_code.rb @@ -13,8 +13,7 @@ # class TradeCode < ActiveRecord::Base - attr_accessible :code, :type, - :name_en, :name_es, :name_fr + # attr_accessible :code, :type, :name_en, :name_es, :name_fr translates :name validates :code, :presence => true, :uniqueness => { :scope => :type } diff --git a/app/models/trade_restriction.rb b/app/models/trade_restriction.rb index b3a3abda4..36fe13b55 100644 --- a/app/models/trade_restriction.rb +++ b/app/models/trade_restriction.rb @@ -34,12 +34,12 @@ require 'csv' class TradeRestriction < ActiveRecord::Base track_who_does_it - attr_accessible :end_date, :geo_entity_id, :is_current, - :notes, :publication_date, :purpose_ids, :quota, :type, - :source_ids, :start_date, :term_ids, :unit_id, :internal_notes, - :nomenclature_note_en, :nomenclature_note_es, :nomenclature_note_fr, - :created_by_id, :updated_by_id, :url, - :taxon_concept_id + # attr_accessible :end_date, :geo_entity_id, :is_current, + # :notes, :publication_date, :purpose_ids, :quota, :type, + # :source_ids, :start_date, :term_ids, :unit_id, :internal_notes, + # :nomenclature_note_en, :nomenclature_note_es, :nomenclature_note_fr, + # :created_by_id, :updated_by_id, :url, + # :taxon_concept_id belongs_to :taxon_concept belongs_to :m_taxon_concept, :foreign_key => :taxon_concept_id diff --git a/app/models/user.rb b/app/models/user.rb index 0193057aa..f2772b05d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -27,9 +27,9 @@ class User < ActiveRecord::Base include SentientUser devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable - attr_accessible :email, :name, :password, :password_confirmation, - :remember_me, :role, :terms_and_conditions, :is_cites_authority, - :organisation, :geo_entity_id, :is_active + # attr_accessible :email, :name, :password, :password_confirmation, + # :remember_me, :role, :terms_and_conditions, :is_cites_authority, + # :organisation, :geo_entity_id, :is_active MANAGER = 'admin' CONTRIBUTOR = 'default' # nonsense diff --git a/app/views/admin/cites_extraordinary_meetings/_form.html.erb b/app/views/admin/cites_extraordinary_meetings/_form.html.erb index 215c05d0e..be588d6b1 100644 --- a/app/views/admin/cites_extraordinary_meetings/_form.html.erb +++ b/app/views/admin/cites_extraordinary_meetings/_form.html.erb @@ -1,6 +1,6 @@ <%= form_for [:admin, @cites_extraordinary_meeting], :remote => true do |f| %> - <%= error_messages_for(@cites_cop) %> + <%= error_messages_for(@cites_extraordinary_meeting) %> <%= f.hidden_field :designation_id %> <%= f.text_field :name %>
diff --git a/app/views/admin/cites_pcs/_form.html.erb b/app/views/admin/cites_pcs/_form.html.erb index 720d68ce4..3830306c6 100644 --- a/app/views/admin/cites_pcs/_form.html.erb +++ b/app/views/admin/cites_pcs/_form.html.erb @@ -1,6 +1,6 @@ <%= form_for [:admin, @cites_pc], :remote => true do |f| %> - <%= error_messages_for(@cites_cop) %> + <%= error_messages_for(@cites_pc) %> <%= f.hidden_field :designation_id %> <%= f.text_field :name %>
diff --git a/app/views/admin/cites_tcs/_form.html.erb b/app/views/admin/cites_tcs/_form.html.erb index 7195e8a0a..e9907e607 100644 --- a/app/views/admin/cites_tcs/_form.html.erb +++ b/app/views/admin/cites_tcs/_form.html.erb @@ -1,6 +1,6 @@ <%= form_for [:admin, @cites_tc], :remote => true do |f| %> - <%= error_messages_for(@cites_cop) %> + <%= error_messages_for(@cites_tc) %> <%= f.hidden_field :designation_id %> <%= f.text_field :name %>
diff --git a/app/views/admin/taxon_commons/_form.html.erb b/app/views/admin/taxon_commons/_form.html.erb index eb3180de9..46991aca4 100644 --- a/app/views/admin/taxon_commons/_form.html.erb +++ b/app/views/admin/taxon_commons/_form.html.erb @@ -1,8 +1,8 @@ <%= form_for [:admin, @taxon_concept, @taxon_common], :remote => true do |f| %> <%= error_messages_for(@taxon_common) %> - <%= f.text_field :name, :value => @taxon_common.common_name.name %> + <%= f.text_field :name, :value => @taxon_common.common_name&.name %> <%= f.select :language_id, - options_from_collection_for_select(@languages, :id, :name_en, @taxon_common.common_name.language_id ), + options_from_collection_for_select(@languages, :id, :name_en, @taxon_common.common_name&.language_id ), {}, :class => 'select2' %> <% end %> diff --git a/app/views/admin/taxon_instruments/_list.html.erb b/app/views/admin/taxon_instruments/_list.html.erb index 46dcb826c..5d7c9e9ce 100644 --- a/app/views/admin/taxon_instruments/_list.html.erb +++ b/app/views/admin/taxon_instruments/_list.html.erb @@ -6,7 +6,7 @@ <% @taxon_instruments.each do |ti| %> - <%= ti.instrument.name %> + <%= ti.instrument&.name %> <%= ti.effective_from_formatted %> <%= link_to delete_icon, diff --git a/app/views/admin/taxon_relationships/_form.html.erb b/app/views/admin/taxon_relationships/_form.html.erb index 167ed64c2..60509e8b4 100644 --- a/app/views/admin/taxon_relationships/_form.html.erb +++ b/app/views/admin/taxon_relationships/_form.html.erb @@ -18,7 +18,7 @@ diff --git a/config/routes.rb b/config/routes.rb index f2ab7f643..9d7e27379 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -162,7 +162,7 @@ resources :cites_captivity_processes, :only => [:index, :new, :create, :edit, :update, :destroy] end - resources :nomenclature_changes do + resources :nomenclature_changes do # TODO: look like only support :index, :show, :destroy resources :split, controller: 'nomenclature_changes/split' resources :lump, controller: 'nomenclature_changes/lump' resources :status_to_accepted, From 7bc1606b994977cf6b559d44dc42130155c30f22 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Thu, 18 Jan 2024 09:46:16 +0000 Subject: [PATCH 018/241] quiet assets from rails log --- config/environments/development.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/environments/development.rb b/config/environments/development.rb index 612a1a3dc..3c65a31ae 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -27,6 +27,9 @@ # number of complex assets. config.assets.debug = true + # Suppress logger output for asset requests. + config.assets.quiet = true + # Asset digests allow you to set far-future HTTP expiration dates on all assets, # yet still be able to expire them through the digest params. config.assets.digest = true From 3061263c50c67459abe885f84b823fae1b5b81fe Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Thu, 18 Jan 2024 10:38:31 +0000 Subject: [PATCH 019/241] strong params (round 3) --- .../nomenclature_changes/build_controller.rb | 3 +- .../nomenclature_changes/lump_controller.rb | 49 +++++++++++++- .../nomenclature_changes/split_controller.rb | 49 +++++++++++++- .../status_swap_controller.rb | 65 ++++++++++++++++++- .../status_to_accepted_controller.rb | 65 ++++++++++++++++++- .../status_to_synonym_controller.rb | 65 ++++++++++++++++++- app/models/nomenclature_change/input.rb | 13 ++-- app/models/nomenclature_change/lump.rb | 3 +- app/models/nomenclature_change/output.rb | 15 +++-- .../parent_reassignment.rb | 3 +- .../reassignment_helpers.rb | 7 +- app/models/nomenclature_change/split.rb | 3 +- .../status_change_helpers.rb | 5 +- .../nomenclature_change/status_to_accepted.rb | 3 +- 14 files changed, 320 insertions(+), 28 deletions(-) diff --git a/app/controllers/admin/nomenclature_changes/build_controller.rb b/app/controllers/admin/nomenclature_changes/build_controller.rb index 2771dc6c7..c1a5cbede 100644 --- a/app/controllers/admin/nomenclature_changes/build_controller.rb +++ b/app/controllers/admin/nomenclature_changes/build_controller.rb @@ -10,7 +10,8 @@ def finish_wizard_path end def create - @nomenclature_change = klass.new(:status => NomenclatureChange::NEW) + @nomenclature_change = klass.new() + @nomenclature_change.status = NomenclatureChange::NEW if @nomenclature_change.save redirect_to wizard_path(steps.first, :nomenclature_change_id => @nomenclature_change.id) else diff --git a/app/controllers/admin/nomenclature_changes/lump_controller.rb b/app/controllers/admin/nomenclature_changes/lump_controller.rb index c258b17dd..f095d0c2c 100644 --- a/app/controllers/admin/nomenclature_changes/lump_controller.rb +++ b/app/controllers/admin/nomenclature_changes/lump_controller.rb @@ -31,7 +31,7 @@ def show def update @nomenclature_change.assign_attributes( - (params[:nomenclature_change_lump] || {}).merge({ + (nomenclature_change_lump_params || {}).merge({ :status => (step == steps.last ? NomenclatureChange::SUBMITTED : step.to_s) }) ) @@ -58,4 +58,51 @@ def klass NomenclatureChange::Lump end + def nomenclature_change_lump_params + params.require(:nomenclature_change_lump).permit( + :event_id, :status, + inputs_attributes: [ + :nomenclature_change_id, :taxon_concept_id, + :note_en, :note_es, :note_fr, :internal_note, + parent_reassignments_attributes: [ + :id, :_destroy, + :reassignment_target_attributes, + :type, :reassignable_id, :reassignable_type, + :nomenclature_change_input_id, :nomenclature_change_output_id, + :note_en, :note_es, :note_fr, :internal_note, :output_ids + ], + name_reassignments_attributes: [ + :id, :_destroy, + :type, :reassignable_id, :reassignable_type, + :nomenclature_change_input_id, :nomenclature_change_output_id, + :note_en, :note_es, :note_fr, :internal_note, :output_ids + ], + distribution_reassignments_attributes: [ + :id, :_destroy, + :type, :reassignable_id, :reassignable_type, + :nomenclature_change_input_id, :nomenclature_change_output_id, + :note_en, :note_es, :note_fr, :internal_note, :output_ids + ], + legislation_reassignments_attributes: [ + :id, :_destroy, + :type, :reassignable_id, :reassignable_type, + :nomenclature_change_input_id, :nomenclature_change_output_id, + :note_en, :note_es, :note_fr, :internal_note, :output_ids + ] + ], + output_attributes: [ + :nomenclature_change_id, :taxon_concept_id, + :new_taxon_concept_id, :rank_id, :new_scientific_name, :new_author_year, + :new_name_status, :new_parent_id, :new_rank_id, :taxonomy_id, + :note_en, :note_es, :note_fr, :internal_note, :is_primary_output, + :output_type, :tag_list, :created_by_id, :updated_by_id + # app/models/nomenclature_change/output.rb does not have `accepts_nested_attributes_for`, so + # xxx_attributes suppose not in-use. + # :parent_reassignments_attributes, + # :name_reassignments_attributes, + # :distribution_reassignments_attributes, + # :legislation_reassignments_attributes, + ] + ) + end end diff --git a/app/controllers/admin/nomenclature_changes/split_controller.rb b/app/controllers/admin/nomenclature_changes/split_controller.rb index eacd07e99..b558f532d 100644 --- a/app/controllers/admin/nomenclature_changes/split_controller.rb +++ b/app/controllers/admin/nomenclature_changes/split_controller.rb @@ -37,7 +37,7 @@ def show def update @nomenclature_change.assign_attributes( - (params[:nomenclature_change_split] || {}).merge({ + (nomenclature_change_split_params || {}).merge({ :status => (step == steps.last ? NomenclatureChange::SUBMITTED : step.to_s) }) ) @@ -59,4 +59,51 @@ def klass NomenclatureChange::Split end + def nomenclature_change_split_params + params.require(:nomenclature_change_split).permit( + :event_id, :status, + inputs_attributes: [ + :nomenclature_change_id, :taxon_concept_id, + :note_en, :note_es, :note_fr, :internal_note, + parent_reassignments_attributes: [ + :id, :_destroy, + :reassignment_target_attributes, + :type, :reassignable_id, :reassignable_type, + :nomenclature_change_input_id, :nomenclature_change_output_id, + :note_en, :note_es, :note_fr, :internal_note, :output_ids + ], + name_reassignments_attributes: [ + :id, :_destroy, + :type, :reassignable_id, :reassignable_type, + :nomenclature_change_input_id, :nomenclature_change_output_id, + :note_en, :note_es, :note_fr, :internal_note, :output_ids + ], + distribution_reassignments_attributes: [ + :id, :_destroy, + :type, :reassignable_id, :reassignable_type, + :nomenclature_change_input_id, :nomenclature_change_output_id, + :note_en, :note_es, :note_fr, :internal_note, :output_ids + ], + legislation_reassignments_attributes: [ + :id, :_destroy, + :type, :reassignable_id, :reassignable_type, + :nomenclature_change_input_id, :nomenclature_change_output_id, + :note_en, :note_es, :note_fr, :internal_note, :output_ids + ] + ], + output_attributes: [ + :nomenclature_change_id, :taxon_concept_id, + :new_taxon_concept_id, :rank_id, :new_scientific_name, :new_author_year, + :new_name_status, :new_parent_id, :new_rank_id, :taxonomy_id, + :note_en, :note_es, :note_fr, :internal_note, :is_primary_output, + :output_type, :tag_list, :created_by_id, :updated_by_id + # app/models/nomenclature_change/output.rb does not have `accepts_nested_attributes_for`, so + # xxx_attributes suppose not in-use. + # :parent_reassignments_attributes, + # :name_reassignments_attributes, + # :distribution_reassignments_attributes, + # :legislation_reassignments_attributes, + ] + ) + end end diff --git a/app/controllers/admin/nomenclature_changes/status_swap_controller.rb b/app/controllers/admin/nomenclature_changes/status_swap_controller.rb index 75aa7531c..5c25d695c 100644 --- a/app/controllers/admin/nomenclature_changes/status_swap_controller.rb +++ b/app/controllers/admin/nomenclature_changes/status_swap_controller.rb @@ -27,7 +27,7 @@ def show def update @nomenclature_change.assign_attributes( - (params[:nomenclature_change_status_swap] || {}).merge({ + (nomenclature_change_status_swap_params || {}).merge({ :status => (step == steps.last ? NomenclatureChange::SUBMITTED : step.to_s) }) ) @@ -53,4 +53,67 @@ def klass NomenclatureChange::StatusSwap end + def nomenclature_change_status_swap_params + params.require(:nomenclature_change_status_swap).permit( + :event_id, :status, + primary_output_attributes: [ + :id, :_destroy, + :nomenclature_change_id, :taxon_concept_id, + :new_taxon_concept_id, :rank_id, :new_scientific_name, :new_author_year, + :new_name_status, :new_parent_id, :new_rank_id, :taxonomy_id, + :note_en, :note_es, :note_fr, :internal_note, :is_primary_output, + :output_type, :tag_list, :created_by_id, :updated_by_id + # app/models/nomenclature_change/output.rb does not have `accepts_nested_attributes_for`, so + # xxx_attributes suppose not in-use. + # :parent_reassignments_attributes, + # :name_reassignments_attributes, + # :distribution_reassignments_attributes, + # :legislation_reassignments_attributes, + ], + secondary_output_attributes: [ + :id, :_destroy, + :nomenclature_change_id, :taxon_concept_id, + :new_taxon_concept_id, :rank_id, :new_scientific_name, :new_author_year, + :new_name_status, :new_parent_id, :new_rank_id, :taxonomy_id, + :note_en, :note_es, :note_fr, :internal_note, :is_primary_output, + :output_type, :tag_list, :created_by_id, :updated_by_id + # app/models/nomenclature_change/output.rb does not have `accepts_nested_attributes_for`, so + # xxx_attributes suppose not in-use. + # :parent_reassignments_attributes, + # :name_reassignments_attributes, + # :distribution_reassignments_attributes, + # :legislation_reassignments_attributes, + ], + input_attributes: [ + :id, :_destroy, + :nomenclature_change_id, :taxon_concept_id, + :note_en, :note_es, :note_fr, :internal_note, + parent_reassignments_attributes: [ + :id, :_destroy, + :reassignment_target_attributes, + :type, :reassignable_id, :reassignable_type, + :nomenclature_change_input_id, :nomenclature_change_output_id, + :note_en, :note_es, :note_fr, :internal_note, :output_ids + ], + name_reassignments_attributes: [ + :id, :_destroy, + :type, :reassignable_id, :reassignable_type, + :nomenclature_change_input_id, :nomenclature_change_output_id, + :note_en, :note_es, :note_fr, :internal_note, :output_ids + ], + distribution_reassignments_attributes: [ + :id, :_destroy, + :type, :reassignable_id, :reassignable_type, + :nomenclature_change_input_id, :nomenclature_change_output_id, + :note_en, :note_es, :note_fr, :internal_note, :output_ids + ], + legislation_reassignments_attributes: [ + :id, :_destroy, + :type, :reassignable_id, :reassignable_type, + :nomenclature_change_input_id, :nomenclature_change_output_id, + :note_en, :note_es, :note_fr, :internal_note, :output_ids + ] + ] + ) + end end diff --git a/app/controllers/admin/nomenclature_changes/status_to_accepted_controller.rb b/app/controllers/admin/nomenclature_changes/status_to_accepted_controller.rb index 4e4a88433..3f3aecde4 100644 --- a/app/controllers/admin/nomenclature_changes/status_to_accepted_controller.rb +++ b/app/controllers/admin/nomenclature_changes/status_to_accepted_controller.rb @@ -19,7 +19,7 @@ def show def update @nomenclature_change.assign_attributes( - (params[:nomenclature_change_status_to_accepted] || {}).merge({ + (nomenclature_change_status_to_accepted_params || {}).merge({ :status => (step == steps.last ? NomenclatureChange::SUBMITTED : step.to_s) }) ) @@ -41,4 +41,67 @@ def klass NomenclatureChange::StatusToAccepted end + def nomenclature_change_status_to_accepted_params + params.require(:nomenclature_change_status_to_accepted).permit( + :created_by_id, :updated_by_id, :event_id, :status, + primary_output_attributes: [ + :id, :_destroy, + :nomenclature_change_id, :taxon_concept_id, + :new_taxon_concept_id, :rank_id, :new_scientific_name, :new_author_year, + :new_name_status, :new_parent_id, :new_rank_id, :taxonomy_id, + :note_en, :note_es, :note_fr, :internal_note, :is_primary_output, + :output_type, :tag_list, :created_by_id, :updated_by_id + # app/models/nomenclature_change/output.rb does not have `accepts_nested_attributes_for`, so + # xxx_attributes suppose not in-use. + # :parent_reassignments_attributes, + # :name_reassignments_attributes, + # :distribution_reassignments_attributes, + # :legislation_reassignments_attributes, + ], + secondary_output_attributes: [ + :id, :_destroy, + :nomenclature_change_id, :taxon_concept_id, + :new_taxon_concept_id, :rank_id, :new_scientific_name, :new_author_year, + :new_name_status, :new_parent_id, :new_rank_id, :taxonomy_id, + :note_en, :note_es, :note_fr, :internal_note, :is_primary_output, + :output_type, :tag_list, :created_by_id, :updated_by_id + # app/models/nomenclature_change/output.rb does not have `accepts_nested_attributes_for`, so + # xxx_attributes suppose not in-use. + # :parent_reassignments_attributes, + # :name_reassignments_attributes, + # :distribution_reassignments_attributes, + # :legislation_reassignments_attributes, + ], + input_attributes: [ + :id, :_destroy, + :nomenclature_change_id, :taxon_concept_id, + :note_en, :note_es, :note_fr, :internal_note, + parent_reassignments_attributes: [ + :id, :_destroy, + :reassignment_target_attributes, + :type, :reassignable_id, :reassignable_type, + :nomenclature_change_input_id, :nomenclature_change_output_id, + :note_en, :note_es, :note_fr, :internal_note, :output_ids + ], + name_reassignments_attributes: [ + :id, :_destroy, + :type, :reassignable_id, :reassignable_type, + :nomenclature_change_input_id, :nomenclature_change_output_id, + :note_en, :note_es, :note_fr, :internal_note, :output_ids + ], + distribution_reassignments_attributes: [ + :id, :_destroy, + :type, :reassignable_id, :reassignable_type, + :nomenclature_change_input_id, :nomenclature_change_output_id, + :note_en, :note_es, :note_fr, :internal_note, :output_ids + ], + legislation_reassignments_attributes: [ + :id, :_destroy, + :type, :reassignable_id, :reassignable_type, + :nomenclature_change_input_id, :nomenclature_change_output_id, + :note_en, :note_es, :note_fr, :internal_note, :output_ids + ] + ] + ) + end end diff --git a/app/controllers/admin/nomenclature_changes/status_to_synonym_controller.rb b/app/controllers/admin/nomenclature_changes/status_to_synonym_controller.rb index df3ef63fd..6a5beca53 100644 --- a/app/controllers/admin/nomenclature_changes/status_to_synonym_controller.rb +++ b/app/controllers/admin/nomenclature_changes/status_to_synonym_controller.rb @@ -29,7 +29,7 @@ def show def update @nomenclature_change.assign_attributes( - (params[:nomenclature_change_status_to_synonym] || {}).merge({ + (nomenclature_change_status_to_synonym_params || {}).merge({ :status => (step == steps.last ? NomenclatureChange::SUBMITTED : step.to_s) }) ) @@ -52,4 +52,67 @@ def klass NomenclatureChange::StatusToSynonym end + def nomenclature_change_status_to_synonym_params + params.require(:nomenclature_change_status_to_synonym).permit( + :event_id, :status, + primary_output_attributes: [ + :id, :_destroy, + :nomenclature_change_id, :taxon_concept_id, + :new_taxon_concept_id, :rank_id, :new_scientific_name, :new_author_year, + :new_name_status, :new_parent_id, :new_rank_id, :taxonomy_id, + :note_en, :note_es, :note_fr, :internal_note, :is_primary_output, + :output_type, :tag_list, :created_by_id, :updated_by_id + # app/models/nomenclature_change/output.rb does not have `accepts_nested_attributes_for`, so + # xxx_attributes suppose not in-use. + # :parent_reassignments_attributes, + # :name_reassignments_attributes, + # :distribution_reassignments_attributes, + # :legislation_reassignments_attributes, + ], + secondary_output_attributes: [ + :id, :_destroy, + :nomenclature_change_id, :taxon_concept_id, + :new_taxon_concept_id, :rank_id, :new_scientific_name, :new_author_year, + :new_name_status, :new_parent_id, :new_rank_id, :taxonomy_id, + :note_en, :note_es, :note_fr, :internal_note, :is_primary_output, + :output_type, :tag_list, :created_by_id, :updated_by_id + # app/models/nomenclature_change/output.rb does not have `accepts_nested_attributes_for`, so + # xxx_attributes suppose not in-use. + # :parent_reassignments_attributes, + # :name_reassignments_attributes, + # :distribution_reassignments_attributes, + # :legislation_reassignments_attributes, + ], + input_attributes: [ + :id, :_destroy, + :nomenclature_change_id, :taxon_concept_id, + :note_en, :note_es, :note_fr, :internal_note, + parent_reassignments_attributes: [ + :id, :_destroy, + :reassignment_target_attributes, + :type, :reassignable_id, :reassignable_type, + :nomenclature_change_input_id, :nomenclature_change_output_id, + :note_en, :note_es, :note_fr, :internal_note, :output_ids + ], + name_reassignments_attributes: [ + :id, :_destroy, + :type, :reassignable_id, :reassignable_type, + :nomenclature_change_input_id, :nomenclature_change_output_id, + :note_en, :note_es, :note_fr, :internal_note, :output_ids + ], + distribution_reassignments_attributes: [ + :id, :_destroy, + :type, :reassignable_id, :reassignable_type, + :nomenclature_change_input_id, :nomenclature_change_output_id, + :note_en, :note_es, :note_fr, :internal_note, :output_ids + ], + legislation_reassignments_attributes: [ + :id, :_destroy, + :type, :reassignable_id, :reassignable_type, + :nomenclature_change_input_id, :nomenclature_change_output_id, + :note_en, :note_es, :note_fr, :internal_note, :output_ids + ] + ] + ) + end end diff --git a/app/models/nomenclature_change/input.rb b/app/models/nomenclature_change/input.rb index f2ea35e01..fd0f5f65a 100644 --- a/app/models/nomenclature_change/input.rb +++ b/app/models/nomenclature_change/input.rb @@ -19,12 +19,13 @@ # Inputs are required to be existing taxon concepts. class NomenclatureChange::Input < ActiveRecord::Base track_who_does_it - attr_accessible :nomenclature_change_id, :taxon_concept_id, - :note_en, :note_es, :note_fr, :internal_note, - :parent_reassignments_attributes, - :name_reassignments_attributes, - :distribution_reassignments_attributes, - :legislation_reassignments_attributes + # Migrated to controller (Strong Parameters) + # attr_accessible :nomenclature_change_id, :taxon_concept_id, + # :note_en, :note_es, :note_fr, :internal_note, + # :parent_reassignments_attributes, + # :name_reassignments_attributes, + # :distribution_reassignments_attributes, + # :legislation_reassignments_attributes belongs_to :nomenclature_change belongs_to :taxon_concept has_many :reassignments, diff --git a/app/models/nomenclature_change/lump.rb b/app/models/nomenclature_change/lump.rb index 7e865f12a..bfcd7fc5b 100644 --- a/app/models/nomenclature_change/lump.rb +++ b/app/models/nomenclature_change/lump.rb @@ -14,7 +14,8 @@ class NomenclatureChange::Lump < NomenclatureChange build_steps(:inputs, :outputs, :notes, :legislation, :summary) - attr_accessible :inputs_attributes, :output_attributes + # Migrated to controller (Strong Parameters) + # attr_accessible :inputs_attributes, :output_attributes has_many :inputs, :inverse_of => :nomenclature_change, :class_name => NomenclatureChange::Input, :foreign_key => :nomenclature_change_id, diff --git a/app/models/nomenclature_change/output.rb b/app/models/nomenclature_change/output.rb index 57129c727..424d408fd 100644 --- a/app/models/nomenclature_change/output.rb +++ b/app/models/nomenclature_change/output.rb @@ -35,13 +35,14 @@ class NomenclatureChange::Output < ActiveRecord::Base track_who_does_it attr_accessor :output_type # New taxon, Existing subspecies, Existing taxon - attr_accessible :nomenclature_change_id, :taxon_concept_id, - :new_taxon_concept_id, :rank_id, :new_scientific_name, :new_author_year, - :new_name_status, :new_parent_id, :new_rank_id, :taxonomy_id, - :note_en, :note_es, :note_fr, :internal_note, :is_primary_output, - :parent_reassignments_attributes, :name_reassignments_attributes, - :distribution_reassignments_attributes, :legislation_reassignments_attributes, - :output_type, :tag_list, :created_by_id, :updated_by_id + # Migrated to controller (Strong Parameters) + # attr_accessible :nomenclature_change_id, :taxon_concept_id, + # :new_taxon_concept_id, :rank_id, :new_scientific_name, :new_author_year, + # :new_name_status, :new_parent_id, :new_rank_id, :taxonomy_id, + # :note_en, :note_es, :note_fr, :internal_note, :is_primary_output, + # :parent_reassignments_attributes, :name_reassignments_attributes, + # :distribution_reassignments_attributes, :legislation_reassignments_attributes, + # :output_type, :tag_list, :created_by_id, :updated_by_id belongs_to :nomenclature_change belongs_to :taxon_concept diff --git a/app/models/nomenclature_change/parent_reassignment.rb b/app/models/nomenclature_change/parent_reassignment.rb index 5ba0a4318..94c61b3ce 100644 --- a/app/models/nomenclature_change/parent_reassignment.rb +++ b/app/models/nomenclature_change/parent_reassignment.rb @@ -19,7 +19,8 @@ # Reassignable is a taxon concept that is reassigned to a new parent class NomenclatureChange::ParentReassignment < NomenclatureChange::Reassignment - attr_accessible :reassignment_target_attributes + # Migrated to controller (Strong Parameters) + # attr_accessible :reassignment_target_attributes has_one :reassignment_target, :inverse_of => :reassignment, :class_name => NomenclatureChange::ReassignmentTarget, :foreign_key => :nomenclature_change_reassignment_id diff --git a/app/models/nomenclature_change/reassignment_helpers.rb b/app/models/nomenclature_change/reassignment_helpers.rb index 210f2eee6..68670490e 100644 --- a/app/models/nomenclature_change/reassignment_helpers.rb +++ b/app/models/nomenclature_change/reassignment_helpers.rb @@ -3,9 +3,10 @@ module NomenclatureChange::ReassignmentHelpers def self.included(base) base.class_eval do track_who_does_it - attr_accessible :type, :reassignable_id, :reassignable_type, - :nomenclature_change_input_id, :nomenclature_change_output_id, - :note_en, :note_es, :note_fr, :internal_note, :output_ids + # Migrated to controller (Strong Parameters) + # attr_accessible :type, :reassignable_id, :reassignable_type, + # :nomenclature_change_input_id, :nomenclature_change_output_id, + # :note_en, :note_es, :note_fr, :internal_note, :output_ids belongs_to :reassignable, :polymorphic => true has_many :reassignment_targets, inverse_of: :reassignment, diff --git a/app/models/nomenclature_change/split.rb b/app/models/nomenclature_change/split.rb index c8dc95f30..e51cb4d7c 100644 --- a/app/models/nomenclature_change/split.rb +++ b/app/models/nomenclature_change/split.rb @@ -15,7 +15,8 @@ class NomenclatureChange::Split < NomenclatureChange build_steps(:inputs, :outputs, :notes, :children, :names, :distribution, :legislation, :summary) - attr_accessible :input_attributes, :outputs_attributes + # Migrated to controller (Strong Parameters) + # attr_accessible :input_attributes, :outputs_attributes has_one :input, :inverse_of => :nomenclature_change, :class_name => NomenclatureChange::Input, :foreign_key => :nomenclature_change_id, diff --git a/app/models/nomenclature_change/status_change_helpers.rb b/app/models/nomenclature_change/status_change_helpers.rb index 6a93d7770..77f80bf85 100644 --- a/app/models/nomenclature_change/status_change_helpers.rb +++ b/app/models/nomenclature_change/status_change_helpers.rb @@ -2,8 +2,9 @@ module NomenclatureChange::StatusChangeHelpers def self.included(base) base.class_eval do - attr_accessible :primary_output_attributes, :secondary_output_attributes, - :input_attributes + # Migrated to controller (Strong Parameters) + # attr_accessible :primary_output_attributes, :secondary_output_attributes, + # :input_attributes has_one :input, :inverse_of => :nomenclature_change, :class_name => NomenclatureChange::Input, :foreign_key => :nomenclature_change_id, diff --git a/app/models/nomenclature_change/status_to_accepted.rb b/app/models/nomenclature_change/status_to_accepted.rb index ec5f0e42e..d91467610 100644 --- a/app/models/nomenclature_change/status_to_accepted.rb +++ b/app/models/nomenclature_change/status_to_accepted.rb @@ -15,7 +15,8 @@ # A status change to A needs to have one output. class NomenclatureChange::StatusToAccepted < NomenclatureChange include NomenclatureChange::StatusChangeHelpers - attr_accessible :created_by_id, :updated_by_id + # Migrated to controller (Strong Parameters) + # attr_accessible :created_by_id, :updated_by_id build_steps( :primary_output, :summary ) From c0b1ad9e194cff5f3df777b443c48fea06f58f40 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Thu, 18 Jan 2024 10:39:04 +0000 Subject: [PATCH 020/241] strong params (round 3) (missed) --- app/models/nomenclature_change.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/nomenclature_change.rb b/app/models/nomenclature_change.rb index 832305892..a8569a65f 100644 --- a/app/models/nomenclature_change.rb +++ b/app/models/nomenclature_change.rb @@ -17,7 +17,8 @@ class NomenclatureChange < ActiveRecord::Base include StatusDictionary build_steps track_who_does_it - attr_accessible :event_id, :status + # Migrated to controller (Strong Parameters) + # attr_accessible :event_id, :status belongs_to :event From d9c35910a81619426c59334190b96c8734e43ee8 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Thu, 18 Jan 2024 10:39:18 +0000 Subject: [PATCH 021/241] strong params (round 4) add comments --- app/models/annotation.rb | 2 ++ app/models/change_type.rb | 1 + app/models/cites_ac.rb | 1 + app/models/cites_cop.rb | 1 + app/models/cites_extraordinary_meeting.rb | 1 + app/models/cites_pc.rb | 1 + app/models/cites_process.rb | 1 + app/models/cites_suspension.rb | 1 + app/models/cites_suspension_confirmation.rb | 1 + app/models/cites_suspension_notification.rb | 1 + app/models/cites_tc.rb | 1 + app/models/cms_mapping.rb | 1 + app/models/comment.rb | 1 + app/models/designation.rb | 1 + app/models/distribution.rb | 1 + app/models/document.rb | 1 + app/models/document_citation.rb | 1 + app/models/ec_srg.rb | 1 + app/models/eu_decision.rb | 1 + app/models/eu_decision_type.rb | 1 + app/models/eu_opinion.rb | 1 + app/models/eu_regulation.rb | 1 + app/models/eu_suspension_regulation.rb | 1 + app/models/event.rb | 1 + app/models/geo_entity.rb | 1 + app/models/geo_relationship.rb | 1 + app/models/instrument.rb | 1 + app/models/language.rb | 1 + app/models/listing_change.rb | 1 + app/models/listing_distribution.rb | 1 + app/models/preset_tag.rb | 1 + app/models/quota.rb | 2 +- app/models/rank.rb | 1 + app/models/reference.rb | 1 + app/models/species_listing.rb | 1 + app/models/srg_history.rb | 1 + app/models/taxon_common.rb | 1 + app/models/taxon_concept.rb | 1 + app/models/taxon_concept_reference.rb | 1 + app/models/taxon_instrument.rb | 1 + app/models/taxon_relationship.rb | 1 + app/models/taxonomy.rb | 1 + app/models/term_trade_codes_pair.rb | 1 + app/models/trade/taxon_concept_term_pair.rb | 1 + app/models/trade_code.rb | 1 + app/models/trade_restriction.rb | 1 + app/models/user.rb | 1 + 47 files changed, 48 insertions(+), 1 deletion(-) diff --git a/app/models/annotation.rb b/app/models/annotation.rb index ad0cf1bd9..11675ae57 100644 --- a/app/models/annotation.rb +++ b/app/models/annotation.rb @@ -24,6 +24,8 @@ class Annotation < ActiveRecord::Base track_who_does_it + + # Migrated to controller (Strong Parameters) # attr_accessible :listing_change_id, :symbol, :parent_symbol, :short_note_en, # :full_note_en, :short_note_fr, :full_note_fr, :short_note_es, :full_note_es, # :display_in_index, :display_in_footnote, :event_id diff --git a/app/models/change_type.rb b/app/models/change_type.rb index 602378679..66fd7aeec 100644 --- a/app/models/change_type.rb +++ b/app/models/change_type.rb @@ -13,6 +13,7 @@ # class ChangeType < ActiveRecord::Base + # Migrated to controller (Strong Parameters) # attr_accessible :name, :display_name_en, :display_name_es, :display_name_fr, # :designation_id include Dictionary diff --git a/app/models/cites_ac.rb b/app/models/cites_ac.rb index e6a65b556..dcff48034 100644 --- a/app/models/cites_ac.rb +++ b/app/models/cites_ac.rb @@ -26,6 +26,7 @@ # Cites Animal Committee class CitesAc < Event + # Migrated to controller (Strong Parameters) # attr_accessible :is_current validates :effective_at, :presence => true diff --git a/app/models/cites_cop.rb b/app/models/cites_cop.rb index 4e129ba0e..b92787a86 100644 --- a/app/models/cites_cop.rb +++ b/app/models/cites_cop.rb @@ -24,6 +24,7 @@ # class CitesCop < Event + # Migrated to controller (Strong Parameters) # attr_accessible :is_current has_many :listing_changes, :foreign_key => :event_id has_many :hash_annotations, :class_name => 'Annotation', :foreign_key => :event_id diff --git a/app/models/cites_extraordinary_meeting.rb b/app/models/cites_extraordinary_meeting.rb index fb5ff452f..21101aec6 100644 --- a/app/models/cites_extraordinary_meeting.rb +++ b/app/models/cites_extraordinary_meeting.rb @@ -24,6 +24,7 @@ # class CitesExtraordinaryMeeting < Event + # Migrated to controller (Strong Parameters) # attr_accessible :is_current validates :effective_at, :presence => true diff --git a/app/models/cites_pc.rb b/app/models/cites_pc.rb index b17133806..7aa7aa9ba 100644 --- a/app/models/cites_pc.rb +++ b/app/models/cites_pc.rb @@ -26,6 +26,7 @@ # Cites Plant Committee class CitesPc < Event + # Migrated to controller (Strong Parameters) # attr_accessible :is_current validates :effective_at, :presence => true diff --git a/app/models/cites_process.rb b/app/models/cites_process.rb index 13ede34dd..d794f10eb 100644 --- a/app/models/cites_process.rb +++ b/app/models/cites_process.rb @@ -1,5 +1,6 @@ class CitesProcess < ActiveRecord::Base track_who_does_it + # Migrated to controller (Strong Parameters) # attr_accessible :start_event_id, :geo_entity_id, :resolution, :start_date, # :taxon_concept_id, :notes, :status, :document, :document_title, # :created_by_id, :updated_by_id diff --git a/app/models/cites_suspension.rb b/app/models/cites_suspension.rb index f68cf27d1..f7765a83e 100644 --- a/app/models/cites_suspension.rb +++ b/app/models/cites_suspension.rb @@ -31,6 +31,7 @@ # class CitesSuspension < TradeRestriction + # Migrated to controller (Strong Parameters) # attr_accessible :start_notification_id, :end_notification_id, # :cites_suspension_confirmations_attributes, # :applies_to_import diff --git a/app/models/cites_suspension_confirmation.rb b/app/models/cites_suspension_confirmation.rb index bbabaffb3..68b86bfef 100644 --- a/app/models/cites_suspension_confirmation.rb +++ b/app/models/cites_suspension_confirmation.rb @@ -10,6 +10,7 @@ # class CitesSuspensionConfirmation < ActiveRecord::Base + # Migrated to controller (Strong Parameters) # attr_accessible :cites_suspension_notification_id belongs_to :confirmation_notification, :class_name => 'CitesSuspensionNotification', :foreign_key => :cites_suspension_notification_id diff --git a/app/models/cites_suspension_notification.rb b/app/models/cites_suspension_notification.rb index ce4703ff5..1c2e2aad1 100644 --- a/app/models/cites_suspension_notification.rb +++ b/app/models/cites_suspension_notification.rb @@ -24,6 +24,7 @@ # class CitesSuspensionNotification < Event + # Migrated to controller (Strong Parameters) # attr_accessible :subtype, :new_subtype, :end_date attr_accessor :new_subtype has_many :started_suspensions, :foreign_key => :start_notification_id, :class_name => 'CitesSuspension' diff --git a/app/models/cites_tc.rb b/app/models/cites_tc.rb index 31fa43cbc..a946cdae1 100644 --- a/app/models/cites_tc.rb +++ b/app/models/cites_tc.rb @@ -26,6 +26,7 @@ # Cites Tech Committee class CitesTc < Event + # Migrated to controller (Strong Parameters) # attr_accessible :is_current validates :effective_at, :presence => true diff --git a/app/models/cms_mapping.rb b/app/models/cms_mapping.rb index 21d6622ee..fb3b3febb 100644 --- a/app/models/cms_mapping.rb +++ b/app/models/cms_mapping.rb @@ -14,6 +14,7 @@ # class CmsMapping < ActiveRecord::Base + # Migrated to controller (Strong Parameters) # attr_accessible :accepted_name_id, :cms_author, :cms_taxon_name, :cms_uuid, :details, :taxon_concept_id # serialize :details, ActiveRecord::Coders::Hstore diff --git a/app/models/comment.rb b/app/models/comment.rb index 19cb9ba33..ad09ffc74 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -15,6 +15,7 @@ class Comment < ActiveRecord::Base track_who_does_it + # Migrated to controller (Strong Parameters) # attr_accessible :comment_type, :commentable_id, :commentable_type, :note, # :created_by_id, :updated_by_id belongs_to :commentable, polymorphic: true diff --git a/app/models/designation.rb b/app/models/designation.rb index 138c75110..3efc3307d 100644 --- a/app/models/designation.rb +++ b/app/models/designation.rb @@ -10,6 +10,7 @@ # class Designation < ActiveRecord::Base + # Migrated to controller (Strong Parameters) # attr_accessible :name, :taxonomy_id include Dictionary build_dictionary :cites, :eu, :cms diff --git a/app/models/distribution.rb b/app/models/distribution.rb index c26edf7f2..904c8e046 100644 --- a/app/models/distribution.rb +++ b/app/models/distribution.rb @@ -14,6 +14,7 @@ class Distribution < ActiveRecord::Base track_who_does_it + # Migrated to controller (Strong Parameters) # attr_accessible :geo_entity_id, :taxon_concept_id, :tag_list, # :references_attributes, :internal_notes, :created_by_id, :updated_by_id acts_as_taggable diff --git a/app/models/document.rb b/app/models/document.rb index 276499e12..8a569b937 100644 --- a/app/models/document.rb +++ b/app/models/document.rb @@ -30,6 +30,7 @@ class Document < ActiveRecord::Base :using => { :tsearch => { :prefix => true } }, :order_within_rank => "documents.date, documents.title, documents.id" track_who_does_it + # Migrated to controller (Strong Parameters) # attr_accessible :event_id, :filename, :date, :type, :title, :is_public, # :language_id, :citations_attributes, # :sort_index, :discussion_id, :discussion_sort_index, diff --git a/app/models/document_citation.rb b/app/models/document_citation.rb index cc3c473ac..002f7ff1a 100644 --- a/app/models/document_citation.rb +++ b/app/models/document_citation.rb @@ -13,6 +13,7 @@ class DocumentCitation < ActiveRecord::Base track_who_does_it + # Migrated to controller (Strong Parameters) # attr_accessible :document_id, :stringy_taxon_concept_ids, :geo_entity_ids has_many :document_citation_taxon_concepts, dependent: :destroy, autosave: true has_many :taxon_concepts, through: :document_citation_taxon_concepts diff --git a/app/models/ec_srg.rb b/app/models/ec_srg.rb index bea212d04..c22f6893c 100644 --- a/app/models/ec_srg.rb +++ b/app/models/ec_srg.rb @@ -26,6 +26,7 @@ # European Commission Scientific Review Group class EcSrg < Event + # Migrated to controller (Strong Parameters) # attr_accessible :is_current has_many :eu_opinions, :foreign_key => :start_event_id, diff --git a/app/models/eu_decision.rb b/app/models/eu_decision.rb index 936348e08..24552839d 100644 --- a/app/models/eu_decision.rb +++ b/app/models/eu_decision.rb @@ -30,6 +30,7 @@ require 'csv' class EuDecision < ActiveRecord::Base track_who_does_it + # Migrated to controller (Strong Parameters) # attr_accessible :end_date, :end_event_id, :geo_entity_id, :internal_notes, # :is_current, :notes, :start_date, :start_event_id, :eu_decision_type_id, # :taxon_concept_id, :type, :conditions_apply, :term_id, :source_id, diff --git a/app/models/eu_decision_type.rb b/app/models/eu_decision_type.rb index 94dff7df9..69339af37 100644 --- a/app/models/eu_decision_type.rb +++ b/app/models/eu_decision_type.rb @@ -11,6 +11,7 @@ # class EuDecisionType < ActiveRecord::Base + # Migrated to controller (Strong Parameters) # attr_accessible :name, :tooltip, :decision_type include Dictionary build_dictionary :negative_opinion, :positive_opinion, :no_opinion, diff --git a/app/models/eu_opinion.rb b/app/models/eu_opinion.rb index 0913297df..c74e0506b 100644 --- a/app/models/eu_opinion.rb +++ b/app/models/eu_opinion.rb @@ -27,6 +27,7 @@ # class EuOpinion < EuDecision + # Migrated to controller (Strong Parameters) # attr_accessible :document_id belongs_to :document diff --git a/app/models/eu_regulation.rb b/app/models/eu_regulation.rb index c53cb154b..3eebcf2ec 100644 --- a/app/models/eu_regulation.rb +++ b/app/models/eu_regulation.rb @@ -24,6 +24,7 @@ # class EuRegulation < Event + # Migrated to controller (Strong Parameters) # attr_accessible :listing_changes_event_id, :end_date attr_accessor :listing_changes_event_id diff --git a/app/models/eu_suspension_regulation.rb b/app/models/eu_suspension_regulation.rb index 39e300d1a..63cdfbfe8 100644 --- a/app/models/eu_suspension_regulation.rb +++ b/app/models/eu_suspension_regulation.rb @@ -24,6 +24,7 @@ # class EuSuspensionRegulation < Event + # Migrated to controller (Strong Parameters) # attr_accessible :eu_suspensions_event_id attr_accessor :eu_suspensions_event_id diff --git a/app/models/event.rb b/app/models/event.rb index 16dd1e5dd..1ae3fe8eb 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -26,6 +26,7 @@ class Event < ActiveRecord::Base track_who_does_it + # Migrated to controller (Strong Parameters) # attr_accessible :name, :designation_id, :description, :extended_description, # :url, :private_url, :multilingual_url, :published_at, :effective_at, :is_current, :end_date, # :created_by_id, :updated_by_id diff --git a/app/models/geo_entity.rb b/app/models/geo_entity.rb index d21b9d9ba..a5a89b34b 100644 --- a/app/models/geo_entity.rb +++ b/app/models/geo_entity.rb @@ -18,6 +18,7 @@ # class GeoEntity < ActiveRecord::Base + # Migrated to controller (Strong Parameters) # attr_accessible :geo_entity_type_id, :iso_code2, :iso_code3, # :legacy_id, :legacy_type, :long_name, :name_en, :name_es, :name_fr, # :is_current diff --git a/app/models/geo_relationship.rb b/app/models/geo_relationship.rb index e1f3028f0..8c010de0c 100644 --- a/app/models/geo_relationship.rb +++ b/app/models/geo_relationship.rb @@ -11,6 +11,7 @@ # class GeoRelationship < ActiveRecord::Base + # Migrated to controller (Strong Parameters) # attr_accessible :geo_entity_id, :geo_relationship_type_id, :other_geo_entity_id belongs_to :geo_relationship_type belongs_to :geo_entity diff --git a/app/models/instrument.rb b/app/models/instrument.rb index a8e52a58c..329b4e5bc 100644 --- a/app/models/instrument.rb +++ b/app/models/instrument.rb @@ -10,6 +10,7 @@ # class Instrument < ActiveRecord::Base + # Migrated to controller (Strong Parameters) # attr_accessible :designation_id, :name validates :name, :presence => true, :uniqueness => { :scope => :designation_id } diff --git a/app/models/language.rb b/app/models/language.rb index 234d21134..61a919e56 100644 --- a/app/models/language.rb +++ b/app/models/language.rb @@ -13,6 +13,7 @@ # class Language < ActiveRecord::Base + # Migrated to controller (Strong Parameters) # attr_accessible :iso_code1, :iso_code3, :name_en, :name_fr, :name_es translates :name diff --git a/app/models/listing_change.rb b/app/models/listing_change.rb index 1b1ac9c51..fcac058c8 100644 --- a/app/models/listing_change.rb +++ b/app/models/listing_change.rb @@ -28,6 +28,7 @@ class ListingChange < ActiveRecord::Base track_who_does_it + # Migrated to controller (Strong Parameters) # attr_accessible :taxon_concept_id, :species_listing_id, :change_type_id, # :effective_at, :is_current, :parent_id, :geo_entity_ids, # :party_listing_distribution_attributes, :inclusion_taxon_concept_id, diff --git a/app/models/listing_distribution.rb b/app/models/listing_distribution.rb index a69275486..9f7876d94 100644 --- a/app/models/listing_distribution.rb +++ b/app/models/listing_distribution.rb @@ -15,6 +15,7 @@ class ListingDistribution < ActiveRecord::Base track_who_does_it + # Migrated to controller (Strong Parameters) # attr_accessible :geo_entity_id, :listing_change_id, :is_party belongs_to :geo_entity belongs_to :listing_change diff --git a/app/models/preset_tag.rb b/app/models/preset_tag.rb index 276d370a0..cbe54e402 100644 --- a/app/models/preset_tag.rb +++ b/app/models/preset_tag.rb @@ -10,6 +10,7 @@ # class PresetTag < ActiveRecord::Base + # Migrated to controller (Strong Parameters) # attr_accessible :model, :name TYPES = { diff --git a/app/models/quota.rb b/app/models/quota.rb index 5c4b752bc..cf1d0dcc4 100644 --- a/app/models/quota.rb +++ b/app/models/quota.rb @@ -31,7 +31,7 @@ # class Quota < TradeRestriction - + # Migrated to controller (Strong Parameters) # attr_accessible :public_display validates :quota, :presence => true diff --git a/app/models/rank.rb b/app/models/rank.rb index 8a5f372c2..765a9aacc 100644 --- a/app/models/rank.rb +++ b/app/models/rank.rb @@ -14,6 +14,7 @@ # class Rank < ActiveRecord::Base + # Migrated to controller (Strong Parameters) # attr_accessible :name, :display_name_en, :display_name_es, :display_name_fr, # :taxonomic_position, :fixed_order include Dictionary diff --git a/app/models/reference.rb b/app/models/reference.rb index dd2e21215..21566ef1a 100644 --- a/app/models/reference.rb +++ b/app/models/reference.rb @@ -18,6 +18,7 @@ class Reference < ActiveRecord::Base track_who_does_it + # Migrated to controller (Strong Parameters) # attr_accessible :citation, :created_by_id, :updated_by_id validates :citation, :presence => true diff --git a/app/models/species_listing.rb b/app/models/species_listing.rb index 6c1676cca..433371648 100644 --- a/app/models/species_listing.rb +++ b/app/models/species_listing.rb @@ -11,6 +11,7 @@ # class SpeciesListing < ActiveRecord::Base + # Migrated to controller (Strong Parameters) # attr_accessible :designation_id, :name, :abbreviation belongs_to :designation diff --git a/app/models/srg_history.rb b/app/models/srg_history.rb index faabb9de0..446a99e33 100644 --- a/app/models/srg_history.rb +++ b/app/models/srg_history.rb @@ -1,4 +1,5 @@ class SrgHistory < ActiveRecord::Base + # Migrated to controller (Strong Parameters) # attr_accessible :name, :tooltip has_many :eu_decisions diff --git a/app/models/taxon_common.rb b/app/models/taxon_common.rb index 22d74e487..1f92a1564 100644 --- a/app/models/taxon_common.rb +++ b/app/models/taxon_common.rb @@ -13,6 +13,7 @@ class TaxonCommon < ActiveRecord::Base track_who_does_it + # Migrated to controller (Strong Parameters) # attr_accessible :common_name_id, :taxon_concept_id, :created_by_id, # :updated_by_id, :name, :language_id attr_accessor :name, :language_id diff --git a/app/models/taxon_concept.rb b/app/models/taxon_concept.rb index 045b90cce..d8d40bf1b 100644 --- a/app/models/taxon_concept.rb +++ b/app/models/taxon_concept.rb @@ -42,6 +42,7 @@ class TaxonConcept < ActiveRecord::Base rank_name: :rank_name } + # Migrated to controller (Strong Parameters) # attr_accessible :parent_id, :taxonomy_id, :rank_id, # :parent_id, :author_year, :taxon_name_id, :taxonomic_position, # :legacy_id, :legacy_type, :scientific_name, :name_status, diff --git a/app/models/taxon_concept_reference.rb b/app/models/taxon_concept_reference.rb index cce24be17..45e39b885 100644 --- a/app/models/taxon_concept_reference.rb +++ b/app/models/taxon_concept_reference.rb @@ -16,6 +16,7 @@ class TaxonConceptReference < ActiveRecord::Base track_who_does_it + # Migrated to controller (Strong Parameters) # attr_accessible :reference_id, :taxon_concept_id, :is_standard, :is_cascaded, # :excluded_taxon_concepts_ids, :reference_attributes, # :created_by_id, :updated_by_id diff --git a/app/models/taxon_instrument.rb b/app/models/taxon_instrument.rb index 57cfaca40..012d13f82 100644 --- a/app/models/taxon_instrument.rb +++ b/app/models/taxon_instrument.rb @@ -14,6 +14,7 @@ class TaxonInstrument < ActiveRecord::Base track_who_does_it + # Migrated to controller (Strong Parameters) # attr_accessible :effective_from, :instrument_id, :taxon_concept_id belongs_to :instrument diff --git a/app/models/taxon_relationship.rb b/app/models/taxon_relationship.rb index 09eeaa932..76032d46c 100644 --- a/app/models/taxon_relationship.rb +++ b/app/models/taxon_relationship.rb @@ -14,6 +14,7 @@ class TaxonRelationship < ActiveRecord::Base track_who_does_it + # Migrated to controller (Strong Parameters) # attr_accessible :taxon_concept_id, :other_taxon_concept_id, :taxon_relationship_type_id, # :created_by_id, :updated_by_id belongs_to :taxon_relationship_type diff --git a/app/models/taxonomy.rb b/app/models/taxonomy.rb index 96faee418..fb31c3fb9 100644 --- a/app/models/taxonomy.rb +++ b/app/models/taxonomy.rb @@ -12,6 +12,7 @@ class Taxonomy < ActiveRecord::Base include Dictionary build_dictionary :cites_eu, :cms + # Migrated to controller (Strong Parameters) # attr_accessible :name has_many :designations has_many :taxon_concepts diff --git a/app/models/term_trade_codes_pair.rb b/app/models/term_trade_codes_pair.rb index c1a4cf5ef..c5dd1545c 100644 --- a/app/models/term_trade_codes_pair.rb +++ b/app/models/term_trade_codes_pair.rb @@ -11,6 +11,7 @@ # class TermTradeCodesPair < ActiveRecord::Base + # Migrated to controller (Strong Parameters) # attr_accessible :trade_code_id, :trade_code_type, :term_id belongs_to :term, :class_name => "TradeCode" diff --git a/app/models/trade/taxon_concept_term_pair.rb b/app/models/trade/taxon_concept_term_pair.rb index 50b2421a8..781164679 100644 --- a/app/models/trade/taxon_concept_term_pair.rb +++ b/app/models/trade/taxon_concept_term_pair.rb @@ -10,6 +10,7 @@ # class Trade::TaxonConceptTermPair < ActiveRecord::Base + # Migrated to controller (Strong Parameters) # attr_accessible :taxon_concept_id, :term_id validates :taxon_concept_id, :presence => true validates :term_id, :presence => true diff --git a/app/models/trade_code.rb b/app/models/trade_code.rb index f179bbe0b..d633df6a4 100644 --- a/app/models/trade_code.rb +++ b/app/models/trade_code.rb @@ -13,6 +13,7 @@ # class TradeCode < ActiveRecord::Base + # Migrated to controller (Strong Parameters) # attr_accessible :code, :type, :name_en, :name_es, :name_fr translates :name diff --git a/app/models/trade_restriction.rb b/app/models/trade_restriction.rb index 36fe13b55..ada48d8fb 100644 --- a/app/models/trade_restriction.rb +++ b/app/models/trade_restriction.rb @@ -34,6 +34,7 @@ require 'csv' class TradeRestriction < ActiveRecord::Base track_who_does_it + # Migrated to controller (Strong Parameters) # attr_accessible :end_date, :geo_entity_id, :is_current, # :notes, :publication_date, :purpose_ids, :quota, :type, # :source_ids, :start_date, :term_ids, :unit_id, :internal_notes, diff --git a/app/models/user.rb b/app/models/user.rb index f2772b05d..d730345bc 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -27,6 +27,7 @@ class User < ActiveRecord::Base include SentientUser devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable + # Migrated to controller (Strong Parameters) # attr_accessible :email, :name, :password, :password_confirmation, # :remember_me, :role, :terms_and_conditions, :is_cites_authority, # :organisation, :geo_entity_id, :is_active From fdb91a8cb03241a05b3ddab34450def5d095b234 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Thu, 18 Jan 2024 12:37:45 +0000 Subject: [PATCH 022/241] strong params (round 5), trace from model --- app/controllers/admin/documents_controller.rb | 7 +++++ .../checklist/downloads_controller.rb | 2 +- app/models/analytics_event.rb | 3 ++- app/models/cites_rst_process.rb | 3 ++- app/models/common_name.rb | 5 ++-- app/models/designation_geo_entity.rb | 4 ++- app/models/distribution_reference.rb | 5 ++-- app/models/document/proposal.rb | 3 ++- app/models/document/proposal_details.rb | 5 ++-- app/models/document/review_details.rb | 3 ++- .../document/review_of_significant_trade.rb | 3 ++- app/models/document_citation_geo_entity.rb | 4 ++- app/models/document_citation_taxon_concept.rb | 5 ++-- app/models/document_tag.rb | 3 ++- app/models/download.rb | 3 ++- app/models/eu_country_date.rb | 3 ++- app/models/eu_decision_confirmation.rb | 4 ++- app/models/geo_entity_type.rb | 3 ++- app/models/geo_relationship_type.rb | 3 ++- app/models/iucn_mapping.rb | 5 ++-- .../reassignment_target.rb | 5 ++-- app/models/taxon_name.rb | 3 ++- app/models/taxon_relationship_type.rb | 3 ++- app/models/trade/annual_report_upload.rb | 7 ++--- app/models/trade/format_validation_rule.rb | 3 ++- app/models/trade/inclusion_validation_rule.rb | 3 ++- app/models/trade/permit.rb | 3 ++- app/models/trade/sandbox_template.rb | 27 ++++++++++--------- app/models/trade/shipment.rb | 17 ++++++------ app/models/trade/trade_data_download.rb | 8 +++--- app/models/trade/validation_error.rb | 16 ++++++----- app/models/trade/validation_rule.rb | 3 ++- app/models/trade_restriction_purpose.rb | 3 ++- app/models/trade_restriction_source.rb | 3 ++- app/models/trade_restriction_term.rb | 3 ++- app/models/year_annual_reports_by_country.rb | 4 +-- config/initializers/acts_as_taggable_on.rb | 13 ++++----- config/initializers/paper_trail.rb | 15 ++++++----- 38 files changed, 130 insertions(+), 85 deletions(-) diff --git a/app/controllers/admin/documents_controller.rb b/app/controllers/admin/documents_controller.rb index 283eab48f..658ce0e4a 100644 --- a/app/controllers/admin/documents_controller.rb +++ b/app/controllers/admin/documents_controller.rb @@ -151,6 +151,13 @@ def document_params :designation_id, citations_attributes: [ :id, :_destroy, :document_id, :stringy_taxon_concept_ids, :geo_entity_ids + ], + proposal_details_attributes: [ + :id, :_destroy, :document_id, :proposal_nature, :proposal_outcome_id, + :representation, :proposal_number + ], + review_details_attributes: [ + :id, :_destroy, :document_id, :review_phase_id, :process_stage_id, :recommended_category ] ) end diff --git a/app/controllers/checklist/downloads_controller.rb b/app/controllers/checklist/downloads_controller.rb index 897d080c5..600cf2267 100644 --- a/app/controllers/checklist/downloads_controller.rb +++ b/app/controllers/checklist/downloads_controller.rb @@ -96,7 +96,7 @@ def not_found def download_params params.require(:download).permit( # attributes used in this controller. - :doc_typ, + :doc_type, # other attributes were in model `attr_accessible`. :format ) diff --git a/app/models/analytics_event.rb b/app/models/analytics_event.rb index 8b333c3b8..c2b4bd14f 100644 --- a/app/models/analytics_event.rb +++ b/app/models/analytics_event.rb @@ -6,5 +6,6 @@ class AnalyticsEvent < ActiveRecord::Base validates :event_name, inclusion: { in: EVENT_NAMES }, presence: true validates :event_type, inclusion: { in: EVENT_TYPES }, presence: true - attr_accessible :event_name, :event_type + # Used by `download_db` in app/controllers/cites_trade/home_controller.rb + # attr_accessible :event_name, :event_type end diff --git a/app/models/cites_rst_process.rb b/app/models/cites_rst_process.rb index f7e0a546c..619e2971e 100644 --- a/app/models/cites_rst_process.rb +++ b/app/models/cites_rst_process.rb @@ -1,6 +1,7 @@ class CitesRstProcess < CitesProcess - attr_accessible :case_id + # Used by lib/modules/import/rst/importer.rb + # attr_accessible :case_id STATUS = ['Initiated', 'Ongoing', 'Retained', 'Trade Suspension', 'Closed'] diff --git a/app/models/common_name.rb b/app/models/common_name.rb index d43fe50e1..5ac437323 100644 --- a/app/models/common_name.rb +++ b/app/models/common_name.rb @@ -13,8 +13,9 @@ class CommonName < ActiveRecord::Base track_who_does_it - attr_accessible :language_id, :name, - :created_by_id, :updated_by_id + # Used by app/models/taxon_common.rb + # attr_accessible :language_id, :name, + # :created_by_id, :updated_by_id belongs_to :language validates :name, :presence => true, :uniqueness => { :scope => :language_id } diff --git a/app/models/designation_geo_entity.rb b/app/models/designation_geo_entity.rb index 33a76f386..6295a5002 100644 --- a/app/models/designation_geo_entity.rb +++ b/app/models/designation_geo_entity.rb @@ -10,6 +10,8 @@ # class DesignationGeoEntity < ActiveRecord::Base - attr_accessible :designation_id, :geo_entity_id + # Relationship table between Designation and GeoEntity. + # attr_accessible :designation_id, :geo_entity_id belongs_to :designation + # TODO: missing `belongs_to :geo_entity` end diff --git a/app/models/distribution_reference.rb b/app/models/distribution_reference.rb index 5010c832d..7ea850d24 100644 --- a/app/models/distribution_reference.rb +++ b/app/models/distribution_reference.rb @@ -13,8 +13,9 @@ class DistributionReference < ActiveRecord::Base track_who_does_it - attr_accessible :reference_id, :distribution_id, :created_by_id, - :updated_by_id + # Used by app/models/cms_mapping_manager.rb + # attr_accessible :reference_id, :distribution_id, :created_by_id, + # :updated_by_id belongs_to :reference belongs_to :distribution, :touch => true diff --git a/app/models/document/proposal.rb b/app/models/document/proposal.rb index 332659708..49ec4dc02 100644 --- a/app/models/document/proposal.rb +++ b/app/models/document/proposal.rb @@ -29,7 +29,8 @@ def self.display_name 'Proposal' end - attr_accessible :proposal_details_attributes + # Used in Document Controller + # attr_accessible :proposal_details_attributes has_one :proposal_details, :class_name => 'Document::ProposalDetails', diff --git a/app/models/document/proposal_details.rb b/app/models/document/proposal_details.rb index 866921183..6c9c2ec95 100644 --- a/app/models/document/proposal_details.rb +++ b/app/models/document/proposal_details.rb @@ -13,8 +13,9 @@ # class Document::ProposalDetails < ActiveRecord::Base - attr_accessible :document_id, :proposal_nature, :proposal_outcome_id, - :representation, :proposal_number + # Used in DocumentsController + # attr_accessible :document_id, :proposal_nature, :proposal_outcome_id, + # :representation, :proposal_number self.table_name = 'proposal_details' belongs_to :document, touch: true end diff --git a/app/models/document/review_details.rb b/app/models/document/review_details.rb index 7fe8f3468..5af2d2949 100644 --- a/app/models/document/review_details.rb +++ b/app/models/document/review_details.rb @@ -12,7 +12,8 @@ # class Document::ReviewDetails < ActiveRecord::Base - attr_accessible :document_id, :review_phase_id, :process_stage_id, :recommended_category + # Used by DocumentController. + # attr_accessible :document_id, :review_phase_id, :process_stage_id, :recommended_category self.table_name = 'review_details' belongs_to :document, touch: true diff --git a/app/models/document/review_of_significant_trade.rb b/app/models/document/review_of_significant_trade.rb index be6c7fd08..f834612f6 100644 --- a/app/models/document/review_of_significant_trade.rb +++ b/app/models/document/review_of_significant_trade.rb @@ -29,7 +29,8 @@ def self.display_name 'Review of Significant Trade' end - attr_accessible :review_details_attributes + # Used by DocumentsController. + # attr_accessible :review_details_attributes has_one :review_details, :class_name => 'Document::ReviewDetails', diff --git a/app/models/document_citation_geo_entity.rb b/app/models/document_citation_geo_entity.rb index 4110c7f60..d5d4d0de4 100644 --- a/app/models/document_citation_geo_entity.rb +++ b/app/models/document_citation_geo_entity.rb @@ -13,7 +13,9 @@ class DocumentCitationGeoEntity < ActiveRecord::Base track_who_does_it - attr_accessible :created_by_id, :document_citation_id, :geo_entity_id, :updated_by_id + # Used by app/models/nomenclature_change/reassignment_copy_processor.rb and lib/tasks/elibrary/identification_docs_distributions_importer.rb + # attr_accessible :created_by_id, :document_citation_id, :geo_entity_id, :updated_by_id + belongs_to :geo_entity belongs_to :document_citation, touch: true validates :geo_entity_id, uniqueness: { diff --git a/app/models/document_citation_taxon_concept.rb b/app/models/document_citation_taxon_concept.rb index 02877f825..39cd90a4a 100644 --- a/app/models/document_citation_taxon_concept.rb +++ b/app/models/document_citation_taxon_concept.rb @@ -13,8 +13,9 @@ class DocumentCitationTaxonConcept < ActiveRecord::Base track_who_does_it - attr_accessible :created_by_id, :document_citation_id, :taxon_concept_id, :updated_by_id, - :updated_at + # Used by other models, not controllers. + # attr_accessible :created_by_id, :document_citation_id, :taxon_concept_id, :updated_by_id, + # :updated_at belongs_to :taxon_concept belongs_to :document_citation, touch: true validates :taxon_concept_id, uniqueness: { diff --git a/app/models/document_tag.rb b/app/models/document_tag.rb index 1cf024fad..ff573a982 100644 --- a/app/models/document_tag.rb +++ b/app/models/document_tag.rb @@ -10,7 +10,8 @@ # class DocumentTag < ActiveRecord::Base - attr_accessible :name + # Only created by seed and rake task. + # attr_accessible :name has_and_belongs_to_many :documents diff --git a/app/models/download.rb b/app/models/download.rb index 93a89d9c1..f144ddc97 100644 --- a/app/models/download.rb +++ b/app/models/download.rb @@ -14,7 +14,8 @@ # class Download < ActiveRecord::Base - attr_accessible :format, :doc_type + # Migrated to controller (Strong Parameters) + # attr_accessible :format, :doc_type validates :format, :presence => true, :inclusion => { :in => %w(pdf csv json zip) } validates :doc_type, :presence => true, :inclusion => { :in => %w(history index citesidmanual) } diff --git a/app/models/eu_country_date.rb b/app/models/eu_country_date.rb index a3a3920a3..0643cfd4f 100644 --- a/app/models/eu_country_date.rb +++ b/app/models/eu_country_date.rb @@ -1,5 +1,6 @@ class EuCountryDate < ActiveRecord::Base - attr_accessible :eu_accession_year, :eu_exit_year, :geo_entity + # Used by rake task. + # attr_accessible :eu_accession_year, :eu_exit_year, :geo_entity belongs_to :geo_entity validates :geo_entity, :eu_accession_year, :presence => true diff --git a/app/models/eu_decision_confirmation.rb b/app/models/eu_decision_confirmation.rb index 10baafabf..3d2dc4cba 100644 --- a/app/models/eu_decision_confirmation.rb +++ b/app/models/eu_decision_confirmation.rb @@ -10,7 +10,9 @@ # class EuDecisionConfirmation < ActiveRecord::Base - attr_accessible :eu_decision_id, :event_id + # Relationship table between Event and EuDecision + # attr_accessible :eu_decision_id, :event_id belongs_to :eu_decision + # TODO: missing `belongs_to :event` end diff --git a/app/models/geo_entity_type.rb b/app/models/geo_entity_type.rb index 98d31533d..31dd6c495 100644 --- a/app/models/geo_entity_type.rb +++ b/app/models/geo_entity_type.rb @@ -9,7 +9,8 @@ # class GeoEntityType < ActiveRecord::Base - attr_accessible :name + # Look like the only place create GeoEntityType is lib/tasks/import_trade_shipments.rake + # attr_accessible :name include Dictionary build_dictionary :country, :cites_region, :region, :territory, diff --git a/app/models/geo_relationship_type.rb b/app/models/geo_relationship_type.rb index 11a3ce7a5..23e65f5a9 100644 --- a/app/models/geo_relationship_type.rb +++ b/app/models/geo_relationship_type.rb @@ -9,7 +9,8 @@ # class GeoRelationshipType < ActiveRecord::Base - attr_accessible :name + # Used by seed only. + # attr_accessible :name include Dictionary build_dictionary :contains, :intersects diff --git a/app/models/iucn_mapping.rb b/app/models/iucn_mapping.rb index d3e7cc2c2..898ee944f 100644 --- a/app/models/iucn_mapping.rb +++ b/app/models/iucn_mapping.rb @@ -15,8 +15,9 @@ # class IucnMapping < ActiveRecord::Base - attr_accessible :iucn_author, :iucn_category, :iucn_taxon_id, - :iucn_taxon_name, :taxon_concept_id, :details, :accepted_name_id + # Used by IucnMappingManager + # attr_accessible :iucn_author, :iucn_category, :iucn_taxon_id, + # :iucn_taxon_name, :taxon_concept_id, :details, :accepted_name_id # serialize :details, ActiveRecord::Coders::Hstore belongs_to :taxon_concept diff --git a/app/models/nomenclature_change/reassignment_target.rb b/app/models/nomenclature_change/reassignment_target.rb index 8afc362e1..bd8f7f962 100644 --- a/app/models/nomenclature_change/reassignment_target.rb +++ b/app/models/nomenclature_change/reassignment_target.rb @@ -13,8 +13,9 @@ class NomenclatureChange::ReassignmentTarget < ActiveRecord::Base track_who_does_it - attr_accessible :nomenclature_change_output_id, - :nomenclature_change_reassignment_id, :note + # Relationship table + # attr_accessible :nomenclature_change_output_id, + # :nomenclature_change_reassignment_id, :note belongs_to :output, :class_name => NomenclatureChange::Output, :foreign_key => :nomenclature_change_output_id belongs_to :reassignment, diff --git a/app/models/taxon_name.rb b/app/models/taxon_name.rb index d213b1477..e0254f94a 100644 --- a/app/models/taxon_name.rb +++ b/app/models/taxon_name.rb @@ -9,7 +9,8 @@ # class TaxonName < ActiveRecord::Base - attr_accessible :basionym_id, :scientific_name + # Used by seed and rake task. + # attr_accessible :basionym_id, :scientific_name validates :scientific_name, :presence => true diff --git a/app/models/taxon_relationship_type.rb b/app/models/taxon_relationship_type.rb index da4fba33c..6fd9bab4e 100644 --- a/app/models/taxon_relationship_type.rb +++ b/app/models/taxon_relationship_type.rb @@ -11,7 +11,8 @@ # class TaxonRelationshipType < ActiveRecord::Base - attr_accessible :name, :is_intertaxonomic, :is_bidirectional + # Used by seed and rake task. + # attr_accessible :name, :is_intertaxonomic, :is_bidirectional include Dictionary build_dictionary :equal_to, :includes, :overlaps, :disjunct, :has_synonym, diff --git a/app/models/trade/annual_report_upload.rb b/app/models/trade/annual_report_upload.rb index 456a1c910..52de7aaa0 100644 --- a/app/models/trade/annual_report_upload.rb +++ b/app/models/trade/annual_report_upload.rb @@ -21,9 +21,10 @@ class Trade::AnnualReportUpload < ActiveRecord::Base include ActiveModel::ForbiddenAttributesProtection track_who_does_it - attr_accessible :csv_source_file, :trading_country_id, :point_of_view, - :submitted_at, :submitted_by_id, :number_of_rows, - :number_of_records_submitted, :aws_storage_path + # Suppose use in controller, but controller using strong parameters... + # attr_accessible :csv_source_file, :trading_country_id, :point_of_view, + # :submitted_at, :submitted_by_id, :number_of_rows, + # :number_of_records_submitted, :aws_storage_path mount_uploader :csv_source_file, Trade::CsvSourceFileUploader belongs_to :trading_country, :class_name => GeoEntity, :foreign_key => :trading_country_id validates :csv_source_file, :csv_column_headers => true, :on => :create diff --git a/app/models/trade/format_validation_rule.rb b/app/models/trade/format_validation_rule.rb index 22fab9d2b..14c6377c8 100644 --- a/app/models/trade/format_validation_rule.rb +++ b/app/models/trade/format_validation_rule.rb @@ -16,7 +16,8 @@ # class Trade::FormatValidationRule < Trade::ValidationRule - attr_accessible :format_re + # Only created by seed. + # attr_accessible :format_re def error_message column_names.join(', ') + ' must be formatted as ' + format_re diff --git a/app/models/trade/inclusion_validation_rule.rb b/app/models/trade/inclusion_validation_rule.rb index 0bc98ad59..e92c98658 100644 --- a/app/models/trade/inclusion_validation_rule.rb +++ b/app/models/trade/inclusion_validation_rule.rb @@ -16,7 +16,8 @@ # class Trade::InclusionValidationRule < Trade::ValidationRule - attr_accessible :valid_values_view + # Only created by seed. + # attr_accessible :valid_values_view def matching_records_for_aru_and_error(annual_report_upload, validation_error) @query = matching_records(annual_report_upload).where( diff --git a/app/models/trade/permit.rb b/app/models/trade/permit.rb index 2d2e7cddb..6d87f748e 100644 --- a/app/models/trade/permit.rb +++ b/app/models/trade/permit.rb @@ -9,5 +9,6 @@ # class Trade::Permit < ActiveRecord::Base - attr_accessible :number + # app/models/trade/shipment.rb is the only place create this record. + # attr_accessible :number end diff --git a/app/models/trade/sandbox_template.rb b/app/models/trade/sandbox_template.rb index e0850ca25..0dcebe97c 100644 --- a/app/models/trade/sandbox_template.rb +++ b/app/models/trade/sandbox_template.rb @@ -46,19 +46,20 @@ def self.ar_klass(table_name) self.table_name = table_name has_paper_trail include ActiveModel::ForbiddenAttributesProtection - attr_accessible :appendix, - :taxon_name, - :term_code, - :quantity, - :unit_code, - :trading_partner, - :country_of_origin, - :import_permit, - :export_permit, - :origin_permit, - :purpose_code, - :source_code, - :year + # Too dynamic, hard to trace where using it. + # attr_accessible :appendix, + # :taxon_name, + # :term_code, + # :quantity, + # :unit_code, + # :trading_partner, + # :country_of_origin, + # :import_permit, + # :export_permit, + # :origin_permit, + # :purpose_code, + # :source_code, + # :year belongs_to :taxon_concept belongs_to :reported_taxon_concept, :class_name => TaxonConcept diff --git a/app/models/trade/shipment.rb b/app/models/trade/shipment.rb index cd769d34f..e84059711 100644 --- a/app/models/trade/shipment.rb +++ b/app/models/trade/shipment.rb @@ -33,14 +33,15 @@ class Trade::Shipment < ActiveRecord::Base track_who_does_it - attr_accessible :annual_report_upload_id, :appendix, - :country_of_origin_id, :origin_permit_id, - :exporter_id, :import_permit_id, :importer_id, :purpose_id, - :quantity, :reporter_type, :reported_by_exporter, - :source_id, :taxon_concept_id, :reported_taxon_concept_id, - :term_id, :unit_id, :year, - :import_permit_number, :export_permit_number, :origin_permit_number, - :ignore_warnings, :created_by_id, :updated_by_id + # Not sure where using this. + # attr_accessible :annual_report_upload_id, :appendix, + # :country_of_origin_id, :origin_permit_id, + # :exporter_id, :import_permit_id, :importer_id, :purpose_id, + # :quantity, :reporter_type, :reported_by_exporter, + # :source_id, :taxon_concept_id, :reported_taxon_concept_id, + # :term_id, :unit_id, :year, + # :import_permit_number, :export_permit_number, :origin_permit_number, + # :ignore_warnings, :created_by_id, :updated_by_id attr_accessor :reporter_type, :warnings, :ignore_warnings validates :quantity, :presence => true, :numericality => { diff --git a/app/models/trade/trade_data_download.rb b/app/models/trade/trade_data_download.rb index 5b0ab52bd..03b332065 100644 --- a/app/models/trade/trade_data_download.rb +++ b/app/models/trade/trade_data_download.rb @@ -25,9 +25,9 @@ # class Trade::TradeDataDownload < ActiveRecord::Base - - attr_accessible :user_ip, :report_type, :year_from, :year_to, :taxon, - :appendix, :importer, :exporter, :origin, :term, :unit, :source, :purpose, - :number_of_rows, :city, :country, :organization + # Used by app/models/trade/trade_data_download_logger.rb + # attr_accessible :user_ip, :report_type, :year_from, :year_to, :taxon, + # :appendix, :importer, :exporter, :origin, :term, :unit, :source, :purpose, + # :number_of_rows, :city, :country, :organization end diff --git a/app/models/trade/validation_error.rb b/app/models/trade/validation_error.rb index 098bf11e0..fc73c1d04 100644 --- a/app/models/trade/validation_error.rb +++ b/app/models/trade/validation_error.rb @@ -1,11 +1,13 @@ class Trade::ValidationError < ActiveRecord::Base belongs_to :annual_report_upload, class_name: Trade::AnnualReportUpload belongs_to :validation_rule, class_name: Trade::ValidationRule - attr_accessible :annual_report_upload_id, - :validation_rule_id, - :matching_criteria, - :is_ignored, - :is_primary, - :error_message, - :error_count + + # Used by app/models/trade/validation_rule.rb + # attr_accessible :annual_report_upload_id, + # :validation_rule_id, + # :matching_criteria, + # :is_ignored, + # :is_primary, + # :error_message, + # :error_count end diff --git a/app/models/trade/validation_rule.rb b/app/models/trade/validation_rule.rb index 957b8aec9..c80c22727 100644 --- a/app/models/trade/validation_rule.rb +++ b/app/models/trade/validation_rule.rb @@ -16,7 +16,8 @@ # class Trade::ValidationRule < ActiveRecord::Base - attr_accessible :column_names, :run_order, :is_primary, :scope, :is_strict + # Used by seed. + # attr_accessible :column_names, :run_order, :is_primary, :scope, :is_strict serialize :scope, ActiveRecord::Coders::NestedHstore has_many :validation_errors, class_name: Trade::ValidationError diff --git a/app/models/trade_restriction_purpose.rb b/app/models/trade_restriction_purpose.rb index cbe4887cb..ee714b09e 100644 --- a/app/models/trade_restriction_purpose.rb +++ b/app/models/trade_restriction_purpose.rb @@ -13,7 +13,8 @@ class TradeRestrictionPurpose < ActiveRecord::Base track_who_does_it - attr_accessible :purpose_id, :trade_restriction_id + # Relationship model between TradeCode(purpose) and TradeRestriction + # attr_accessible :purpose_id, :trade_restriction_id belongs_to :trade_restriction belongs_to :purpose, :class_name => 'TradeCode' end diff --git a/app/models/trade_restriction_source.rb b/app/models/trade_restriction_source.rb index 408bd3458..bfcc487e5 100644 --- a/app/models/trade_restriction_source.rb +++ b/app/models/trade_restriction_source.rb @@ -13,7 +13,8 @@ class TradeRestrictionSource < ActiveRecord::Base track_who_does_it - attr_accessible :source_id, :trade_restriction_id + # Relationship model between TradeCode(source) and TradeRestriction + # attr_accessible :source_id, :trade_restriction_id belongs_to :trade_restriction belongs_to :source, :class_name => 'TradeCode' end diff --git a/app/models/trade_restriction_term.rb b/app/models/trade_restriction_term.rb index 5e8c3f8b2..465cea6d9 100644 --- a/app/models/trade_restriction_term.rb +++ b/app/models/trade_restriction_term.rb @@ -13,7 +13,8 @@ class TradeRestrictionTerm < ActiveRecord::Base track_who_does_it - attr_accessible :term_id, :trade_restriction_id + # Relationship model between TradeCode(term) and TradeRestriction + # attr_accessible :term_id, :trade_restriction_id belongs_to :trade_restriction belongs_to :term, :class_name => 'TradeCode' end diff --git a/app/models/year_annual_reports_by_country.rb b/app/models/year_annual_reports_by_country.rb index ebbb71d46..d5dde3a66 100644 --- a/app/models/year_annual_reports_by_country.rb +++ b/app/models/year_annual_reports_by_country.rb @@ -10,6 +10,6 @@ # class YearAnnualReportsByCountry < ActiveRecord::Base - attr_accessible :no, :name_en, :year, :reporter_type, :year_created - + # No idea where using this. + # attr_accessible :no, :name_en, :year, :reporter_type, :year_created end diff --git a/config/initializers/acts_as_taggable_on.rb b/config/initializers/acts_as_taggable_on.rb index 63f51f3c4..78e8d0b79 100644 --- a/config/initializers/acts_as_taggable_on.rb +++ b/config/initializers/acts_as_taggable_on.rb @@ -1,7 +1,8 @@ -ActsAsTaggableOn::Tag.class_eval do - attr_accessible :name -end +# Migrated to Strong Parameters +# ActsAsTaggableOn::Tag.class_eval do +# attr_accessible :name +# end -ActsAsTaggableOn::Tagging.class_eval do - attr_accessible :tag_id, :context, :taggable, :taggable_id, :taggable_type, :tagger_id, :tagger_type -end +# ActsAsTaggableOn::Tagging.class_eval do +# attr_accessible :tag_id, :context, :taggable, :taggable_id, :taggable_type, :tagger_id, :tagger_type +# end diff --git a/config/initializers/paper_trail.rb b/config/initializers/paper_trail.rb index 2205fa8db..c1ba79436 100644 --- a/config/initializers/paper_trail.rb +++ b/config/initializers/paper_trail.rb @@ -1,15 +1,16 @@ # config/initializers/paper_trail.rb -# the following line is required for PaperTrail >= 4.0.0 with Rails +# the following line is required for PaperTrail >= 4.0.0 and < 12.0.0 with Rails PaperTrail::Rails::Engine.eager_load! class TaxonConceptVersion < PaperTrail::Version - attr_accessible :taxon_concept_id, - :taxonomy_name, - :full_name, - :author_year, - :name_status, - :rank_name + # Migrated to Strong Parameters + # attr_accessible :taxon_concept_id, + # :taxonomy_name, + # :full_name, + # :author_year, + # :name_status, + # :rank_name self.table_name = :taxon_concept_versions self.sequence_name = :taxon_concept_versions_id_seq end From 561fe1b0c0ab33f95edf04818d76f24dc9856cab Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Thu, 18 Jan 2024 15:57:02 +0000 Subject: [PATCH 023/241] update config for strong params, bugs fix, and fix rspec. (still got 1 spec failed) --- Gemfile | 1 - Gemfile.lock | 3 --- .../admin/nomenclature_changes/lump_controller.rb | 10 ++++++++-- .../admin/nomenclature_changes/split_controller.rb | 14 ++++++++++---- .../nomenclature_changes/status_swap_controller.rb | 8 ++++++-- .../status_to_accepted_controller.rb | 8 ++++++-- .../status_to_synonym_controller.rb | 8 ++++++-- app/controllers/admin/users_controller.rb | 6 +++--- .../reassignment_copy_processor.rb | 4 ++-- config/environments/development.rb | 4 ---- config/environments/test.rb | 4 ---- config/initializers/paper_trail.rb | 2 ++ .../admin/change_types_controller_spec.rb | 2 +- .../admin/cites_suspensions_controller_spec.rb | 2 +- .../admin/designations_controller_spec.rb | 2 +- .../admin/eu_opinions_controller_spec.rb | 2 +- .../admin/instruments_controller_spec.rb | 2 +- .../controllers/admin/languages_controller_spec.rb | 2 +- .../status_swap_controller_spec.rb | 2 +- .../status_to_accepted_controller_spec.rb | 2 +- .../status_to_synonym_controller_spec.rb | 2 +- spec/controllers/admin/ranks_controller_spec.rb | 2 +- .../admin/species_listings_controller_spec.rb | 2 +- .../admin/srg_histories_controller_spec.rb | 4 ++-- spec/controllers/admin/tags_controller_spec.rb | 8 ++++---- .../taxon_cites_suspensions_controller_spec.rb | 2 +- .../admin/taxon_commons_controller_spec.rb | 3 +-- .../admin/taxon_concepts_controller_spec.rb | 6 +++--- .../admin/taxon_eu_suspensions_controller_spec.rb | 2 +- .../admin/taxon_listing_changes_controller_spec.rb | 2 +- .../admin/taxon_quotas_controller_spec.rb | 2 +- 31 files changed, 68 insertions(+), 55 deletions(-) diff --git a/Gemfile b/Gemfile index f4f25bb45..0e74c3d4d 100644 --- a/Gemfile +++ b/Gemfile @@ -20,7 +20,6 @@ gem 'oj', '3.14.2' # optimised JSON (picked by multi_json) # TODO: to upgrade to gem 'nokogiri', '1.12.5' # TODO: 1.12.5 is the last version support 2.5. New version need Ruby 2.6+ gem 'inherited_resources', '1.9.0' # Deprecated (https://github.com/activeadmin/inherited_resources#notice) # TODO: need upgrade when upgrade to Rails 6 gem 'traco', '~> 5.3', '>= 5.3.3' # TODO: latest version @ 2021. Suggest migrate to Mobility gem. -gem 'protected_attributes_continued', '1.2.4' # TODO: upgrade to latest after we successfully upgrade Rails to 5. gem 'devise', '4.4.3' # TODO: version 4.4.3 work under <=Rails 5.3 and <=Ruby 2.6 gem 'cancancan', '1.17.0' # TODO, can upgrade to 2.0 after Rails 5 gem 'ahoy_matey', '2.2.1' # TODO: latest 5.0.2. Can't upgrade to 3.0 until upgrade to Rails 5 diff --git a/Gemfile.lock b/Gemfile.lock index 85978d3cc..b553e0c07 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -350,8 +350,6 @@ GEM pdf-reader (~> 1.2) ruby-rc4 ttfunk (~> 1.0.3) - protected_attributes_continued (1.2.4) - activemodel (>= 4.0.1, < 6.0) pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) @@ -618,7 +616,6 @@ DEPENDENCIES pg_array_parser (~> 0.0.9) pg_search (= 2.3.0) prawn (= 0.13.2) - protected_attributes_continued (= 1.2.4) rack-cors (= 0.3.0) rack-livereload (= 0.3.11) rails (= 4.2.11.3) diff --git a/app/controllers/admin/nomenclature_changes/lump_controller.rb b/app/controllers/admin/nomenclature_changes/lump_controller.rb index f095d0c2c..f8303285d 100644 --- a/app/controllers/admin/nomenclature_changes/lump_controller.rb +++ b/app/controllers/admin/nomenclature_changes/lump_controller.rb @@ -62,14 +62,19 @@ def nomenclature_change_lump_params params.require(:nomenclature_change_lump).permit( :event_id, :status, inputs_attributes: [ + :id, :_destroy, :nomenclature_change_id, :taxon_concept_id, :note_en, :note_es, :note_fr, :internal_note, parent_reassignments_attributes: [ :id, :_destroy, - :reassignment_target_attributes, :type, :reassignable_id, :reassignable_type, :nomenclature_change_input_id, :nomenclature_change_output_id, - :note_en, :note_es, :note_fr, :internal_note, :output_ids + :note_en, :note_es, :note_fr, :internal_note, :output_ids, + reassignment_target_attributes: [ + :id, :_destroy, + :nomenclature_change_output_id, + :nomenclature_change_reassignment_id, :note + ] ], name_reassignments_attributes: [ :id, :_destroy, @@ -91,6 +96,7 @@ def nomenclature_change_lump_params ] ], output_attributes: [ + :id, :_destroy, :nomenclature_change_id, :taxon_concept_id, :new_taxon_concept_id, :rank_id, :new_scientific_name, :new_author_year, :new_name_status, :new_parent_id, :new_rank_id, :taxonomy_id, diff --git a/app/controllers/admin/nomenclature_changes/split_controller.rb b/app/controllers/admin/nomenclature_changes/split_controller.rb index b558f532d..4cc127c35 100644 --- a/app/controllers/admin/nomenclature_changes/split_controller.rb +++ b/app/controllers/admin/nomenclature_changes/split_controller.rb @@ -62,15 +62,20 @@ def klass def nomenclature_change_split_params params.require(:nomenclature_change_split).permit( :event_id, :status, - inputs_attributes: [ + input_attributes: [ + :id, :_destroy, :nomenclature_change_id, :taxon_concept_id, :note_en, :note_es, :note_fr, :internal_note, parent_reassignments_attributes: [ :id, :_destroy, - :reassignment_target_attributes, :type, :reassignable_id, :reassignable_type, :nomenclature_change_input_id, :nomenclature_change_output_id, - :note_en, :note_es, :note_fr, :internal_note, :output_ids + :note_en, :note_es, :note_fr, :internal_note, :output_ids, + reassignment_target_attributes: [ + :id, :_destroy, + :nomenclature_change_output_id, + :nomenclature_change_reassignment_id, :note + ] ], name_reassignments_attributes: [ :id, :_destroy, @@ -91,7 +96,8 @@ def nomenclature_change_split_params :note_en, :note_es, :note_fr, :internal_note, :output_ids ] ], - output_attributes: [ + outputs_attributes: [ + :id, :_destroy, :nomenclature_change_id, :taxon_concept_id, :new_taxon_concept_id, :rank_id, :new_scientific_name, :new_author_year, :new_name_status, :new_parent_id, :new_rank_id, :taxonomy_id, diff --git a/app/controllers/admin/nomenclature_changes/status_swap_controller.rb b/app/controllers/admin/nomenclature_changes/status_swap_controller.rb index 5c25d695c..393fef8b8 100644 --- a/app/controllers/admin/nomenclature_changes/status_swap_controller.rb +++ b/app/controllers/admin/nomenclature_changes/status_swap_controller.rb @@ -90,10 +90,14 @@ def nomenclature_change_status_swap_params :note_en, :note_es, :note_fr, :internal_note, parent_reassignments_attributes: [ :id, :_destroy, - :reassignment_target_attributes, :type, :reassignable_id, :reassignable_type, :nomenclature_change_input_id, :nomenclature_change_output_id, - :note_en, :note_es, :note_fr, :internal_note, :output_ids + :note_en, :note_es, :note_fr, :internal_note, :output_ids, + reassignment_target_attributes: [ + :id, :_destroy, + :nomenclature_change_output_id, + :nomenclature_change_reassignment_id, :note + ] ], name_reassignments_attributes: [ :id, :_destroy, diff --git a/app/controllers/admin/nomenclature_changes/status_to_accepted_controller.rb b/app/controllers/admin/nomenclature_changes/status_to_accepted_controller.rb index 3f3aecde4..0cdb352ab 100644 --- a/app/controllers/admin/nomenclature_changes/status_to_accepted_controller.rb +++ b/app/controllers/admin/nomenclature_changes/status_to_accepted_controller.rb @@ -78,10 +78,14 @@ def nomenclature_change_status_to_accepted_params :note_en, :note_es, :note_fr, :internal_note, parent_reassignments_attributes: [ :id, :_destroy, - :reassignment_target_attributes, :type, :reassignable_id, :reassignable_type, :nomenclature_change_input_id, :nomenclature_change_output_id, - :note_en, :note_es, :note_fr, :internal_note, :output_ids + :note_en, :note_es, :note_fr, :internal_note, :output_ids, + reassignment_target_attributes: [ + :id, :_destroy, + :nomenclature_change_output_id, + :nomenclature_change_reassignment_id, :note + ] ], name_reassignments_attributes: [ :id, :_destroy, diff --git a/app/controllers/admin/nomenclature_changes/status_to_synonym_controller.rb b/app/controllers/admin/nomenclature_changes/status_to_synonym_controller.rb index 6a5beca53..1808a6056 100644 --- a/app/controllers/admin/nomenclature_changes/status_to_synonym_controller.rb +++ b/app/controllers/admin/nomenclature_changes/status_to_synonym_controller.rb @@ -89,10 +89,14 @@ def nomenclature_change_status_to_synonym_params :note_en, :note_es, :note_fr, :internal_note, parent_reassignments_attributes: [ :id, :_destroy, - :reassignment_target_attributes, :type, :reassignable_id, :reassignable_type, :nomenclature_change_input_id, :nomenclature_change_output_id, - :note_en, :note_es, :note_fr, :internal_note, :output_ids + :note_en, :note_es, :note_fr, :internal_note, :output_ids, + reassignment_target_attributes: [ + :id, :_destroy, + :nomenclature_change_output_id, + :nomenclature_change_reassignment_id, :note + ] ], name_reassignments_attributes: [ :id, :_destroy, diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 578ee0654..6da5fcc65 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -18,10 +18,10 @@ def edit def update update_result = - if params[:user][:password].blank? - @user.update_without_password(params[:user]) + if user_params[:password].blank? + @user.update_without_password(user_params) else - @user.update_attributes(params[:user]) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + @user.update_attributes(user_params) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. end respond_to do |format| format.js { diff --git a/app/models/nomenclature_change/reassignment_copy_processor.rb b/app/models/nomenclature_change/reassignment_copy_processor.rb index 4be8f7e80..b2d99665c 100644 --- a/app/models/nomenclature_change/reassignment_copy_processor.rb +++ b/app/models/nomenclature_change/reassignment_copy_processor.rb @@ -131,7 +131,7 @@ def build_listing_change_associations(reassignable, copied_object) !copied_object.new_record? && exclusion.duplicates({ parent_id: copied_object.id }).first || copied_object.exclusions.build( - exclusion.comparison_attributes, :without_protection => true + exclusion.comparison_attributes ) end # annotation @@ -150,7 +150,7 @@ def build_trade_restriction_associations(reassignable, copied_object) !copied_object.new_record? && trade_restr_code.duplicates({ trade_restriction_id: copied_object.id }).first || copied_object.send(trade_restriction_codes).build( - trade_restr_code.comparison_attributes, :without_protection => true + trade_restr_code.comparison_attributes ) end end diff --git a/config/environments/development.rb b/config/environments/development.rb index 3c65a31ae..f0c7f2016 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -42,10 +42,6 @@ # Raises error for missing translations # config.action_view.raise_on_missing_translations = true - # TODO: Only support Rails version < 5 - # GEM `protected_attributes` settings (https://github.com/rails/protected_attributes#errors) - config.active_record.mass_assignment_sanitizer = :strict - # Custom cache settings config.cache_store = :memory_store, { size: 64.megabytes } diff --git a/config/environments/test.rb b/config/environments/test.rb index 4472f8ae8..6e180d753 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -41,10 +41,6 @@ # Raises error for missing translations # config.action_view.raise_on_missing_translations = true - # TODO: Only support Rails version < 5 - # GEM `protected_attributes` settings (https://github.com/rails/protected_attributes#errors) - config.active_record.mass_assignment_sanitizer = :strict - # Custom cache settings config.cache_store = :null_store diff --git a/config/initializers/paper_trail.rb b/config/initializers/paper_trail.rb index c1ba79436..9cfcb752e 100644 --- a/config/initializers/paper_trail.rb +++ b/config/initializers/paper_trail.rb @@ -1,5 +1,7 @@ # config/initializers/paper_trail.rb +PaperTrail.config.track_associations = false + # the following line is required for PaperTrail >= 4.0.0 and < 12.0.0 with Rails PaperTrail::Rails::Engine.eager_load! diff --git a/spec/controllers/admin/change_types_controller_spec.rb b/spec/controllers/admin/change_types_controller_spec.rb index 166354d29..1326a485c 100644 --- a/spec/controllers/admin/change_types_controller_spec.rb +++ b/spec/controllers/admin/change_types_controller_spec.rb @@ -25,7 +25,7 @@ expect(response).to render_template("create") end it "renders new when not successful" do - xhr :post, :create, change_type: {} + xhr :post, :create, change_type: { dummy: 'test' } expect(response).to render_template("new") end end diff --git a/spec/controllers/admin/cites_suspensions_controller_spec.rb b/spec/controllers/admin/cites_suspensions_controller_spec.rb index cfa6ce7e5..49e8c22e8 100644 --- a/spec/controllers/admin/cites_suspensions_controller_spec.rb +++ b/spec/controllers/admin/cites_suspensions_controller_spec.rb @@ -45,7 +45,7 @@ end end it "renders new when not successful" do - post :create, :cites_suspension => {} + post :create, :cites_suspension => { dummy: 'test'} expect(response).to render_template("new") end end diff --git a/spec/controllers/admin/designations_controller_spec.rb b/spec/controllers/admin/designations_controller_spec.rb index 52c724438..16d75c220 100644 --- a/spec/controllers/admin/designations_controller_spec.rb +++ b/spec/controllers/admin/designations_controller_spec.rb @@ -34,7 +34,7 @@ expect(response).to render_template("create") end it "renders new when not successful" do - xhr :post, :create, designation: {} + xhr :post, :create, designation: { dummy: 'test'} expect(response).to render_template("new") end end diff --git a/spec/controllers/admin/eu_opinions_controller_spec.rb b/spec/controllers/admin/eu_opinions_controller_spec.rb index c54d779d0..ed1911c10 100644 --- a/spec/controllers/admin/eu_opinions_controller_spec.rb +++ b/spec/controllers/admin/eu_opinions_controller_spec.rb @@ -77,7 +77,7 @@ context "when not successful" do it "renders new" do - post :create, eu_opinion: {}, + post :create, eu_opinion: { dummy: 'test' }, taxon_concept_id: @taxon_concept.id expect(response).to render_template("new") end diff --git a/spec/controllers/admin/instruments_controller_spec.rb b/spec/controllers/admin/instruments_controller_spec.rb index 335f3cb4c..8e9604117 100644 --- a/spec/controllers/admin/instruments_controller_spec.rb +++ b/spec/controllers/admin/instruments_controller_spec.rb @@ -34,7 +34,7 @@ expect(response).to render_template("create") end it "renders new when not successful" do - xhr :post, :create, instrument: {} + xhr :post, :create, instrument: { dummy: 'test' } expect(response).to render_template("new") end end diff --git a/spec/controllers/admin/languages_controller_spec.rb b/spec/controllers/admin/languages_controller_spec.rb index 6eab1781c..e6388d2f4 100644 --- a/spec/controllers/admin/languages_controller_spec.rb +++ b/spec/controllers/admin/languages_controller_spec.rb @@ -22,7 +22,7 @@ expect(response).to render_template("create") end it "renders new when not successful" do - xhr :post, :create, language: {} + xhr :post, :create, language: { dummy: 'test' } expect(response).to render_template("new") end end diff --git a/spec/controllers/admin/nomenclature_changes/status_swap_controller_spec.rb b/spec/controllers/admin/nomenclature_changes/status_swap_controller_spec.rb index 2f124144d..7fc49a850 100644 --- a/spec/controllers/admin/nomenclature_changes/status_swap_controller_spec.rb +++ b/spec/controllers/admin/nomenclature_changes/status_swap_controller_spec.rb @@ -90,7 +90,7 @@ end context 'when unsuccessful' do it 're-renders step' do - put :update, nomenclature_change_status_swap: {}, + put :update, nomenclature_change_status_swap: { dummy: 'test'}, nomenclature_change_id: @status_change.id, id: 'primary_output' expect(response).to render_template('primary_output') end diff --git a/spec/controllers/admin/nomenclature_changes/status_to_accepted_controller_spec.rb b/spec/controllers/admin/nomenclature_changes/status_to_accepted_controller_spec.rb index 1ad1a65ce..90416ae2b 100644 --- a/spec/controllers/admin/nomenclature_changes/status_to_accepted_controller_spec.rb +++ b/spec/controllers/admin/nomenclature_changes/status_to_accepted_controller_spec.rb @@ -63,7 +63,7 @@ end context 'when unsuccessful' do it 're-renders step' do - put :update, nomenclature_change_status_to_accepted: {}, + put :update, nomenclature_change_status_to_accepted: { dummy: 'test'}, nomenclature_change_id: @status_change.id, id: 'primary_output' expect(response).to render_template('primary_output') end diff --git a/spec/controllers/admin/nomenclature_changes/status_to_synonym_controller_spec.rb b/spec/controllers/admin/nomenclature_changes/status_to_synonym_controller_spec.rb index f750f8e7d..07370ae57 100644 --- a/spec/controllers/admin/nomenclature_changes/status_to_synonym_controller_spec.rb +++ b/spec/controllers/admin/nomenclature_changes/status_to_synonym_controller_spec.rb @@ -68,7 +68,7 @@ end context 'when unsuccessful' do it 're-renders step' do - put :update, nomenclature_change_status_to_synonym: {}, + put :update, nomenclature_change_status_to_synonym: { dummy: 'test'}, nomenclature_change_id: @status_change.id, id: 'primary_output' expect(response).to render_template('primary_output') end diff --git a/spec/controllers/admin/ranks_controller_spec.rb b/spec/controllers/admin/ranks_controller_spec.rb index dbe03df3f..82ebb203f 100644 --- a/spec/controllers/admin/ranks_controller_spec.rb +++ b/spec/controllers/admin/ranks_controller_spec.rb @@ -22,7 +22,7 @@ expect(response).to render_template("create") end it "renders new when not successful" do - xhr :post, :create, rank: {} + xhr :post, :create, rank: { dummy: 'test'} expect(response).to render_template("new") end end diff --git a/spec/controllers/admin/species_listings_controller_spec.rb b/spec/controllers/admin/species_listings_controller_spec.rb index bb8c91a41..870d1d096 100644 --- a/spec/controllers/admin/species_listings_controller_spec.rb +++ b/spec/controllers/admin/species_listings_controller_spec.rb @@ -25,7 +25,7 @@ expect(response).to render_template("create") end it "renders new when not successful" do - xhr :post, :create, species_listing: {} + xhr :post, :create, species_listing: { dummy: 'test' } expect(response).to render_template("new") end end diff --git a/spec/controllers/admin/srg_histories_controller_spec.rb b/spec/controllers/admin/srg_histories_controller_spec.rb index 2667db0ae..b0cd77095 100644 --- a/spec/controllers/admin/srg_histories_controller_spec.rb +++ b/spec/controllers/admin/srg_histories_controller_spec.rb @@ -25,7 +25,7 @@ context "when not successful" do it "renders new" do - post :create, srg_history: {}, format: :js + post :create, srg_history: { dummy: 'test' }, format: :js expect(response).to render_template("new") end @@ -39,7 +39,7 @@ context "when successful" do it "renders the create js template" do - put :update, id: @srg_history.id, format: :js + put :update, id: @srg_history.id, srg_history: { dummy: 'test' }, format: :js expect(response).to render_template("create") end diff --git a/spec/controllers/admin/tags_controller_spec.rb b/spec/controllers/admin/tags_controller_spec.rb index 3f4bb6d26..771c22a02 100644 --- a/spec/controllers/admin/tags_controller_spec.rb +++ b/spec/controllers/admin/tags_controller_spec.rb @@ -14,11 +14,11 @@ describe "XHR POST create" do it "renders create when successful" do xhr :post, :create, - preset_tag: { name: "Test Tag", model: "TaxonConcept" } + tag: { name: "Test Tag", model: "TaxonConcept" } expect(response).to render_template("create") end it "renders new when not successful" do - xhr :post, :create, preset_tag: {} + xhr :post, :create, tag: { dummy: 'test' } expect(response).to render_template("new") end end @@ -28,12 +28,12 @@ context "when JSON" do it "responds with 200 when successful" do xhr :put, :update, :format => 'json', :id => preset_tag.id, - :preset_tag => {} + :tag => { dummy: 'test' } expect(response).to be_success end it "responds with json error when not successful" do xhr :put, :update, :format => 'json', :id => preset_tag.id, - :preset_tag => { :model => 'FakeCategory' } + :tag => { :model => 'FakeCategory' } expect(JSON.parse(response.body)).to include('errors') end end diff --git a/spec/controllers/admin/taxon_cites_suspensions_controller_spec.rb b/spec/controllers/admin/taxon_cites_suspensions_controller_spec.rb index ed01e82bd..791677de2 100644 --- a/spec/controllers/admin/taxon_cites_suspensions_controller_spec.rb +++ b/spec/controllers/admin/taxon_cites_suspensions_controller_spec.rb @@ -44,7 +44,7 @@ end end it "renders new when not successful" do - post :create, :cites_suspension => {}, + post :create, :cites_suspension => { dummy: 'test'}, :taxon_concept_id => @taxon_concept.id expect(response).to render_template('new') end diff --git a/spec/controllers/admin/taxon_commons_controller_spec.rb b/spec/controllers/admin/taxon_commons_controller_spec.rb index 4682c712a..783816ccf 100644 --- a/spec/controllers/admin/taxon_commons_controller_spec.rb +++ b/spec/controllers/admin/taxon_commons_controller_spec.rb @@ -29,8 +29,7 @@ it "renders new when not successful" do xhr :post, :create, :taxon_concept_id => @taxon_concept.id, - :taxon_common => { - } + :taxon_common => { dummy: 'test' } expect(response).to render_template("new") end end diff --git a/spec/controllers/admin/taxon_concepts_controller_spec.rb b/spec/controllers/admin/taxon_concepts_controller_spec.rb index dde27ef34..dfd65188d 100644 --- a/spec/controllers/admin/taxon_concepts_controller_spec.rb +++ b/spec/controllers/admin/taxon_concepts_controller_spec.rb @@ -46,7 +46,7 @@ expect(response).to render_template("create") end it "renders new when not successful" do - xhr :post, :create, taxon_concept: {} + xhr :post, :create, taxon_concept: { dummy: 'test'} expect(response).to render_template("new") end it "renders new_synonym when not successful S" do @@ -68,7 +68,7 @@ context "when JSON" do it "responds with 200 when successful" do xhr :put, :update, :format => 'json', :id => taxon_concept.id, - :taxon_concept => {} + :taxon_concept => { dummy: 'test' } expect(response).to be_success end it "responds with json error when not successful" do @@ -80,7 +80,7 @@ context "when HTML" do it "redirects to edit when successful" do put :update, :id => taxon_concept.id, - :taxon_commons_attributes => FactoryGirl.attributes_for(:common_name) + :taxon_concept => { dummy: 'test' } expect(response).to redirect_to(edit_admin_taxon_concept_url(taxon_concept)) end it "renders edit when not successful" do diff --git a/spec/controllers/admin/taxon_eu_suspensions_controller_spec.rb b/spec/controllers/admin/taxon_eu_suspensions_controller_spec.rb index f3ddd6a42..3834e5cc6 100644 --- a/spec/controllers/admin/taxon_eu_suspensions_controller_spec.rb +++ b/spec/controllers/admin/taxon_eu_suspensions_controller_spec.rb @@ -52,7 +52,7 @@ context "when not successful" do it "renders new" do - post :create, :eu_suspension => {}, + post :create, :eu_suspension => { dummy: 'test' }, :taxon_concept_id => @taxon_concept.id expect(response).to render_template("new") end diff --git a/spec/controllers/admin/taxon_listing_changes_controller_spec.rb b/spec/controllers/admin/taxon_listing_changes_controller_spec.rb index 190acd9c3..35194fae4 100644 --- a/spec/controllers/admin/taxon_listing_changes_controller_spec.rb +++ b/spec/controllers/admin/taxon_listing_changes_controller_spec.rb @@ -87,7 +87,7 @@ end it "renders new when not successful" do taxon_concept = create(:taxon_concept) - post :create, :listing_change => {}, + post :create, :listing_change => { dummy: 'test' }, :taxon_concept_id => @taxon_concept.id, :designation_id => @designation.id expect(response).to render_template("new") diff --git a/spec/controllers/admin/taxon_quotas_controller_spec.rb b/spec/controllers/admin/taxon_quotas_controller_spec.rb index 368d80731..365660e60 100644 --- a/spec/controllers/admin/taxon_quotas_controller_spec.rb +++ b/spec/controllers/admin/taxon_quotas_controller_spec.rb @@ -50,7 +50,7 @@ end end it "renders new when not successful" do - post :create, :quota => {}, + post :create, :quota => { dummy: 'test' }, :taxon_concept_id => @taxon_concept.id expect(response).to render_template("new") end From 3bab7363a714a00c66afe07323538b4d868f6f0e Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Thu, 18 Jan 2024 17:15:09 +0000 Subject: [PATCH 024/241] bug fixes --- .../admin/nomenclature_changes/lump_controller_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/controllers/admin/nomenclature_changes/lump_controller_spec.rb b/spec/controllers/admin/nomenclature_changes/lump_controller_spec.rb index 9d47f43be..3a619f7c4 100644 --- a/spec/controllers/admin/nomenclature_changes/lump_controller_spec.rb +++ b/spec/controllers/admin/nomenclature_changes/lump_controller_spec.rb @@ -79,8 +79,8 @@ it 'redirects to next step' do put :update, nomenclature_change_lump: { inputs_attributes: { - 0 => { taxon_concept_id: create_cites_eu_species.id }, - 1 => { taxon_concept_id: create_cites_eu_species.id } + '0' => { taxon_concept_id: create_cites_eu_species.id }, + '1' => { taxon_concept_id: create_cites_eu_species.id } } }, nomenclature_change_id: @lump.id, id: 'inputs' expect(response).to redirect_to( @@ -95,7 +95,7 @@ put :update, nomenclature_change_lump: { inputs_attributes: { - 0 => { taxon_concept_id: nil } + '0' => { taxon_concept_id: nil } } }, nomenclature_change_id: @lump.id, id: 'inputs' expect(response).to render_template('inputs') From b991d4f97ded7888148ba0c5034fd028c937c8fc Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Thu, 18 Jan 2024 18:42:26 +0000 Subject: [PATCH 025/241] upgrade paper_trail to 10.3.1 (was 5.2.3) --- Gemfile | 2 +- Gemfile.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 0e74c3d4d..73a853ade 100644 --- a/Gemfile +++ b/Gemfile @@ -139,7 +139,7 @@ gem 'geoip', '1.3.5' # TODO: no change logs, no idea if safe to update. Latest v # track who created or edited a given object gem 'clerk', '0.2.3' # TODO: Need update to 1.0.0 when upgrade to Rails 5. I would say should update our code and just use paper_trail. This gem last update at 2018. -gem 'paper_trail', '5.2.3' # TODO: latest is 15.1.0. Need upgrade to v6 for Rails 5.1; v9 for Rails 5.2 +gem 'paper_trail', '10.3.1' # TODO: latest is 15.1.0. Can upgrade to newer version when we at Rails 5.2 gem 'dotenv-rails', '2.0.1' diff --git a/Gemfile.lock b/Gemfile.lock index b553e0c07..963f44981 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -327,8 +327,8 @@ GEM numerizer (0.1.1) oj (3.14.2) orm_adapter (0.5.0) - paper_trail (5.2.3) - activerecord (>= 3.0, < 6.0) + paper_trail (10.3.1) + activerecord (>= 4.2) request_store (~> 1.1) parser (2.7.2.0) ast (~> 2.4.1) @@ -610,7 +610,7 @@ DEPENDENCIES nested_form (~> 0.3.2) nokogiri (= 1.12.5) oj (= 3.14.2) - paper_trail (= 5.2.3) + paper_trail (= 10.3.1) pdfkit (~> 0.8.7.3) pg (= 0.21.0) pg_array_parser (~> 0.0.9) From 5105c1f88b393da64a59d2c7bd790cde46e4034e Mon Sep 17 00:00:00 2001 From: Daniel Perrett Date: Fri, 19 Jan 2024 10:00:49 +0000 Subject: [PATCH 026/241] deps: fix specific versions of Ember (1.6.1) and Ember Data (0.14) client js libraries Fixes issues caused by updating versions of the ember-rails and ember-data-source gems by running the following: rails generate ember:install --tag=v1.6.1 --ember rails generate ember:install --tag=v0.14 --ember-data ...and committing the files the commands place in `vendor/` --- vendor/assets/ember/development/ember-data.js | 10206 ++++ .../development/ember-template-compiler.js | 338 + vendor/assets/ember/development/ember.js | 46764 ++++++++++++++++ vendor/assets/ember/production/ember-data.js | 10189 ++++ .../production/ember-template-compiler.js | 338 + vendor/assets/ember/production/ember.js | 46321 +++++++++++++++ 6 files changed, 114156 insertions(+) create mode 100644 vendor/assets/ember/development/ember-data.js create mode 100644 vendor/assets/ember/development/ember-template-compiler.js create mode 100644 vendor/assets/ember/development/ember.js create mode 100644 vendor/assets/ember/production/ember-data.js create mode 100644 vendor/assets/ember/production/ember-template-compiler.js create mode 100644 vendor/assets/ember/production/ember.js diff --git a/vendor/assets/ember/development/ember-data.js b/vendor/assets/ember/development/ember-data.js new file mode 100644 index 000000000..8ef3ebe2c --- /dev/null +++ b/vendor/assets/ember/development/ember-data.js @@ -0,0 +1,10206 @@ +// Fetched from channel: tags/v0.14, with url http://builds.emberjs.com/tags/v0.14/ember-data.js +// Fetched on: 2024-01-19T09:54:32Z +// Version: v0.14 +// Last commit: d9cd270 (2013-08-31 17:12:14 -0700) + + +(function() { +var define, requireModule; + +(function() { + var registry = {}, seen = {}; + + define = function(name, deps, callback) { + registry[name] = { deps: deps, callback: callback }; + }; + + requireModule = function(name) { + if (seen[name]) { return seen[name]; } + seen[name] = {}; + + var mod, deps, callback, reified , exports; + + mod = registry[name]; + + if (!mod) { + throw new Error("Module '" + name + "' not found."); + } + + deps = mod.deps; + callback = mod.callback; + reified = []; + exports; + + for (var i=0, l=deps.length; i self.attributeLimit) { return false; } + var desc = capitalize(underscore(name).replace('_', ' ')); + columns.push({ name: name, desc: desc }); + }); + return columns; + }, + + getRecords: function(type) { + return this.get('store').all(type); + }, + + getRecordColumnValues: function(record) { + var self = this, count = 0, + columnValues = { id: get(record, 'id') }; + + record.eachAttribute(function(key) { + if (count++ > self.attributeLimit) { + return false; + } + var value = get(record, key); + columnValues[key] = value; + }); + return columnValues; + }, + + getRecordKeywords: function(record) { + var keywords = [], keys = Ember.A(['id']); + record.eachAttribute(function(key) { + keys.push(key); + }); + keys.forEach(function(key) { + keywords.push(get(record, key)); + }); + return keywords; + }, + + getRecordFilterValues: function(record) { + return { + isNew: record.get('isNew'), + isModified: record.get('isDirty') && !record.get('isNew'), + isClean: !record.get('isDirty') + }; + }, + + getRecordColor: function(record) { + var color = 'black'; + if (record.get('isNew')) { + color = 'green'; + } else if (record.get('isDirty')) { + color = 'blue'; + } + return color; + }, + + observeRecord: function(record, recordUpdated) { + var releaseMethods = Ember.A(), self = this, + keysToObserve = Ember.A(['id', 'isNew', 'isDirty']); + + record.eachAttribute(function(key) { + keysToObserve.push(key); + }); + + keysToObserve.forEach(function(key) { + var handler = function() { + recordUpdated(self.wrapRecord(record)); + }; + Ember.addObserver(record, key, handler); + releaseMethods.push(function() { + Ember.removeObserver(record, key, handler); + }); + }); + + var release = function() { + releaseMethods.forEach(function(fn) { fn(); } ); + }; + + return release; + } + +}); + + +})(); + + + +(function() { +/** + @module ember-data +*/ + +var set = Ember.set; + +/* + This code registers an injection for Ember.Application. + + If an Ember.js developer defines a subclass of DS.Store on their application, + this code will automatically instantiate it and make it available on the + router. + + Additionally, after an application's controllers have been injected, they will + each have the store made available to them. + + For example, imagine an Ember.js application with the following classes: + + App.Store = DS.Store.extend({ + adapter: 'App.MyCustomAdapter' + }); + + App.PostsController = Ember.ArrayController.extend({ + // ... + }); + + When the application is initialized, `App.Store` will automatically be + instantiated, and the instance of `App.PostsController` will have its `store` + property set to that instance. + + Note that this code will only be run if the `ember-application` package is + loaded. If Ember Data is being used in an environment other than a + typical application (e.g., node.js where only `ember-runtime` is available), + this code will be ignored. +*/ + +Ember.onLoad('Ember.Application', function(Application) { + Application.initializer({ + name: "store", + + initialize: function(container, application) { + Ember.assert("You included Ember Data but didn't define "+application.toString()+".Store", application.Store); + + application.register('store:main', application.Store); + application.register('serializer:_default', DS.NewJSONSerializer); + + // Eagerly generate the store so defaultStore is populated. + // TODO: Do this in a finisher hook + container.lookup('store:main'); + } + }); + + // Keep ED compatible with previous versions of ember + // TODO: Remove the if statement for Ember 1.0 + if (DS.DebugAdapter) { + Application.initializer({ + name: "dataAdapter", + + initialize: function(container, application) { + application.register('dataAdapter:main', DS.DebugAdapter); + } + }); + } + + Application.initializer({ + name: "injectStore", + + initialize: function(container, application) { + application.inject('controller', 'store', 'store:main'); + application.inject('route', 'store', 'store:main'); + application.inject('dataAdapter', 'store', 'store:main'); + } + }); + +}); + +})(); + + + +(function() { +/** + @module ember-data +*/ + +/** + Date.parse with progressive enhancement for ISO 8601 + + © 2011 Colin Snover + + Released under MIT license. + + @class Date + @namespace Ember + @static +*/ +Ember.Date = Ember.Date || {}; + +var origParse = Date.parse, numericKeys = [ 1, 4, 5, 6, 7, 10, 11 ]; + +/** + @method parse + @param date +*/ +Ember.Date.parse = function (date) { + var timestamp, struct, minutesOffset = 0; + + // ES5 §15.9.4.2 states that the string should attempt to be parsed as a Date Time String Format string + // before falling back to any implementation-specific date parsing, so that’s what we do, even if native + // implementations could be faster + // 1 YYYY 2 MM 3 DD 4 HH 5 mm 6 ss 7 msec 8 Z 9 ± 10 tzHH 11 tzmm + if ((struct = /^(\d{4}|[+\-]\d{6})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/.exec(date))) { + // avoid NaN timestamps caused by “undefined” values being passed to Date.UTC + for (var i = 0, k; (k = numericKeys[i]); ++i) { + struct[k] = +struct[k] || 0; + } + + // allow undefined days and months + struct[2] = (+struct[2] || 1) - 1; + struct[3] = +struct[3] || 1; + + if (struct[8] !== 'Z' && struct[9] !== undefined) { + minutesOffset = struct[10] * 60 + struct[11]; + + if (struct[9] === '+') { + minutesOffset = 0 - minutesOffset; + } + } + + timestamp = Date.UTC(struct[1], struct[2], struct[3], struct[4], struct[5] + minutesOffset, struct[6], struct[7]); + } + else { + timestamp = origParse ? origParse(date) : NaN; + } + + return timestamp; +}; + +if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Date) { + Date.parse = Ember.Date.parse; +} + +})(); + + + +(function() { + +})(); + + + +(function() { +/** + @module ember-data +*/ + +var Evented = Ember.Evented, // ember-runtime/mixins/evented + Deferred = Ember.DeferredMixin, // ember-runtime/mixins/evented + run = Ember.run, // ember-metal/run-loop + get = Ember.get; // ember-metal/accessors + +var LoadPromise = Ember.Mixin.create(Evented, Deferred, { + init: function() { + this._super.apply(this, arguments); + + this.one('didLoad', this, function() { + this.resolve(this); + }); + + this.one('becameError', this, function() { + this.reject(this); + }); + + if (get(this, 'isLoaded')) { + this.trigger('didLoad'); + } + } +}); + +DS.LoadPromise = LoadPromise; + +})(); + + + +(function() { +/** + @module ember-data +*/ + +var get = Ember.get, set = Ember.set; + +var LoadPromise = DS.LoadPromise; // system/mixins/load_promise + +/** + A record array is an array that contains records of a certain type. The record + array materializes records as needed when they are retrieved for the first + time. You should not create record arrays yourself. Instead, an instance of + DS.RecordArray or its subclasses will be returned by your application's store + in response to queries. + + @class RecordArray + @namespace DS + @extends Ember.ArrayProxy + @uses Ember.Evented + @uses DS.LoadPromise +*/ + +DS.RecordArray = Ember.ArrayProxy.extend(LoadPromise, { + /** + The model type contained by this record array. + + @property type + @type DS.Model + */ + type: null, + + // The array of client ids backing the record array. When a + // record is requested from the record array, the record + // for the client id at the same index is materialized, if + // necessary, by the store. + content: null, + + isLoaded: false, + isUpdating: false, + + // The store that created this record array. + store: null, + + objectAtContent: function(index) { + var content = get(this, 'content'), + reference = content.objectAt(index), + store = get(this, 'store'); + + if (reference instanceof DS.Model) { + return reference; + } + + if (reference) { + return store.recordForReference(reference); + } + }, + + materializedObjectAt: function(index) { + var reference = get(this, 'content').objectAt(index); + if (!reference) { return; } + + if (get(this, 'store').recordIsMaterialized(reference)) { + return this.objectAt(index); + } + }, + + update: function() { + if (get(this, 'isUpdating')) { return; } + + var store = get(this, 'store'), + type = get(this, 'type'); + + store.fetchAll(type, this); + }, + + addReference: function(reference) { + get(this, 'content').addObject(reference); + }, + + removeReference: function(reference) { + get(this, 'content').removeObject(reference); + } +}); + +})(); + + + +(function() { +/** + @module ember-data +*/ + +var get = Ember.get; + +/** + @class FilteredRecordArray + @namespace DS + @extends DS.RecordArray +*/ +DS.FilteredRecordArray = DS.RecordArray.extend({ + filterFunction: null, + isLoaded: true, + + replace: function() { + var type = get(this, 'type').toString(); + throw new Error("The result of a client-side filter (on " + type + ") is immutable."); + }, + + updateFilter: Ember.observer(function() { + var manager = get(this, 'manager'); + manager.updateFilter(this, get(this, 'type'), get(this, 'filterFunction')); + }, 'filterFunction') +}); + +})(); + + + +(function() { +/** + @module ember-data +*/ + +var get = Ember.get, set = Ember.set; + +/** + @class AdapterPopulatedRecordArray + @namespace DS + @extends DS.RecordArray +*/ +DS.AdapterPopulatedRecordArray = DS.RecordArray.extend({ + query: null, + + replace: function() { + var type = get(this, 'type').toString(); + throw new Error("The result of a server query (on " + type + ") is immutable."); + }, + + load: function(references) { + this.setProperties({ + content: Ember.A(references), + isLoaded: true + }); + + // TODO: does triggering didLoad event should be the last action of the runLoop? + Ember.run.once(this, 'trigger', 'didLoad'); + } +}); + +})(); + + + +(function() { +/** + @module ember-data +*/ + +var get = Ember.get, set = Ember.set; +var map = Ember.EnumerableUtils.map; + +/** + A ManyArray is a RecordArray that represents the contents of a has-many + relationship. + + The ManyArray is instantiated lazily the first time the relationship is + requested. + + ### Inverses + + Often, the relationships in Ember Data applications will have + an inverse. For example, imagine the following models are + defined: + + App.Post = DS.Model.extend({ + comments: DS.hasMany('App.Comment') + }); + + App.Comment = DS.Model.extend({ + post: DS.belongsTo('App.Post') + }); + + If you created a new instance of `App.Post` and added + a `App.Comment` record to its `comments` has-many + relationship, you would expect the comment's `post` + property to be set to the post that contained + the has-many. + + We call the record to which a relationship belongs the + relationship's _owner_. + + @class ManyArray + @namespace DS + @extends DS.RecordArray +*/ +DS.ManyArray = DS.RecordArray.extend({ + init: function() { + this._super.apply(this, arguments); + this._changesToSync = Ember.OrderedSet.create(); + }, + + /** + The record to which this relationship belongs. + + @property {DS.Model} + @private + */ + owner: null, + + /** + `true` if the relationship is polymorphic, `false` otherwise. + + @property {Boolean} + @private + */ + isPolymorphic: false, + + // LOADING STATE + + isLoaded: false, + + loadingRecordsCount: function(count) { + this.loadingRecordsCount = count; + }, + + loadedRecord: function() { + this.loadingRecordsCount--; + if (this.loadingRecordsCount === 0) { + set(this, 'isLoaded', true); + this.trigger('didLoad'); + } + }, + + fetch: function() { + var references = get(this, 'content'), + store = get(this, 'store'), + owner = get(this, 'owner'); + + store.fetchUnloadedReferences(references, owner); + }, + + // Overrides Ember.Array's replace method to implement + replaceContent: function(index, removed, added) { + // Map the array of record objects into an array of client ids. + added = map(added, function(record) { + Ember.assert("You can only add records of " + (get(this, 'type') && get(this, 'type').toString()) + " to this relationship.", !get(this, 'type') || (get(this, 'type').detectInstance(record)) ); + return get(record, '_reference'); + }, this); + + this._super(index, removed, added); + }, + + arrangedContentDidChange: function() { + this.fetch(); + }, + + arrayContentWillChange: function(index, removed, added) { + var owner = get(this, 'owner'), + name = get(this, 'name'); + + if (!owner._suspendedRelationships) { + // This code is the first half of code that continues inside + // of arrayContentDidChange. It gets or creates a change from + // the child object, adds the current owner as the old + // parent if this is the first time the object was removed + // from a ManyArray, and sets `newParent` to null. + // + // Later, if the object is added to another ManyArray, + // the `arrayContentDidChange` will set `newParent` on + // the change. + for (var i=index; i "created.uncommitted" + + The `DS.Model` states are themselves stateless. What we mean is that, + though each instance of a record also has a unique instance of a + `DS.StateManager`, the hierarchical states that each of *those* points + to is a shared data structure. For performance reasons, instead of each + record getting its own copy of the hierarchy of states, each state + manager points to this global, immutable shared instance. How does a + state know which record it should be acting on? We pass a reference to + the current state manager as the first parameter to every method invoked + on a state. + + The state manager passed as the first parameter is where you should stash + state about the record if needed; you should never store data on the state + object itself. If you need access to the record being acted on, you can + retrieve the state manager's `record` property. For example, if you had + an event handler `myEvent`: + + myEvent: function(manager) { + var record = manager.get('record'); + record.doSomething(); + } + + For more information about state managers in general, see the Ember.js + documentation on `Ember.StateManager`. + + ### Events, Flags, and Transitions + + A state may implement zero or more events, flags, or transitions. + + #### Events + + Events are named functions that are invoked when sent to a record. The + state manager will first look for a method with the given name on the + current state. If no method is found, it will search the current state's + parent, and then its grandparent, and so on until reaching the top of + the hierarchy. If the root is reached without an event handler being found, + an exception will be raised. This can be very helpful when debugging new + features. + + Here's an example implementation of a state with a `myEvent` event handler: + + aState: DS.State.create({ + myEvent: function(manager, param) { + console.log("Received myEvent with "+param); + } + }) + + To trigger this event: + + record.send('myEvent', 'foo'); + //=> "Received myEvent with foo" + + Note that an optional parameter can be sent to a record's `send()` method, + which will be passed as the second parameter to the event handler. + + Events should transition to a different state if appropriate. This can be + done by calling the state manager's `transitionTo()` method with a path to the + desired state. The state manager will attempt to resolve the state path + relative to the current state. If no state is found at that path, it will + attempt to resolve it relative to the current state's parent, and then its + parent, and so on until the root is reached. For example, imagine a hierarchy + like this: + + * created + * start <-- currentState + * inFlight + * updated + * inFlight + + If we are currently in the `start` state, calling + `transitionTo('inFlight')` would transition to the `created.inFlight` state, + while calling `transitionTo('updated.inFlight')` would transition to + the `updated.inFlight` state. + + Remember that *only events* should ever cause a state transition. You should + never call `transitionTo()` from outside a state's event handler. If you are + tempted to do so, create a new event and send that to the state manager. + + #### Flags + + Flags are Boolean values that can be used to introspect a record's current + state in a more user-friendly way than examining its state path. For example, + instead of doing this: + + var statePath = record.get('stateManager.currentPath'); + if (statePath === 'created.inFlight') { + doSomething(); + } + + You can say: + + if (record.get('isNew') && record.get('isSaving')) { + doSomething(); + } + + If your state does not set a value for a given flag, the value will + be inherited from its parent (or the first place in the state hierarchy + where it is defined). + + The current set of flags are defined below. If you want to add a new flag, + in addition to the area below, you will also need to declare it in the + `DS.Model` class. + + #### Transitions + + Transitions are like event handlers but are called automatically upon + entering or exiting a state. To implement a transition, just call a method + either `enter` or `exit`: + + myState: DS.State.create({ + // Gets called automatically when entering + // this state. + enter: function(manager) { + console.log("Entered myState"); + } + }) + + Note that enter and exit events are called once per transition. If the + current state changes, but changes to another child state of the parent, + the transition event on the parent will not be triggered. +*/ + +var hasDefinedProperties = function(object) { + // Ignore internal property defined by simulated `Ember.create`. + var names = Ember.keys(object); + var i, l, name; + for (i = 0, l = names.length; i < l; i++ ) { + name = names[i]; + if (object.hasOwnProperty(name) && object[name]) { return true; } + } + + return false; +}; + +var didChangeData = function(record) { + record.materializeData(); +}; + +var willSetProperty = function(record, context) { + context.oldValue = get(record, context.name); + + var change = DS.AttributeChange.createChange(context); + record._changesToSync[context.name] = change; +}; + +var didSetProperty = function(record, context) { + var change = record._changesToSync[context.name]; + change.value = get(record, context.name); + change.sync(); +}; + + +// Implementation notes: +// +// Each state has a boolean value for all of the following flags: +// +// * isLoaded: The record has a populated `data` property. When a +// record is loaded via `store.find`, `isLoaded` is false +// until the adapter sets it. When a record is created locally, +// its `isLoaded` property is always true. +// * isDirty: The record has local changes that have not yet been +// saved by the adapter. This includes records that have been +// created (but not yet saved) or deleted. +// * isSaving: The record's transaction has been committed, but +// the adapter has not yet acknowledged that the changes have +// been persisted to the backend. +// * isDeleted: The record was marked for deletion. When `isDeleted` +// is true and `isDirty` is true, the record is deleted locally +// but the deletion was not yet persisted. When `isSaving` is +// true, the change is in-flight. When both `isDirty` and +// `isSaving` are false, the change has persisted. +// * isError: The adapter reported that it was unable to save +// local changes to the backend. This may also result in the +// record having its `isValid` property become false if the +// adapter reported that server-side validations failed. +// * isNew: The record was created on the client and the adapter +// did not yet report that it was successfully saved. +// * isValid: No client-side validations have failed and the +// adapter did not report any server-side validation failures. + +// The dirty state is a abstract state whose functionality is +// shared between the `created` and `updated` states. +// +// The deleted state shares the `isDirty` flag with the +// subclasses of `DirtyState`, but with a very different +// implementation. +// +// Dirty states have three child states: +// +// `uncommitted`: the store has not yet handed off the record +// to be saved. +// `inFlight`: the store has handed off the record to be saved, +// but the adapter has not yet acknowledged success. +// `invalid`: the record has invalid information and cannot be +// send to the adapter yet. +var DirtyState = { + initialState: 'uncommitted', + + // FLAGS + isDirty: true, + + // SUBSTATES + + // When a record first becomes dirty, it is `uncommitted`. + // This means that there are local pending changes, but they + // have not yet begun to be saved, and are not invalid. + uncommitted: { + + // EVENTS + willSetProperty: willSetProperty, + didSetProperty: didSetProperty, + + becomeDirty: Ember.K, + + willCommit: function(record) { + record.transitionTo('inFlight'); + }, + + becameClean: function(record) { + record.withTransaction(function(t) { + t.remove(record); + }); + + record.transitionTo('loaded.materializing'); + }, + + becameInvalid: function(record) { + record.transitionTo('invalid'); + }, + + rollback: function(record) { + record.rollback(); + } + }, + + // Once a record has been handed off to the adapter to be + // saved, it is in the 'in flight' state. Changes to the + // record cannot be made during this window. + inFlight: { + // FLAGS + isSaving: true, + + // TRANSITIONS + enter: function(record) { + record.becameInFlight(); + }, + + // EVENTS + + materializingData: function(record) { + set(record, 'lastDirtyType', get(this, 'dirtyType')); + record.transitionTo('materializing'); + }, + + didCommit: function(record) { + var dirtyType = get(this, 'dirtyType'); + + record.withTransaction(function(t) { + t.remove(record); + }); + + record.transitionTo('saved'); + record.send('invokeLifecycleCallbacks', dirtyType); + }, + + didChangeData: didChangeData, + + becameInvalid: function(record, errors) { + set(record, 'errors', errors); + + record.transitionTo('invalid'); + record.send('invokeLifecycleCallbacks'); + }, + + becameError: function(record) { + record.transitionTo('error'); + record.send('invokeLifecycleCallbacks'); + } + }, + + // A record is in the `invalid` state when its client-side + // invalidations have failed, or if the adapter has indicated + // the the record failed server-side invalidations. + invalid: { + // FLAGS + isValid: false, + + exit: function(record) { + record.withTransaction(function (t) { + t.remove(record); + }); + }, + + // EVENTS + deleteRecord: function(record) { + record.transitionTo('deleted.uncommitted'); + record.clearRelationships(); + }, + + willSetProperty: willSetProperty, + + didSetProperty: function(record, context) { + var errors = get(record, 'errors'), + key = context.name; + + set(errors, key, null); + + if (!hasDefinedProperties(errors)) { + record.send('becameValid'); + } + + didSetProperty(record, context); + }, + + becomeDirty: Ember.K, + + rollback: function(record) { + record.send('becameValid'); + record.send('rollback'); + }, + + becameValid: function(record) { + record.transitionTo('uncommitted'); + }, + + invokeLifecycleCallbacks: function(record) { + record.trigger('becameInvalid', record); + } + } +}; + +// The created and updated states are created outside the state +// chart so we can reopen their substates and add mixins as +// necessary. + +function deepClone(object) { + var clone = {}, value; + + for (var prop in object) { + value = object[prop]; + if (value && typeof value === 'object') { + clone[prop] = deepClone(value); + } else { + clone[prop] = value; + } + } + + return clone; +} + +function mixin(original, hash) { + for (var prop in hash) { + original[prop] = hash[prop]; + } + + return original; +} + +function dirtyState(options) { + var newState = deepClone(DirtyState); + return mixin(newState, options); +} + +var createdState = dirtyState({ + dirtyType: 'created', + + // FLAGS + isNew: true +}); + +var updatedState = dirtyState({ + dirtyType: 'updated' +}); + +createdState.uncommitted.deleteRecord = function(record) { + record.clearRelationships(); + record.transitionTo('deleted.saved'); +}; + +createdState.uncommitted.rollback = function(record) { + DirtyState.uncommitted.rollback.apply(this, arguments); + record.transitionTo('deleted.saved'); +}; + +updatedState.uncommitted.deleteRecord = function(record) { + record.transitionTo('deleted.uncommitted'); + record.clearRelationships(); +}; + +var RootState = { + // FLAGS + isEmpty: false, + isLoading: false, + isLoaded: false, + isReloading: false, + isDirty: false, + isSaving: false, + isDeleted: false, + isError: false, + isNew: false, + isValid: true, + + // SUBSTATES + + // A record begins its lifecycle in the `empty` state. + // If its data will come from the adapter, it will + // transition into the `loading` state. Otherwise, if + // the record is being created on the client, it will + // transition into the `created` state. + empty: { + isEmpty: true, + + // EVENTS + loadingData: function(record) { + record.transitionTo('loading'); + }, + + loadedData: function(record) { + record.transitionTo('loaded.created.uncommitted'); + }, + + pushedData: function(record) { + record.transitionTo('loaded.saved'); + } + }, + + // A record enters this state when the store askes + // the adapter for its data. It remains in this state + // until the adapter provides the requested data. + // + // Usually, this process is asynchronous, using an + // XHR to retrieve the data. + loading: { + // FLAGS + isLoading: true, + + // EVENTS + loadedData: didChangeData, + + materializingData: function(record) { + record.transitionTo('loaded.materializing.firstTime'); + }, + + becameError: function(record) { + record.transitionTo('error'); + record.send('invokeLifecycleCallbacks'); + } + }, + + // A record enters this state when its data is populated. + // Most of a record's lifecycle is spent inside substates + // of the `loaded` state. + loaded: { + initialState: 'saved', + + // FLAGS + isLoaded: true, + + // SUBSTATES + + materializing: { + // EVENTS + willSetProperty: Ember.K, + didSetProperty: Ember.K, + + didChangeData: didChangeData, + + finishedMaterializing: function(record) { + record.transitionTo('loaded.saved'); + }, + + // SUBSTATES + firstTime: { + // FLAGS + isLoaded: false, + + exit: function(record) { + once(function() { + record.trigger('didLoad'); + }); + } + } + }, + + reloading: { + // FLAGS + isReloading: true, + + // TRANSITIONS + enter: function(record) { + var store = get(record, 'store'); + store.reloadRecord(record); + }, + + exit: function(record) { + once(record, 'trigger', 'didReload'); + }, + + // EVENTS + loadedData: didChangeData, + + materializingData: function(record) { + record.transitionTo('loaded.materializing'); + } + }, + + // If there are no local changes to a record, it remains + // in the `saved` state. + saved: { + // EVENTS + willSetProperty: willSetProperty, + didSetProperty: didSetProperty, + + didChangeData: didChangeData, + loadedData: didChangeData, + + reloadRecord: function(record) { + record.transitionTo('loaded.reloading'); + }, + + materializingData: function(record) { + record.transitionTo('loaded.materializing'); + }, + + becomeDirty: function(record) { + record.transitionTo('updated.uncommitted'); + }, + + deleteRecord: function(record) { + record.transitionTo('deleted.uncommitted'); + record.clearRelationships(); + }, + + unloadRecord: function(record) { + // clear relationships before moving to deleted state + // otherwise it fails + record.clearRelationships(); + record.transitionTo('deleted.saved'); + }, + + didCommit: function(record) { + record.withTransaction(function(t) { + t.remove(record); + }); + + record.send('invokeLifecycleCallbacks', get(record, 'lastDirtyType')); + }, + + invokeLifecycleCallbacks: function(record, dirtyType) { + if (dirtyType === 'created') { + record.trigger('didCreate', record); + } else { + record.trigger('didUpdate', record); + } + + record.trigger('didCommit', record); + } + }, + + // A record is in this state after it has been locally + // created but before the adapter has indicated that + // it has been saved. + created: createdState, + + // A record is in this state if it has already been + // saved to the server, but there are new local changes + // that have not yet been saved. + updated: updatedState + }, + + // A record is in this state if it was deleted from the store. + deleted: { + initialState: 'uncommitted', + dirtyType: 'deleted', + + // FLAGS + isDeleted: true, + isLoaded: true, + isDirty: true, + + // TRANSITIONS + setup: function(record) { + var store = get(record, 'store'); + + store.recordArrayManager.remove(record); + }, + + // SUBSTATES + + // When a record is deleted, it enters the `start` + // state. It will exit this state when the record's + // transaction starts to commit. + uncommitted: { + + // EVENTS + willCommit: function(record) { + record.transitionTo('inFlight'); + }, + + rollback: function(record) { + record.rollback(); + }, + + becomeDirty: Ember.K, + + becameClean: function(record) { + record.withTransaction(function(t) { + t.remove(record); + }); + record.transitionTo('loaded.materializing'); + } + }, + + // After a record's transaction is committing, but + // before the adapter indicates that the deletion + // has saved to the server, a record is in the + // `inFlight` substate of `deleted`. + inFlight: { + // FLAGS + isSaving: true, + + // TRANSITIONS + enter: function(record) { + record.becameInFlight(); + }, + + // EVENTS + didCommit: function(record) { + record.withTransaction(function(t) { + t.remove(record); + }); + + record.transitionTo('saved'); + + record.send('invokeLifecycleCallbacks'); + } + }, + + // Once the adapter indicates that the deletion has + // been saved, the record enters the `saved` substate + // of `deleted`. + saved: { + // FLAGS + isDirty: false, + + setup: function(record) { + var store = get(record, 'store'); + store.dematerializeRecord(record); + }, + + invokeLifecycleCallbacks: function(record) { + record.trigger('didDelete', record); + record.trigger('didCommit', record); + } + } + }, + + // If the adapter indicates that there was an unknown + // error saving a record, the record enters the `error` + // state. + error: { + isError: true, + + // EVENTS + + invokeLifecycleCallbacks: function(record) { + record.trigger('becameError', record); + } + } +}; + +var hasOwnProp = {}.hasOwnProperty; + +function wireState(object, parent, name) { + /*jshint proto:true*/ + // TODO: Use Object.create and copy instead + object = mixin(parent ? Ember.create(parent) : {}, object); + object.parentState = parent; + object.stateName = name; + + for (var prop in object) { + if (!object.hasOwnProperty(prop) || prop === 'parentState' || prop === 'stateName') { continue; } + if (typeof object[prop] === 'object') { + object[prop] = wireState(object[prop], object, name + "." + prop); + } + } + + return object; +} + +RootState = wireState(RootState, null, "root"); + +DS.RootState = RootState; + +})(); + + + +(function() { +/** + @module ember-data +*/ + +var LoadPromise = DS.LoadPromise; // system/mixins/load_promise + +var get = Ember.get, set = Ember.set, map = Ember.EnumerableUtils.map, merge = Ember.merge; + +var arrayMap = Ember.ArrayPolyfills.map; + +var retrieveFromCurrentState = Ember.computed(function(key, value) { + return get(get(this, 'currentState'), key); +}).property('currentState').readOnly(); + +/** + + The model class that all Ember Data records descend from. + + @class Model + @namespace DS + @extends Ember.Object + @uses Ember.Evented + @uses DS.LoadPromise +*/ +DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { + isEmpty: retrieveFromCurrentState, + isLoading: retrieveFromCurrentState, + isLoaded: retrieveFromCurrentState, + isReloading: retrieveFromCurrentState, + isDirty: retrieveFromCurrentState, + isSaving: retrieveFromCurrentState, + isDeleted: retrieveFromCurrentState, + isError: retrieveFromCurrentState, + isNew: retrieveFromCurrentState, + isValid: retrieveFromCurrentState, + dirtyType: retrieveFromCurrentState, + + clientId: null, + id: null, + transaction: null, + currentState: null, + errors: null, + + /** + Create a JSON representation of the record, using the serialization + strategy of the store's adapter. + + @method serialize + @param {Object} options Available options: + + * `includeId`: `true` if the record's ID should be included in the + JSON representation. + + @returns {Object} an object whose values are primitive JSON values only + */ + serialize: function(options) { + var store = get(this, 'store'); + return store.serialize(this, options); + }, + + /** + Use {{#crossLink "DS.JSONSerializer"}}DS.JSONSerializer{{/crossLink}} to + get the JSON representation of a record. + + @method toJSON + @param {Object} options Available options: + + * `includeId`: `true` if the record's ID should be included in the + JSON representation. + + @returns {Object} A JSON representation of the object. + */ + toJSON: function(options) { + var serializer = DS.JSONSerializer.create(); + return serializer.serialize(this, options); + }, + + /** + Fired when the record is loaded from the server. + + @event didLoad + */ + didLoad: Ember.K, + + /** + Fired when the record is reloaded from the server. + + @event didReload + */ + didReload: Ember.K, + + /** + Fired when the record is updated. + + @event didUpdate + */ + didUpdate: Ember.K, + + /** + Fired when the record is created. + + @event didCreate + */ + didCreate: Ember.K, + + /** + Fired when the record is deleted. + + @event didDelete + */ + didDelete: Ember.K, + + /** + Fired when the record becomes invalid. + + @event becameInvalid + */ + becameInvalid: Ember.K, + + /** + Fired when the record enters the error state. + + @event becameError + */ + becameError: Ember.K, + + data: Ember.computed(function() { + if (!this._data) { + this.setupData(); + } + + return this._data; + }).property(), + + materializeData: function() { + this.send('materializingData'); + + get(this, 'store').materializeData(this); + + this.suspendRelationshipObservers(function() { + this.notifyPropertyChange('data'); + }); + }, + + _data: null, + + init: function() { + set(this, 'currentState', DS.RootState.empty); + this._super(); + this._setup(); + }, + + _setup: function() { + this._changesToSync = {}; + }, + + send: function(name, context) { + var currentState = get(this, 'currentState'); + + if (!currentState[name]) { + this._unhandledEvent(currentState, name, context); + } + + return currentState[name](this, context); + }, + + transitionTo: function(name) { + // POSSIBLE TODO: Remove this code and replace with + // always having direct references to state objects + + var pivotName = name.split(".", 1), + currentState = get(this, 'currentState'), + state = currentState; + + do { + if (state.exit) { state.exit(this); } + state = state.parentState; + } while (!state.hasOwnProperty(pivotName)); + + var path = name.split("."); + + var setups = [], enters = [], i, l; + + for (i=0, l=path.length; i tuple for a polymorphic association. + return store.referenceForId(id.type, id.id); + } + } + return store.referenceForId(type, id); + }); + + set(cachedValue, 'content', Ember.A(references)); + } + }, + + updateRecordArraysLater: function() { + Ember.run.once(this, this.updateRecordArrays); + }, + + setupData: function(data) { + this._data = data || { id: null }; + + if (data) { this.pushedData(); } + }, + + materializeId: function(id) { + set(this, 'id', id); + }, + + materializeAttributes: function(attributes) { + Ember.assert("Must pass a hash of attributes to materializeAttributes", !!attributes); + merge(this._data, attributes); + }, + + materializeAttribute: function(name, value) { + this._data[name] = value; + }, + + materializeHasMany: function(name, tuplesOrReferencesOrOpaque) { + var tuplesOrReferencesOrOpaqueType = typeof tuplesOrReferencesOrOpaque; + + if (tuplesOrReferencesOrOpaque && tuplesOrReferencesOrOpaqueType !== 'string' && tuplesOrReferencesOrOpaque.length > 1) { + Ember.assert('materializeHasMany expects tuples, references or opaque token, not ' + tuplesOrReferencesOrOpaque[0], tuplesOrReferencesOrOpaque[0].hasOwnProperty('id') && tuplesOrReferencesOrOpaque[0].type); + } + + if( tuplesOrReferencesOrOpaqueType === "string" ) { + this._data[name] = tuplesOrReferencesOrOpaque; + } else { + var references = tuplesOrReferencesOrOpaque; + + if (tuplesOrReferencesOrOpaque && Ember.isArray(tuplesOrReferencesOrOpaque)) { + references = this._convertTuplesToReferences(tuplesOrReferencesOrOpaque); + } + + this._data[name] = references; + } + }, + + materializeBelongsTo: function(name, tupleOrReference) { + if (tupleOrReference) { Ember.assert('materializeBelongsTo expects a tuple or a reference, not a ' + tupleOrReference, !tupleOrReference || (tupleOrReference.hasOwnProperty('id') && tupleOrReference.hasOwnProperty('type'))); } + + this._data[name] = tupleOrReference; + }, + + _convertTuplesToReferences: function(tuplesOrReferences) { + return map(tuplesOrReferences, function(tupleOrReference) { + return this._convertTupleToReference(tupleOrReference); + }, this); + }, + + _convertTupleToReference: function(tupleOrReference) { + var store = get(this, 'store'); + if(tupleOrReference.clientId) { + return tupleOrReference; + } else { + return store.referenceForId(tupleOrReference.type, tupleOrReference.id); + } + }, + + rollback: function() { + this._setup(); + this.send('becameClean'); + + this.suspendRelationshipObservers(function() { + this.notifyPropertyChange('data'); + }); + }, + + toStringExtension: function() { + return get(this, 'id'); + }, + + /** + The goal of this method is to temporarily disable specific observers + that take action in response to application changes. + + This allows the system to make changes (such as materialization and + rollback) that should not trigger secondary behavior (such as setting an + inverse relationship or marking records as dirty). + + The specific implementation will likely change as Ember proper provides + better infrastructure for suspending groups of observers, and if Array + observation becomes more unified with regular observers. + + @method suspendRelationshipObservers + @private + @param callback + @param binding + */ + suspendRelationshipObservers: function(callback, binding) { + var observers = get(this.constructor, 'relationshipNames').belongsTo; + var self = this; + + try { + this._suspendedRelationships = true; + Ember._suspendObservers(self, observers, null, 'belongsToDidChange', function() { + Ember._suspendBeforeObservers(self, observers, null, 'belongsToWillChange', function() { + callback.call(binding || self); + }); + }); + } finally { + this._suspendedRelationships = false; + } + }, + + becameInFlight: function() { + }, + + /** + @method resolveOn + @private + @param successEvent + */ + resolveOn: function(successEvent) { + var model = this; + + return new Ember.RSVP.Promise(function(resolve, reject) { + function success() { + this.off('becameError', error); + this.off('becameInvalid', error); + resolve(this); + } + function error() { + this.off(successEvent, success); + reject(this); + } + + model.one(successEvent, success); + model.one('becameError', error); + model.one('becameInvalid', error); + }); + }, + + /** + Save the record. + + @method save + */ + save: function() { + this.get('store').scheduleSave(this); + + return this.resolveOn('didCommit'); + }, + + /** + Reload the record from the adapter. + + This will only work if the record has already finished loading + and has not yet been modified (`isLoaded` but not `isDirty`, + or `isSaving`). + + @method reload + */ + reload: function() { + this.send('reloadRecord'); + + return this.resolveOn('didReload'); + }, + + // FOR USE DURING COMMIT PROCESS + + adapterDidUpdateAttribute: function(attributeName, value) { + + // If a value is passed in, update the internal attributes and clear + // the attribute cache so it picks up the new value. Otherwise, + // collapse the current value into the internal attributes because + // the adapter has acknowledged it. + if (value !== undefined) { + get(this, 'data')[attributeName] = value; + this.notifyPropertyChange(attributeName); + } else { + value = get(this, attributeName); + get(this, 'data')[attributeName] = value; + } + + this.updateRecordArraysLater(); + }, + + adapterDidInvalidate: function(errors) { + this.send('becameInvalid', errors); + }, + + adapterDidError: function() { + this.send('becameError'); + }, + + /** + Override the default event firing from Ember.Evented to + also call methods with the given name. + + @method trigger + @private + @param name + */ + trigger: function(name) { + Ember.tryInvoke(this, name, [].slice.call(arguments, 1)); + this._super.apply(this, arguments); + } +}); + +// Helper function to generate store aliases. +// This returns a function that invokes the named alias +// on the default store, but injects the class as the +// first parameter. +var storeAlias = function(methodName) { + return function() { + var store = get(DS, 'defaultStore'), + args = [].slice.call(arguments); + + args.unshift(this); + Ember.assert("Your application does not have a 'Store' property defined. Attempts to call '" + methodName + "' on model classes will fail. Please provide one as with 'YourAppName.Store = DS.Store.extend()'", !!store); + return store[methodName].apply(store, args); + }; +}; + +DS.Model.reopenClass({ + + /** + Alias DS.Model's `create` method to `_create`. This allows us to create DS.Model + instances from within the store, but if end users accidentally call `create()` + (instead of `createRecord()`), we can raise an error. + + @method _create + @private + @static + */ + _create: DS.Model.create, + + /** + Override the class' `create()` method to raise an error. This prevents end users + from inadvertently calling `create()` instead of `createRecord()`. The store is + still able to create instances by calling the `_create()` method. + + @method create + @private + @static + */ + create: function() { + throw new Ember.Error("You should not call `create` on a model. Instead, call `createRecord` with the attributes you would like to set."); + }, + + /** + See `DS.Store.find()`. + + @method find + @param {Object|String|Array|null} query A query to find records by. + */ + find: storeAlias('find'), + + /** + See `DS.Store.all()`. + + @method all + @return {DS.RecordArray} + */ + all: storeAlias('all'), + + /** + See `DS.Store.findQuery()`. + + @method query + @param {Object} query an opaque query to be used by the adapter + @return {DS.AdapterPopulatedRecordArray} + */ + query: storeAlias('findQuery'), + + /** + See `DS.Store.filter()`. + + @method filter + @param {Function} filter + @return {DS.FilteredRecordArray} + */ + filter: storeAlias('filter'), + + /** + See `DS.Store.createRecord()`. + + @method createRecord + @param {Object} properties a hash of properties to set on the + newly created record. + @return DS.Model + */ + createRecord: storeAlias('createRecord') +}); + +})(); + + + +(function() { +/** + @module ember-data +*/ + +var get = Ember.get; + +/** + @class Model + @namespace DS +*/ +DS.Model.reopenClass({ + attributes: Ember.computed(function() { + var map = Ember.Map.create(); + + this.eachComputedProperty(function(name, meta) { + if (meta.isAttribute) { + Ember.assert("You may not set `id` as an attribute on your model. Please remove any lines that look like: `id: DS.attr('')` from " + this.toString(), name !== 'id'); + + meta.name = name; + map.set(name, meta); + } + }); + + return map; + }) +}); + + +DS.Model.reopen({ + eachAttribute: function(callback, binding) { + get(this.constructor, 'attributes').forEach(function(name, meta) { + callback.call(binding, name, meta); + }, binding); + }, + + attributeWillChange: Ember.beforeObserver(function(record, key) { + var reference = get(record, '_reference'), + store = get(record, 'store'); + + record.send('willSetProperty', { reference: reference, store: store, name: key }); + }), + + attributeDidChange: Ember.observer(function(record, key) { + record.send('didSetProperty', { name: key }); + }) +}); + +function getAttr(record, options, key) { + var attributes = get(record, 'data'); + var value = attributes[key]; + + if (value === undefined) { + if (typeof options.defaultValue === "function") { + value = options.defaultValue(); + } else { + value = options.defaultValue; + } + } + + return value; +} + +DS.attr = function(type, options) { + options = options || {}; + + var meta = { + type: type, + isAttribute: true, + options: options + }; + + return Ember.computed(function(key, value, oldValue) { + if (arguments.length > 1) { + Ember.assert("You may not set `id` as an attribute on your model. Please remove any lines that look like: `id: DS.attr('')` from " + this.constructor.toString(), key !== 'id'); + } else { + value = getAttr(this, options, key); + } + + return value; + // `data` is never set directly. However, it may be + // invalidated from the state manager's setData + // event. + }).property('data').meta(meta); +}; + + +})(); + + + +(function() { +/** + @module ember-data +*/ + +})(); + + + +(function() { +/** + @module ember-data +*/ + +/** + An AttributeChange object is created whenever a record's + attribute changes value. It is used to track changes to a + record between transaction commits. + + @class AttributeChange + @namespace DS + @private + @constructor +*/ +var AttributeChange = DS.AttributeChange = function(options) { + this.reference = options.reference; + this.store = options.store; + this.name = options.name; + this.oldValue = options.oldValue; +}; + +AttributeChange.createChange = function(options) { + return new AttributeChange(options); +}; + +AttributeChange.prototype = { + sync: function() { + this.store.recordAttributeDidChange(this.reference, this.name, this.value, this.oldValue); + + // TODO: Use this object in the commit process + this.destroy(); + }, + + /** + If the AttributeChange is destroyed (either by being rolled back + or being committed), remove it from the list of pending changes + on the record. + + @method destroy + */ + destroy: function() { + var record = this.reference.record; + + delete record._changesToSync[this.name]; + } +}; + +})(); + + + +(function() { +/** + @module ember-data +*/ + +var get = Ember.get, set = Ember.set; +var forEach = Ember.EnumerableUtils.forEach; + +/** + @class RelationshipChange + @namespace DS + @private + @construtor +*/ +DS.RelationshipChange = function(options) { + this.parentReference = options.parentReference; + this.childReference = options.childReference; + this.firstRecordReference = options.firstRecordReference; + this.firstRecordKind = options.firstRecordKind; + this.firstRecordName = options.firstRecordName; + this.secondRecordReference = options.secondRecordReference; + this.secondRecordKind = options.secondRecordKind; + this.secondRecordName = options.secondRecordName; + this.changeType = options.changeType; + this.store = options.store; + + this.committed = {}; +}; + +/** + @class RelationshipChangeAdd + @namespace DS + @private + @construtor +*/ +DS.RelationshipChangeAdd = function(options){ + DS.RelationshipChange.call(this, options); +}; + +/** + @class RelationshipChangeRemove + @namespace DS + @private + @construtor +*/ +DS.RelationshipChangeRemove = function(options){ + DS.RelationshipChange.call(this, options); +}; + +DS.RelationshipChange.create = function(options) { + return new DS.RelationshipChange(options); +}; + +DS.RelationshipChangeAdd.create = function(options) { + return new DS.RelationshipChangeAdd(options); +}; + +DS.RelationshipChangeRemove.create = function(options) { + return new DS.RelationshipChangeRemove(options); +}; + +DS.OneToManyChange = {}; +DS.OneToNoneChange = {}; +DS.ManyToNoneChange = {}; +DS.OneToOneChange = {}; +DS.ManyToManyChange = {}; + +DS.RelationshipChange._createChange = function(options){ + if(options.changeType === "add"){ + return DS.RelationshipChangeAdd.create(options); + } + if(options.changeType === "remove"){ + return DS.RelationshipChangeRemove.create(options); + } +}; + + +DS.RelationshipChange.determineRelationshipType = function(recordType, knownSide){ + var knownKey = knownSide.key, key, otherKind; + var knownKind = knownSide.kind; + + var inverse = recordType.inverseFor(knownKey); + + if (inverse){ + key = inverse.name; + otherKind = inverse.kind; + } + + if (!inverse){ + return knownKind === "belongsTo" ? "oneToNone" : "manyToNone"; + } + else{ + if(otherKind === "belongsTo"){ + return knownKind === "belongsTo" ? "oneToOne" : "manyToOne"; + } + else{ + return knownKind === "belongsTo" ? "oneToMany" : "manyToMany"; + } + } + +}; + +DS.RelationshipChange.createChange = function(firstRecordReference, secondRecordReference, store, options){ + // Get the type of the child based on the child's client ID + var firstRecordType = firstRecordReference.type, changeType; + changeType = DS.RelationshipChange.determineRelationshipType(firstRecordType, options); + if (changeType === "oneToMany"){ + return DS.OneToManyChange.createChange(firstRecordReference, secondRecordReference, store, options); + } + else if (changeType === "manyToOne"){ + return DS.OneToManyChange.createChange(secondRecordReference, firstRecordReference, store, options); + } + else if (changeType === "oneToNone"){ + return DS.OneToNoneChange.createChange(firstRecordReference, secondRecordReference, store, options); + } + else if (changeType === "manyToNone"){ + return DS.ManyToNoneChange.createChange(firstRecordReference, secondRecordReference, store, options); + } + else if (changeType === "oneToOne"){ + return DS.OneToOneChange.createChange(firstRecordReference, secondRecordReference, store, options); + } + else if (changeType === "manyToMany"){ + return DS.ManyToManyChange.createChange(firstRecordReference, secondRecordReference, store, options); + } +}; + +DS.OneToNoneChange.createChange = function(childReference, parentReference, store, options) { + var key = options.key; + var change = DS.RelationshipChange._createChange({ + parentReference: parentReference, + childReference: childReference, + firstRecordReference: childReference, + store: store, + changeType: options.changeType, + firstRecordName: key, + firstRecordKind: "belongsTo" + }); + + store.addRelationshipChangeFor(childReference, key, parentReference, null, change); + + return change; +}; + +DS.ManyToNoneChange.createChange = function(childReference, parentReference, store, options) { + var key = options.key; + var change = DS.RelationshipChange._createChange({ + parentReference: childReference, + childReference: parentReference, + secondRecordReference: childReference, + store: store, + changeType: options.changeType, + secondRecordName: options.key, + secondRecordKind: "hasMany" + }); + + store.addRelationshipChangeFor(childReference, key, parentReference, null, change); + return change; +}; + + +DS.ManyToManyChange.createChange = function(childReference, parentReference, store, options) { + // If the name of the belongsTo side of the relationship is specified, + // use that + // If the type of the parent is specified, look it up on the child's type + // definition. + var key = options.key; + + var change = DS.RelationshipChange._createChange({ + parentReference: parentReference, + childReference: childReference, + firstRecordReference: childReference, + secondRecordReference: parentReference, + firstRecordKind: "hasMany", + secondRecordKind: "hasMany", + store: store, + changeType: options.changeType, + firstRecordName: key + }); + + store.addRelationshipChangeFor(childReference, key, parentReference, null, change); + + + return change; +}; + +DS.OneToOneChange.createChange = function(childReference, parentReference, store, options) { + var key; + + // If the name of the belongsTo side of the relationship is specified, + // use that + // If the type of the parent is specified, look it up on the child's type + // definition. + if (options.parentType) { + key = options.parentType.inverseFor(options.key).name; + } else if (options.key) { + key = options.key; + } else { + Ember.assert("You must pass either a parentType or belongsToName option to OneToManyChange.forChildAndParent", false); + } + + var change = DS.RelationshipChange._createChange({ + parentReference: parentReference, + childReference: childReference, + firstRecordReference: childReference, + secondRecordReference: parentReference, + firstRecordKind: "belongsTo", + secondRecordKind: "belongsTo", + store: store, + changeType: options.changeType, + firstRecordName: key + }); + + store.addRelationshipChangeFor(childReference, key, parentReference, null, change); + + + return change; +}; + +DS.OneToOneChange.maintainInvariant = function(options, store, childReference, key){ + if (options.changeType === "add" && store.recordIsMaterialized(childReference)) { + var child = store.recordForReference(childReference); + var oldParent = get(child, key); + if (oldParent){ + var correspondingChange = DS.OneToOneChange.createChange(childReference, oldParent.get('_reference'), store, { + parentType: options.parentType, + hasManyName: options.hasManyName, + changeType: "remove", + key: options.key + }); + store.addRelationshipChangeFor(childReference, key, options.parentReference , null, correspondingChange); + correspondingChange.sync(); + } + } +}; + +DS.OneToManyChange.createChange = function(childReference, parentReference, store, options) { + var key; + + // If the name of the belongsTo side of the relationship is specified, + // use that + // If the type of the parent is specified, look it up on the child's type + // definition. + if (options.parentType) { + key = options.parentType.inverseFor(options.key).name; + DS.OneToManyChange.maintainInvariant( options, store, childReference, key ); + } else if (options.key) { + key = options.key; + } else { + Ember.assert("You must pass either a parentType or belongsToName option to OneToManyChange.forChildAndParent", false); + } + + var change = DS.RelationshipChange._createChange({ + parentReference: parentReference, + childReference: childReference, + firstRecordReference: childReference, + secondRecordReference: parentReference, + firstRecordKind: "belongsTo", + secondRecordKind: "hasMany", + store: store, + changeType: options.changeType, + firstRecordName: key + }); + + store.addRelationshipChangeFor(childReference, key, parentReference, change.getSecondRecordName(), change); + + + return change; +}; + + +DS.OneToManyChange.maintainInvariant = function(options, store, childReference, key){ + var child = childReference.record; + + if (options.changeType === "add" && child) { + var oldParent = get(child, key); + if (oldParent){ + var correspondingChange = DS.OneToManyChange.createChange(childReference, oldParent.get('_reference'), store, { + parentType: options.parentType, + hasManyName: options.hasManyName, + changeType: "remove", + key: options.key + }); + store.addRelationshipChangeFor(childReference, key, options.parentReference, correspondingChange.getSecondRecordName(), correspondingChange); + correspondingChange.sync(); + } + } +}; + +DS.OneToManyChange.ensureSameTransaction = function(changes){ + var records = Ember.A(); + forEach(changes, function(change){ + records.addObject(change.getSecondRecord()); + records.addObject(change.getFirstRecord()); + }); + + return DS.Transaction.ensureSameTransaction(records); +}; + +/** + @class RelationshipChange + @namespace DS +*/ +DS.RelationshipChange.prototype = { + + getSecondRecordName: function() { + var name = this.secondRecordName, parent; + + if (!name) { + parent = this.secondRecordReference; + if (!parent) { return; } + + var childType = this.firstRecordReference.type; + var inverse = childType.inverseFor(this.firstRecordName); + this.secondRecordName = inverse.name; + } + + return this.secondRecordName; + }, + + /** + Get the name of the relationship on the belongsTo side. + + @method getFirstRecordName + @return {String} + */ + getFirstRecordName: function() { + var name = this.firstRecordName; + return name; + }, + + /** + @method destroy + @private + */ + destroy: function() { + var childReference = this.childReference, + belongsToName = this.getFirstRecordName(), + hasManyName = this.getSecondRecordName(), + store = this.store; + + store.removeRelationshipChangeFor(childReference, belongsToName, this.parentReference, hasManyName, this.changeType); + }, + + /** + @method getByReference + @private + @param reference + */ + getByReference: function(reference) { + // return null or undefined if the original reference was null or undefined + if (!reference) { return reference; } + + if (reference.record) { + return reference.record; + } + }, + + getSecondRecord: function(){ + return this.getByReference(this.secondRecordReference); + }, + + /** + @method getFirstRecord + @private + */ + getFirstRecord: function() { + return this.getByReference(this.firstRecordReference); + }, + + /** + Make sure that all three parts of the relationship change are part of + the same transaction. If any of the three records is clean and in the + default transaction, and the rest are in a different transaction, move + them all into that transaction. + + @method ensureSameTransaction + @private + */ + ensureSameTransaction: function() { + var child = this.getFirstRecord(), + parentRecord = this.getSecondRecord(); + + var transaction = DS.Transaction.ensureSameTransaction([child, parentRecord]); + + this.transaction = transaction; + return transaction; + }, + + callChangeEvents: function(){ + var child = this.getFirstRecord(), + parentRecord = this.getSecondRecord(); + + var dirtySet = new Ember.OrderedSet(); + + // TODO: This implementation causes a race condition in key-value + // stores. The fix involves buffering changes that happen while + // a record is loading. A similar fix is required for other parts + // of ember-data, and should be done as new infrastructure, not + // a one-off hack. [tomhuda] + if (parentRecord && get(parentRecord, 'isLoaded')) { + this.store.recordHasManyDidChange(dirtySet, parentRecord, this); + } + + if (child) { + this.store.recordBelongsToDidChange(dirtySet, child, this); + } + + dirtySet.forEach(function(record) { + record.adapterDidDirty(); + }); + }, + + coalesce: function(){ + var relationshipPairs = this.store.relationshipChangePairsFor(this.firstRecordReference); + forEach(relationshipPairs, function(pair){ + var addedChange = pair["add"]; + var removedChange = pair["remove"]; + if(addedChange && removedChange) { + addedChange.destroy(); + removedChange.destroy(); + } + }); + } +}; + +DS.RelationshipChangeAdd.prototype = Ember.create(DS.RelationshipChange.create({})); +DS.RelationshipChangeRemove.prototype = Ember.create(DS.RelationshipChange.create({})); + +DS.RelationshipChangeAdd.prototype.changeType = "add"; +DS.RelationshipChangeAdd.prototype.sync = function() { + var secondRecordName = this.getSecondRecordName(), + firstRecordName = this.getFirstRecordName(), + firstRecord = this.getFirstRecord(), + secondRecord = this.getSecondRecord(); + + //Ember.assert("You specified a hasMany (" + hasManyName + ") on " + (!belongsToName && (newParent || oldParent || this.lastParent).constructor) + " but did not specify an inverse belongsTo on " + child.constructor, belongsToName); + //Ember.assert("You specified a belongsTo (" + belongsToName + ") on " + child.constructor + " but did not specify an inverse hasMany on " + (!hasManyName && (newParent || oldParent || this.lastParentRecord).constructor), hasManyName); + + this.ensureSameTransaction(); + + this.callChangeEvents(); + + if (secondRecord && firstRecord) { + if(this.secondRecordKind === "belongsTo"){ + secondRecord.suspendRelationshipObservers(function(){ + set(secondRecord, secondRecordName, firstRecord); + }); + + } + else if(this.secondRecordKind === "hasMany"){ + secondRecord.suspendRelationshipObservers(function(){ + get(secondRecord, secondRecordName).addObject(firstRecord); + }); + } + } + + if (firstRecord && secondRecord && get(firstRecord, firstRecordName) !== secondRecord) { + if(this.firstRecordKind === "belongsTo"){ + firstRecord.suspendRelationshipObservers(function(){ + set(firstRecord, firstRecordName, secondRecord); + }); + } + else if(this.firstRecordKind === "hasMany"){ + firstRecord.suspendRelationshipObservers(function(){ + get(firstRecord, firstRecordName).addObject(secondRecord); + }); + } + } + + this.coalesce(); +}; + +DS.RelationshipChangeRemove.prototype.changeType = "remove"; +DS.RelationshipChangeRemove.prototype.sync = function() { + var secondRecordName = this.getSecondRecordName(), + firstRecordName = this.getFirstRecordName(), + firstRecord = this.getFirstRecord(), + secondRecord = this.getSecondRecord(); + + //Ember.assert("You specified a hasMany (" + hasManyName + ") on " + (!belongsToName && (newParent || oldParent || this.lastParent).constructor) + " but did not specify an inverse belongsTo on " + child.constructor, belongsToName); + //Ember.assert("You specified a belongsTo (" + belongsToName + ") on " + child.constructor + " but did not specify an inverse hasMany on " + (!hasManyName && (newParent || oldParent || this.lastParentRecord).constructor), hasManyName); + + this.ensureSameTransaction(firstRecord, secondRecord, secondRecordName, firstRecordName); + + this.callChangeEvents(); + + if (secondRecord && firstRecord) { + if(this.secondRecordKind === "belongsTo"){ + secondRecord.suspendRelationshipObservers(function(){ + set(secondRecord, secondRecordName, null); + }); + } + else if(this.secondRecordKind === "hasMany"){ + secondRecord.suspendRelationshipObservers(function(){ + get(secondRecord, secondRecordName).removeObject(firstRecord); + }); + } + } + + if (firstRecord && get(firstRecord, firstRecordName)) { + if(this.firstRecordKind === "belongsTo"){ + firstRecord.suspendRelationshipObservers(function(){ + set(firstRecord, firstRecordName, null); + }); + } + else if(this.firstRecordKind === "hasMany"){ + firstRecord.suspendRelationshipObservers(function(){ + get(firstRecord, firstRecordName).removeObject(secondRecord); + }); + } + } + + this.coalesce(); +}; + +})(); + + + +(function() { +/** + @module ember-data +*/ + +})(); + + + +(function() { +var get = Ember.get, set = Ember.set, + isNone = Ember.isNone; + +/** + @module ember-data +*/ + +DS.belongsTo = function(type, options) { + Ember.assert("The first argument DS.belongsTo must be a model type or string, like DS.belongsTo(App.Person)", !!type && (typeof type === 'string' || DS.Model.detect(type))); + + options = options || {}; + + var meta = { type: type, isRelationship: true, options: options, kind: 'belongsTo' }; + + return Ember.computed(function(key, value) { + var data = get(this, 'data'), + store = get(this, 'store'), belongsTo; + + if (typeof type === 'string') { + if (type.indexOf(".") === -1) { + type = store.modelFor(type); + } else { + type = get(Ember.lookup, type); + } + } + + if (arguments.length === 2) { + Ember.assert("You can only add a record of " + type.toString() + " to this relationship", !value || type.detectInstance(value)); + return value === undefined ? null : value; + } + + belongsTo = data[key]; + + if (belongsTo instanceof DS.Model) { return belongsTo; } + + // TODO (tomdale) The value of the belongsTo in the data hash can be + // one of: + // 1. null/undefined + // 2. a record reference + // 3. a tuple returned by the serializer's polymorphism code + // + // We should really normalize #3 to be the same as #2 to reduce the + // complexity here. + + if (isNone(belongsTo)) { + return null; + } + + // The data has been normalized to a record reference, so + // just ask the store for the record for that reference, + // materializing it if necessary. + if (belongsTo.clientId) { + return store.recordForReference(belongsTo); + } + + // The data has been normalized into a type/id pair by the + // serializer's polymorphism code. + return store.findById(belongsTo.type, belongsTo.id); + }).property('data').meta(meta); +}; + +/* + These observers observe all `belongsTo` relationships on the record. See + `relationships/ext` to see how these observers get their dependencies. + + @class Model + @namespace DS +*/ +DS.Model.reopen({ + + /** + @method belongsToWillChange + @private + @static + @param record + @param key + */ + belongsToWillChange: Ember.beforeObserver(function(record, key) { + if (get(record, 'isLoaded')) { + var oldParent = get(record, key); + + var childReference = get(record, '_reference'), + store = get(record, 'store'); + if (oldParent){ + var change = DS.RelationshipChange.createChange(childReference, get(oldParent, '_reference'), store, { key: key, kind:"belongsTo", changeType: "remove" }); + change.sync(); + this._changesToSync[key] = change; + } + } + }), + + /** + @method belongsToDidChange + @private + @static + @param record + @param key + */ + belongsToDidChange: Ember.immediateObserver(function(record, key) { + if (get(record, 'isLoaded')) { + var newParent = get(record, key); + if(newParent){ + var childReference = get(record, '_reference'), + store = get(record, 'store'); + var change = DS.RelationshipChange.createChange(childReference, get(newParent, '_reference'), store, { key: key, kind:"belongsTo", changeType: "add" }); + change.sync(); + if(this._changesToSync[key]){ + DS.OneToManyChange.ensureSameTransaction([change, this._changesToSync[key]], store); + } + } + } + delete this._changesToSync[key]; + }) +}); + +})(); + + + +(function() { +/** + @module ember-data +*/ + +var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach; + +var hasRelationship = function(type, options) { + options = options || {}; + + var meta = { type: type, isRelationship: true, options: options, kind: 'hasMany' }; + + return Ember.computed(function(key, value) { + var data = get(this, 'data'), + store = get(this, 'store'), + ids, relationship; + + if (typeof type === 'string') { + if (type.indexOf(".") === -1) { + type = store.modelFor(type); + } else { + type = get(Ember.lookup, type); + } + } + + //ids can be references or opaque token + //(e.g. `{url: '/relationship'}`) that will be passed to the adapter + ids = data[key]; + + relationship = store.findMany(type, ids, this, meta); + set(relationship, 'owner', this); + set(relationship, 'name', key); + set(relationship, 'isPolymorphic', options.polymorphic); + + return relationship; + }).property().meta(meta); +}; + +DS.hasMany = function(type, options) { + Ember.assert("The type passed to DS.hasMany must be defined", !!type); + return hasRelationship(type, options); +}; + +function clearUnmaterializedHasMany(record, relationship) { + var data = get(record, 'data'); + + var references = data[relationship.key]; + + if (!references) { return; } + + var inverse = record.constructor.inverseFor(relationship.key); + + if (inverse) { + forEach(references, function(reference) { + var childRecord; + + if (childRecord = reference.record) { + record.suspendRelationshipObservers(function() { + set(childRecord, inverse.name, null); + }); + } + }); + } +} + +DS.Model.reopen({ + clearHasMany: function(relationship) { + var hasMany = this.cacheFor(relationship.name); + + if (hasMany) { + hasMany.clear(); + } else { + clearUnmaterializedHasMany(this, relationship); + } + } +}); + +})(); + + + +(function() { +var get = Ember.get, set = Ember.set; + +/** + @module ember-data +*/ + +/* + This file defines several extensions to the base `DS.Model` class that + add support for one-to-many relationships. +*/ + +/** + @class Model + @namespace DS +*/ +DS.Model.reopen({ + + /** + This Ember.js hook allows an object to be notified when a property + is defined. + + In this case, we use it to be notified when an Ember Data user defines a + belongs-to relationship. In that case, we need to set up observers for + each one, allowing us to track relationship changes and automatically + reflect changes in the inverse has-many array. + + This hook passes the class being set up, as well as the key and value + being defined. So, for example, when the user does this: + + DS.Model.extend({ + parent: DS.belongsTo(App.User) + }); + + This hook would be called with "parent" as the key and the computed + property returned by `DS.belongsTo` as the value. + + @method didDefineProperty + @param proto + @param key + @param value + */ + didDefineProperty: function(proto, key, value) { + // Check if the value being set is a computed property. + if (value instanceof Ember.Descriptor) { + + // If it is, get the metadata for the relationship. This is + // populated by the `DS.belongsTo` helper when it is creating + // the computed property. + var meta = value.meta(); + + if (meta.isRelationship && meta.kind === 'belongsTo') { + Ember.addObserver(proto, key, null, 'belongsToDidChange'); + Ember.addBeforeObserver(proto, key, null, 'belongsToWillChange'); + } + + if (meta.isAttribute) { + Ember.addObserver(proto, key, null, 'attributeDidChange'); + Ember.addBeforeObserver(proto, key, null, 'attributeWillChange'); + } + + meta.parentType = proto.constructor; + } + } +}); + +/* + These DS.Model extensions add class methods that provide relationship + introspection abilities about relationships. + + A note about the computed properties contained here: + + **These properties are effectively sealed once called for the first time.** + To avoid repeatedly doing expensive iteration over a model's fields, these + values are computed once and then cached for the remainder of the runtime of + your application. + + If your application needs to modify a class after its initial definition + (for example, using `reopen()` to add additional attributes), make sure you + do it before using your model with the store, which uses these properties + extensively. +*/ + +DS.Model.reopenClass({ + /** + For a given relationship name, returns the model type of the relationship. + + For example, if you define a model like this: + + App.Post = DS.Model.extend({ + comments: DS.hasMany(App.Comment) + }); + + Calling `App.Post.typeForRelationship('comments')` will return `App.Comment`. + + @method typeForRelationship + @static + @param {String} name the name of the relationship + @return {subclass of DS.Model} the type of the relationship, or undefined + */ + typeForRelationship: function(name) { + var relationship = get(this, 'relationshipsByName').get(name); + return relationship && relationship.type; + }, + + inverseFor: function(name) { + var inverseType = this.typeForRelationship(name); + + if (!inverseType) { return null; } + + var options = this.metaForProperty(name).options; + + if (options.inverse === null) { return null; } + + var inverseName, inverseKind; + + if (options.inverse) { + inverseName = options.inverse; + inverseKind = Ember.get(inverseType, 'relationshipsByName').get(inverseName).kind; + } else { + var possibleRelationships = findPossibleInverses(this, inverseType); + + if (possibleRelationships.length === 0) { return null; } + + Ember.assert("You defined the '" + name + "' relationship on " + this + ", but multiple possible inverse relationships of type " + this + " were found on " + inverseType + ".", possibleRelationships.length === 1); + + inverseName = possibleRelationships[0].name; + inverseKind = possibleRelationships[0].kind; + } + + function findPossibleInverses(type, inverseType, possibleRelationships) { + possibleRelationships = possibleRelationships || []; + + var relationshipMap = get(inverseType, 'relationships'); + if (!relationshipMap) { return; } + + var relationships = relationshipMap.get(type); + if (relationships) { + possibleRelationships.push.apply(possibleRelationships, relationshipMap.get(type)); + } + + if (type.superclass) { + findPossibleInverses(type.superclass, inverseType, possibleRelationships); + } + + return possibleRelationships; + } + + return { + type: inverseType, + name: inverseName, + kind: inverseKind + }; + }, + + /** + The model's relationships as a map, keyed on the type of the + relationship. The value of each entry is an array containing a descriptor + for each relationship with that type, describing the name of the relationship + as well as the type. + + For example, given the following model definition: + + App.Blog = DS.Model.extend({ + users: DS.hasMany(App.User), + owner: DS.belongsTo(App.User), + posts: DS.hasMany(App.Post) + }); + + This computed property would return a map describing these + relationships, like this: + + var relationships = Ember.get(App.Blog, 'relationships'); + relationships.get(App.User); + //=> [ { name: 'users', kind: 'hasMany' }, + // { name: 'owner', kind: 'belongsTo' } ] + relationships.get(App.Post); + //=> [ { name: 'posts', kind: 'hasMany' } ] + + @property relationships + @static + @type Ember.Map + @readOnly + */ + relationships: Ember.computed(function() { + var map = new Ember.MapWithDefault({ + defaultValue: function() { return []; } + }); + + // Loop through each computed property on the class + this.eachComputedProperty(function(name, meta) { + + // If the computed property is a relationship, add + // it to the map. + if (meta.isRelationship) { + if (typeof meta.type === 'string') { + meta.type = Ember.get(Ember.lookup, meta.type); + } + + var relationshipsForType = map.get(meta.type); + + relationshipsForType.push({ name: name, kind: meta.kind }); + } + }); + + return map; + }), + + /** + A hash containing lists of the model's relationships, grouped + by the relationship kind. For example, given a model with this + definition: + + App.Blog = DS.Model.extend({ + users: DS.hasMany(App.User), + owner: DS.belongsTo(App.User), + + posts: DS.hasMany(App.Post) + }); + + This property would contain the following: + + var relationshipNames = Ember.get(App.Blog, 'relationshipNames'); + relationshipNames.hasMany; + //=> ['users', 'posts'] + relationshipNames.belongsTo; + //=> ['owner'] + + @property relationshipNames + @static + @type Object + @readOnly + */ + relationshipNames: Ember.computed(function() { + var names = { hasMany: [], belongsTo: [] }; + + this.eachComputedProperty(function(name, meta) { + if (meta.isRelationship) { + names[meta.kind].push(name); + } + }); + + return names; + }), + + /** + An array of types directly related to a model. Each type will be + included once, regardless of the number of relationships it has with + the model. + + For example, given a model with this definition: + + App.Blog = DS.Model.extend({ + users: DS.hasMany(App.User), + owner: DS.belongsTo(App.User), + posts: DS.hasMany(App.Post) + }); + + This property would contain the following: + + var relatedTypes = Ember.get(App.Blog, 'relatedTypes'); + //=> [ App.User, App.Post ] + + @property relatedTypes + @static + @type Ember.Array + @readOnly + */ + relatedTypes: Ember.computed(function() { + var type, + types = Ember.A(); + + // Loop through each computed property on the class, + // and create an array of the unique types involved + // in relationships + this.eachComputedProperty(function(name, meta) { + if (meta.isRelationship) { + type = meta.type; + + if (typeof type === 'string') { + type = get(this, type, false) || get(Ember.lookup, type); + } + + Ember.assert("You specified a hasMany (" + meta.type + ") on " + meta.parentType + " but " + meta.type + " was not found.", type); + + if (!types.contains(type)) { + Ember.assert("Trying to sideload " + name + " on " + this.toString() + " but the type doesn't exist.", !!type); + types.push(type); + } + } + }); + + return types; + }), + + /** + A map whose keys are the relationships of a model and whose values are + relationship descriptors. + + For example, given a model with this + definition: + + App.Blog = DS.Model.extend({ + users: DS.hasMany(App.User), + owner: DS.belongsTo(App.User), + + posts: DS.hasMany(App.Post) + }); + + This property would contain the following: + + var relationshipsByName = Ember.get(App.Blog, 'relationshipsByName'); + relationshipsByName.get('users'); + //=> { key: 'users', kind: 'hasMany', type: App.User } + relationshipsByName.get('owner'); + //=> { key: 'owner', kind: 'belongsTo', type: App.User } + + @property relationshipsByName + @static + @type Ember.Map + @readOnly + */ + relationshipsByName: Ember.computed(function() { + var map = Ember.Map.create(), type; + + this.eachComputedProperty(function(name, meta) { + if (meta.isRelationship) { + meta.key = name; + type = meta.type; + + if (typeof type === 'string') { + if (type.match(/^[^A-Z]/)) { + type = this.store.modelFor(type); + } else { + type = get(this, type, false) || get(Ember.lookup, type); + } + + meta.type = type; + } + + map.set(name, meta); + } + }); + + return map; + }), + + /** + A map whose keys are the fields of the model and whose values are strings + describing the kind of the field. A model's fields are the union of all of its + attributes and relationships. + + For example: + + App.Blog = DS.Model.extend({ + users: DS.hasMany(App.User), + owner: DS.belongsTo(App.User), + + posts: DS.hasMany(App.Post), + + title: DS.attr('string') + }); + + var fields = Ember.get(App.Blog, 'fields'); + fields.forEach(function(field, kind) { + console.log(field, kind); + }); + + // prints: + // users, hasMany + // owner, belongsTo + // posts, hasMany + // title, attribute + + @property fields + @static + @type Ember.Map + @readOnly + */ + fields: Ember.computed(function() { + var map = Ember.Map.create(); + + this.eachComputedProperty(function(name, meta) { + if (meta.isRelationship) { + map.set(name, meta.kind); + } else if (meta.isAttribute) { + map.set(name, 'attribute'); + } + }); + + return map; + }), + + /** + Given a callback, iterates over each of the relationships in the model, + invoking the callback with the name of each relationship and its relationship + descriptor. + + @method eachRelationship + @static + @param {Function} callback the callback to invoke + @param {any} binding the value to which the callback's `this` should be bound + */ + eachRelationship: function(callback, binding) { + get(this, 'relationshipsByName').forEach(function(name, relationship) { + callback.call(binding, name, relationship); + }); + }, + + /** + Given a callback, iterates over each of the types related to a model, + invoking the callback with the related type's class. Each type will be + returned just once, regardless of how many different relationships it has + with a model. + + @method eachRelatedType + @static + @param {Function} callback the callback to invoke + @param {any} binding the value to which the callback's `this` should be bound + */ + eachRelatedType: function(callback, binding) { + get(this, 'relatedTypes').forEach(function(type) { + callback.call(binding, type); + }); + } +}); + +DS.Model.reopen({ + /** + Given a callback, iterates over each of the relationships in the model, + invoking the callback with the name of each relationship and its relationship + descriptor. + + @method eachRelationship + @param {Function} callback the callback to invoke + @param {any} binding the value to which the callback's `this` should be bound + */ + eachRelationship: function(callback, binding) { + this.constructor.eachRelationship(callback, binding); + } +}); + +})(); + + + +(function() { +/** + @module ember-data +*/ + +})(); + + + +(function() { +/** + @module ember-data +*/ + +var get = Ember.get, set = Ember.set; +var once = Ember.run.once; +var forEach = Ember.EnumerableUtils.forEach; + +/** + @class RecordArrayManager + @namespace DS + @private + @extends Ember.Object +*/ +DS.RecordArrayManager = Ember.Object.extend({ + init: function() { + this.filteredRecordArrays = Ember.MapWithDefault.create({ + defaultValue: function() { return []; } + }); + + this.changedReferences = []; + }, + + referenceDidChange: function(reference) { + this.changedReferences.push(reference); + once(this, this.updateRecordArrays); + }, + + recordArraysForReference: function(reference) { + reference.recordArrays = reference.recordArrays || Ember.OrderedSet.create(); + return reference.recordArrays; + }, + + /** + This method is invoked whenever data is loaded into the store + by the adapter or updated by the adapter, or when an attribute + changes on a record. + + It updates all filters that a record belongs to. + + To avoid thrashing, it only runs once per run loop per record. + + @method updateRecordArrays + @param {Class} type + @param {Number|String} clientId + */ + updateRecordArrays: function() { + forEach(this.changedReferences, function(reference) { + var type = reference.type, + recordArrays = this.filteredRecordArrays.get(type), + filter; + + forEach(recordArrays, function(array) { + filter = get(array, 'filterFunction'); + this.updateRecordArray(array, filter, type, reference); + }, this); + + // loop through all manyArrays containing an unloaded copy of this + // clientId and notify them that the record was loaded. + var manyArrays = reference.loadingRecordArrays; + + if (manyArrays) { + for (var i=0, l=manyArrays.length; i blog_post) + // 2. Extract the part after `blog_post_` (`blog_comments`) + // 3. Underscore it, to become `blogComments` + var typeString = type.toString().split(".")[1].underscore(); + return name.match(new RegExp("^" + typeString + "_(.*)$"))[1].camelize(); + } + }); + ``` + + @method keyForHasMany + @param {DS.Model subclass} type the type of the record with + the `belongsTo` relationship. + @param {String} name the relationship name to convert into a key + + @returns {String} the key + */ + keyForHasMany: function(type, name) { + return this.keyForAttributeName(type, name); + }, + + //......................... + //. MATERIALIZATION HOOKS + //......................... + + materialize: function(record, serialized, prematerialized) { + var id; + if (Ember.isNone(get(record, 'id'))) { + if (prematerialized && prematerialized.hasOwnProperty('id')) { + id = prematerialized.id; + } else { + id = this.extractId(record.constructor, serialized); + } + record.materializeId(id); + } + + this.materializeAttributes(record, serialized, prematerialized); + this.materializeRelationships(record, serialized, prematerialized); + }, + + deserializeValue: function(value, attributeType) { + var transform = this.transforms ? this.transforms[attributeType] : null; + + Ember.assert("You tried to use an attribute type (" + attributeType + ") that has not been registered", transform); + return transform.deserialize(value); + }, + + materializeAttributes: function(record, serialized, prematerialized) { + record.eachAttribute(function(name, attribute) { + if (prematerialized && prematerialized.hasOwnProperty(name)) { + record.materializeAttribute(name, prematerialized[name]); + } else { + this.materializeAttribute(record, serialized, name, attribute.type); + } + }, this); + }, + + materializeAttribute: function(record, serialized, attributeName, attributeType) { + var value = this.extractAttribute(record.constructor, serialized, attributeName); + value = this.deserializeValue(value, attributeType); + + record.materializeAttribute(attributeName, value); + }, + + materializeRelationships: function(record, serialized, prematerialized) { + record.eachRelationship(function(name, relationship) { + if (relationship.kind === 'hasMany') { + if (prematerialized && prematerialized.hasOwnProperty(name)) { + var tuplesOrReferencesOrOpaque = this._convertPrematerializedHasMany(relationship.type, prematerialized[name]); + record.materializeHasMany(name, tuplesOrReferencesOrOpaque); + } else { + this.materializeHasMany(name, record, serialized, relationship, prematerialized); + } + } else if (relationship.kind === 'belongsTo') { + if (prematerialized && prematerialized.hasOwnProperty(name)) { + var tupleOrReference = this._convertTuple(relationship.type, prematerialized[name]); + record.materializeBelongsTo(name, tupleOrReference); + } else { + this.materializeBelongsTo(name, record, serialized, relationship, prematerialized); + } + } + }, this); + }, + + materializeHasMany: function(name, record, hash, relationship) { + var type = record.constructor, + key = this._keyForHasMany(type, relationship.key), + idsOrTuples = this.extractHasMany(type, hash, key), + tuples = idsOrTuples; + + if(idsOrTuples && Ember.isArray(idsOrTuples)) { + tuples = this._convertTuples(relationship.type, idsOrTuples); + } + + record.materializeHasMany(name, tuples); + }, + + materializeBelongsTo: function(name, record, hash, relationship) { + var type = record.constructor, + key = this._keyForBelongsTo(type, relationship.key), + idOrTuple, + tuple = null; + + if(relationship.options && relationship.options.polymorphic) { + idOrTuple = this.extractBelongsToPolymorphic(type, hash, key); + } else { + idOrTuple = this.extractBelongsTo(type, hash, key); + } + + if(!isNone(idOrTuple)) { + tuple = this._convertTuple(relationship.type, idOrTuple); + } + + record.materializeBelongsTo(name, tuple); + }, + + _convertPrematerializedHasMany: function(type, prematerializedHasMany) { + var tuplesOrReferencesOrOpaque; + if( typeof prematerializedHasMany === 'string' ) { + tuplesOrReferencesOrOpaque = prematerializedHasMany; + } else { + tuplesOrReferencesOrOpaque = this._convertTuples(type, prematerializedHasMany); + } + return tuplesOrReferencesOrOpaque; + }, + + _convertTuples: function(type, idsOrTuples) { + return map.call(idsOrTuples, function(idOrTuple) { + return this._convertTuple(type, idOrTuple); + }, this); + }, + + _convertTuple: function(type, idOrTuple) { + var foundType; + + if (typeof idOrTuple === 'object') { + if (DS.Model.detect(idOrTuple.type)) { + return idOrTuple; + } else { + foundType = this.typeFromAlias(idOrTuple.type); + Ember.assert("Unable to resolve type " + idOrTuple.type + ". You may need to configure your serializer aliases.", !!foundType); + + return {id: idOrTuple.id, type: foundType}; + } + } + return {id: idOrTuple, type: type}; + }, + + /** + This method is called to get the primary key for a given + type. + + If a primary key configuration exists for this type, this + method will return the configured value. Otherwise, it will + call the public `primaryKey` hook. + + @method _primaryKey + @private + @param {DS.Model subclass} type + @returns {String} the primary key for the type + */ + _primaryKey: function(type) { + var config = this.configurationForType(type), + primaryKey = config && config.primaryKey; + + if (primaryKey) { + return primaryKey; + } else { + return this.primaryKey(type); + } + }, + + /** + This method looks up the key for the attribute name and transforms the + attribute's value using registered transforms. + + Specifically: + + 1. Look up the key for the attribute name. If available, this will use + any registered mappings. Otherwise, it will invoke the public + `keyForAttributeName` hook. + 2. Get the value from the record using the `attributeName`. + 3. Transform the value using registered transforms for the `attributeType`. + 4. Invoke the public `addAttribute` hook with the hash, key, and + transformed value. + + @method _addAttribute + @private + @param {any} data the serialized representation being built + @param {DS.Model} record the record to serialize + @param {String} attributeName the name of the attribute on the record + @param {String} attributeType the type of the attribute (e.g. `string` + or `boolean`) + */ + _addAttribute: function(data, record, attributeName, attributeType) { + var key = this._keyForAttributeName(record.constructor, attributeName); + var value = get(record, attributeName); + + this.addAttribute(data, key, this.serializeValue(value, attributeType)); + }, + + /** + This method looks up the primary key for the `type` and invokes + `serializeId` on the `id`. + + It then invokes the public `addId` hook with the primary key and + the serialized id. + + @method _addId + @private + @param {any} data the serialized representation that is being built + @param {DS.Model subclass} type + @param {any} id the materialized id from the record + */ + _addId: function(hash, type, id) { + var primaryKey = this._primaryKey(type); + + this.addId(hash, primaryKey, this.serializeId(id)); + }, + + /** + This method is called to get a key used in the data from + an attribute name. It first checks for any mappings before + calling the public hook `keyForAttributeName`. + + @method _keyForAttributeName + @private + @param {DS.Model subclass} type the type of the record with + the attribute name `name` + @param {String} name the attribute name to convert into a key + + @returns {String} the key + */ + _keyForAttributeName: function(type, name) { + return this._keyFromMappingOrHook('keyForAttributeName', type, name); + }, + + /** + This method is called to get a key used in the data from + a belongsTo relationship. It first checks for any mappings before + calling the public hook `keyForBelongsTo`. + + @method _keyForBelongsTo + @private + @param {DS.Model subclass} type the type of the record with + the `belongsTo` relationship. + @param {String} name the relationship name to convert into a key + + @returns {String} the key + */ + _keyForBelongsTo: function(type, name) { + return this._keyFromMappingOrHook('keyForBelongsTo', type, name); + }, + + keyFor: function(description) { + var type = description.parentType, + name = description.key; + + switch (description.kind) { + case 'belongsTo': + return this._keyForBelongsTo(type, name); + case 'hasMany': + return this._keyForHasMany(type, name); + } + }, + + /** + This method is called to get a key used in the data from + a hasMany relationship. It first checks for any mappings before + calling the public hook `keyForHasMany`. + + @method _keyForHasMany + @private + @param {DS.Model subclass} type the type of the record with + the `hasMany` relationship. + @param {String} name the relationship name to convert into a key + + @returns {String} the key + */ + _keyForHasMany: function(type, name) { + return this._keyFromMappingOrHook('keyForHasMany', type, name); + }, + + /** + This method converts the relationship name to a key for serialization, + and then invokes the public `addBelongsTo` hook. + + @method _addBelongsTo + @private + @param {any} data the serialized representation that is being built + @param {DS.Model} record the record to serialize + @param {String} name the relationship name + @param {Object} relationship an object representing the relationship + */ + _addBelongsTo: function(data, record, name, relationship) { + var key = this._keyForBelongsTo(record.constructor, name); + this.addBelongsTo(data, record, key, relationship); + }, + + /** + This method converts the relationship name to a key for serialization, + and then invokes the public `addHasMany` hook. + + @method _addHasMany + @private + @param {any} data the serialized representation that is being built + @param {DS.Model} record the record to serialize + @param {String} name the relationship name + @param {Object} relationship an object representing the relationship + */ + _addHasMany: function(data, record, name, relationship) { + var key = this._keyForHasMany(record.constructor, name); + this.addHasMany(data, record, key, relationship); + }, + + /** + An internal method that handles checking whether a mapping + exists for a particular attribute or relationship name before + calling the public hooks. + + If a mapping is found, and the mapping has a key defined, + use that instead of invoking the hook. + + @method _keyFromMappingOrHook + @private + @param {String} publicMethod the public hook to invoke if + a mapping is not found (e.g. `keyForAttributeName`) + @param {DS.Model subclass} type the type of the record with + the attribute or relationship name. + @param {String} name the attribute or relationship name to + convert into a key + */ + _keyFromMappingOrHook: function(publicMethod, type, name) { + var key = this.mappingOption(type, name, 'key'); + + if (key) { + return key; + } else { + return this[publicMethod](type, name); + } + }, + + /* TRANSFORMS */ + + registerTransform: function(type, transform) { + this.transforms[type] = transform; + }, + + registerEnumTransform: function(type, objects) { + var transform = { + deserialize: function(serialized) { + return Ember.A(objects).objectAt(serialized); + }, + serialize: function(deserialized) { + return Ember.EnumerableUtils.indexOf(objects, deserialized); + }, + values: objects + }; + this.registerTransform(type, transform); + }, + + /* MAPPING CONVENIENCE */ + + map: function(type, mappings) { + this.mappings.set(type, mappings); + }, + + configure: function(type, configuration) { + if (type && !configuration) { + Ember.merge(this.globalConfigurations, type); + return; + } + + var config, alias; + + if (configuration.alias) { + alias = configuration.alias; + this.aliases.set(alias, type); + delete configuration.alias; + } + + config = Ember.create(this.globalConfigurations); + Ember.merge(config, configuration); + + this.configurations.set(type, config); + }, + + typeFromAlias: function(alias) { + this._completeAliases(); + return this.aliases.get(alias); + }, + + mappingForType: function(type) { + this._reifyMappings(); + return this.mappings.get(type) || {}; + }, + + configurationForType: function(type) { + this._reifyConfigurations(); + return this.configurations.get(type) || this.globalConfigurations; + }, + + _completeAliases: function() { + this._pluralizeAliases(); + this._reifyAliases(); + }, + + _pluralizeAliases: function() { + if (this._didPluralizeAliases) { return; } + + var aliases = this.aliases, + sideloadMapping = this.aliases.sideloadMapping, + plural, + self = this; + + aliases.forEach(function(key, type) { + plural = self.pluralize(key); + Ember.assert("The '" + key + "' alias has already been defined", !aliases.get(plural)); + aliases.set(plural, type); + }); + + // This map is only for backward compatibility with the `sideloadAs` option. + if (sideloadMapping) { + sideloadMapping.forEach(function(key, type) { + Ember.assert("The '" + key + "' alias has already been defined", !aliases.get(key) || (aliases.get(key)===type) ); + aliases.set(key, type); + }); + delete this.aliases.sideloadMapping; + } + + this._didPluralizeAliases = true; + }, + + _reifyAliases: function() { + if (this._didReifyAliases) { return; } + + var aliases = this.aliases, + reifiedAliases = Ember.Map.create(), + foundType; + + aliases.forEach(function(key, type) { + if (typeof type === 'string') { + foundType = Ember.get(Ember.lookup, type); + Ember.assert("Could not find model at path " + key, type); + + reifiedAliases.set(key, foundType); + } else { + reifiedAliases.set(key, type); + } + }); + + this.aliases = reifiedAliases; + this._didReifyAliases = true; + }, + + _reifyMappings: function() { + if (this._didReifyMappings) { return; } + + var mappings = this.mappings, + reifiedMappings = Ember.Map.create(); + + mappings.forEach(function(key, mapping) { + if (typeof key === 'string') { + var type = Ember.get(Ember.lookup, key); + Ember.assert("Could not find model at path " + key, type); + + reifiedMappings.set(type, mapping); + } else { + reifiedMappings.set(key, mapping); + } + }); + + this.mappings = reifiedMappings; + + this._didReifyMappings = true; + }, + + _reifyConfigurations: function() { + if (this._didReifyConfigurations) { return; } + + var configurations = this.configurations, + reifiedConfigurations = Ember.Map.create(); + + configurations.forEach(function(key, mapping) { + if (typeof key === 'string' && key !== 'plurals') { + var type = Ember.get(Ember.lookup, key); + Ember.assert("Could not find model at path " + key, type); + + reifiedConfigurations.set(type, mapping); + } else { + reifiedConfigurations.set(key, mapping); + } + }); + + this.configurations = reifiedConfigurations; + + this._didReifyConfigurations = true; + }, + + mappingOption: function(type, name, option) { + var mapping = this.mappingForType(type)[name]; + + return mapping && mapping[option]; + }, + + configOption: function(type, option) { + var config = this.configurationForType(type); + + return config[option]; + }, + + // EMBEDDED HELPERS + + embeddedType: function(type, name) { + return this.mappingOption(type, name, 'embedded'); + }, + + eachEmbeddedRecord: function(record, callback, binding) { + this.eachEmbeddedBelongsToRecord(record, callback, binding); + this.eachEmbeddedHasManyRecord(record, callback, binding); + }, + + eachEmbeddedBelongsToRecord: function(record, callback, binding) { + this.eachEmbeddedBelongsTo(record.constructor, function(name, relationship, embeddedType) { + var embeddedRecord = get(record, name); + if (embeddedRecord) { callback.call(binding, embeddedRecord, embeddedType); } + }); + }, + + eachEmbeddedHasManyRecord: function(record, callback, binding) { + this.eachEmbeddedHasMany(record.constructor, function(name, relationship, embeddedType) { + var array = get(record, name); + for (var i=0, l=get(array, 'length'); i 'low' + Server Response / Load: { myTask: {priority: 0} } + + @method registerEnumTransform + @param {String} type of the transform + @param {Array} array of String objects to use for the enumerated values. + This is an ordered list and the index values will be used for the transform. + */ + registerEnumTransform: function(attributeType, objects) { + get(this, 'serializer').registerEnumTransform(attributeType, objects); + }, + + /** + If the globally unique IDs for your records should be generated on the client, + implement the `generateIdForRecord()` method. This method will be invoked + each time you create a new record, and the value returned from it will be + assigned to the record's `primaryKey`. + + Most traditional REST-like HTTP APIs will not use this method. Instead, the ID + of the record will be set by the server, and your adapter will update the store + with the new ID when it calls `didCreateRecord()`. Only implement this method if + you intend to generate record IDs on the client-side. + + The `generateIdForRecord()` method will be invoked with the requesting store as + the first parameter and the newly created record as the second parameter: + + generateIdForRecord: function(store, record) { + var uuid = App.generateUUIDWithStatisticallyLowOddsOfCollision(); + return uuid; + } + + @method generateIdForRecord + @param {DS.Store} store + @param {DS.Model} record + */ + generateIdForRecord: null, + + /** + Proxies to the serializer's `materialize` method. + + @method materialize + @param {DS.Model} record + @param {Object} data + @param {Object} prematerialized + */ + materialize: function(record, data, prematerialized) { + get(this, 'serializer').materialize(record, data, prematerialized); + }, + + /** + Proxies to the serializer's `serialize` method. + + @method serialize + @param {DS.Model} record + @param {Object} options + */ + serialize: function(record, options) { + return get(this, 'serializer').serialize(record, options); + }, + + /** + Proxies to the serializer's `extractId` method. + + @method extractId + @param {DS.Model} type the model class + @param {Object} data + */ + extractId: function(type, data) { + return get(this, 'serializer').extractId(type, data); + }, + + /** + @method groupByType + @private + @param enumerable + */ + groupByType: function(enumerable) { + var map = Ember.MapWithDefault.create({ + defaultValue: function() { return Ember.OrderedSet.create(); } + }); + + forEach(enumerable, function(item) { + map.get(item.constructor).add(item); + }); + + return map; + }, + + /** + The commit method is called when a transaction is being committed. + The `commitDetails` is a map with each record type and a list of + committed, updated and deleted records. + + By default, this just calls the adapter's `save` method. + If you need more advanced handling of commits, e.g., only sending + certain records to the server, you can overwrite this method. + + @method commit + @params {DS.Store} store + @params {Ember.Map} commitDetails see `DS.Transaction#commitDetails`. + */ + commit: function(store, commitDetails) { + this.save(store, commitDetails); + }, + + /** + Iterates over each set of records provided in the commit details and + filters with `DS.Adapter#shouldSave` and then calls `createRecords`, + `updateRecords`, and `deleteRecords` for each set as approriate. + + @method save + @params {DS.Store} store + @params {Ember.Map} commitDetails see `DS.Transaction#commitDetails`. + */ + save: function(store, commitDetails) { + var adapter = this; + + function filter(records) { + var filteredSet = Ember.OrderedSet.create(); + + records.forEach(function(record) { + if (adapter.shouldSave(record)) { + filteredSet.add(record); + } + }); + + return filteredSet; + } + + this.groupByType(commitDetails.created).forEach(function(type, set) { + this.createRecords(store, type, filter(set)); + }, this); + + this.groupByType(commitDetails.updated).forEach(function(type, set) { + this.updateRecords(store, type, filter(set)); + }, this); + + this.groupByType(commitDetails.deleted).forEach(function(type, set) { + this.deleteRecords(store, type, filter(set)); + }, this); + }, + + /** + Called on each record before saving. If false is returned, the record + will not be saved. + + @method shouldSave + @property {DS.Model} record + @return {Boolean} `true` to save, `false` to not. Defaults to true. + */ + shouldSave: function(record) { + return true; + }, + + /** + Implement this method in a subclass to handle the creation of + new records. + + Serializes the record and send it to the server. + + This implementation should call the adapter's `didCreateRecord` + method on success or `didError` method on failure. + + @method createRecord + @property {DS.Store} store + @property {DS.Model} type the DS.Model class of the record + @property {DS.Model} record + */ + createRecord: Ember.required(Function), + + /** + Creates multiple records at once. + + By default, it loops over the supplied array and calls `createRecord` + on each. May be overwritten to improve performance and reduce the number + of server requests. + + @method createRecords + @property {DS.Store} store + @property {DS.Model} type the DS.Model class of the records + @property {Array[DS.Model]} records + */ + createRecords: function(store, type, records) { + records.forEach(function(record) { + this.createRecord(store, type, record); + }, this); + }, + + /** + Implement this method in a subclass to handle the updating of + a record. + + Serializes the record update and send it to the server. + + @method updateRecord + @property {DS.Store} store + @property {DS.Model} type the DS.Model class of the record + @property {DS.Model} record + */ + updateRecord: Ember.required(Function), + + /** + Updates multiple records at once. + + By default, it loops over the supplied array and calls `updateRecord` + on each. May be overwritten to improve performance and reduce the number + of server requests. + + @method updateRecords + @property {DS.Store} store + @property {DS.Model} type the DS.Model class of the records + @property {Array[DS.Model]} records + */ + updateRecords: function(store, type, records) { + records.forEach(function(record) { + this.updateRecord(store, type, record); + }, this); + }, + + /** + Implement this method in a subclass to handle the deletion of + a record. + + Sends a delete request for the record to the server. + + @method deleteRecord + @property {DS.Store} store + @property {DS.Model} type the DS.Model class of the record + @property {DS.Model} record + */ + deleteRecord: Ember.required(Function), + + /** + Delete multiple records at once. + + By default, it loops over the supplied array and calls `deleteRecord` + on each. May be overwritten to improve performance and reduce the number + of server requests. + + @method deleteRecords + @property {DS.Store} store + @property {DS.Model} type the DS.Model class of the records + @property {Array[DS.Model]} records + */ + deleteRecords: function(store, type, records) { + records.forEach(function(record) { + this.deleteRecord(store, type, record); + }, this); + }, + + /** + Find multiple records at once. + + By default, it loops over the provided ids and calls `find` on each. + May be overwritten to improve performance and reduce the number of + server requests. + + @method findMany + @property {DS.Store} store + @property {DS.Model} type the DS.Model class of the records + @property {Array} ids + */ + findMany: function(store, type, ids) { + ids.forEach(function(id) { + this.find(store, type, id); + }, this); + } +}); + +DS.Adapter.reopenClass({ + + /** + Registers a custom attribute transform for the adapter class + + The `transform` property is an object with a `serialize` and + `deserialize` property. These are each functions that respectively + serialize the data to send to the backend or deserialize it for + use on the client. + + @method registerTransform + @static + @property {DS.String} attributeType + @property {Object} transform + */ + registerTransform: function(attributeType, transform) { + var registeredTransforms = this._registeredTransforms || {}; + + registeredTransforms[attributeType] = transform; + + this._registeredTransforms = registeredTransforms; + }, + + /** + Registers a custom enumerable transform for the adapter class + + @method registerEnumTransform + @static + @property {DS.String} attributeType + @property objects + */ + registerEnumTransform: function(attributeType, objects) { + var registeredEnumTransforms = this._registeredEnumTransforms || {}; + + registeredEnumTransforms[attributeType] = objects; + + this._registeredEnumTransforms = registeredEnumTransforms; + }, + + /** + Set adapter attributes for a DS.Model class. + + @method map + @static + @property {DS.Model} type the DS.Model class + @property {Object} attributes + */ + map: DS._Mappable.generateMapFunctionFor('attributes', function(key, newValue, map) { + var existingValue = map.get(key); + + merge(existingValue, newValue); + }), + + /** + Set configuration options for a DS.Model class. + + @method configure + @static + @property {DS.Model} type the DS.Model class + @property {Object} configuration + */ + configure: DS._Mappable.generateMapFunctionFor('configurations', function(key, newValue, map) { + var existingValue = map.get(key); + + // If a mapping configuration is provided, peel it off and apply it + // using the DS.Adapter.map API. + var mappings = newValue && newValue.mappings; + if (mappings) { + this.map(key, mappings); + delete newValue.mappings; + } + + merge(existingValue, newValue); + }), + + /** + Resolved conflicts in configuration settings. + + Calls `Ember.merge` by default. + + @method resolveMapConflict + @static + @property oldValue + @property newValue + */ + resolveMapConflict: function(oldValue, newValue) { + merge(newValue, oldValue); + + return newValue; + } +}); + +})(); + + + +(function() { +/** + @module ember-data +*/ + +var get = Ember.get, set = Ember.set; + +/** + @class FixtureSerializer + @namespace DS + @extends DS.Serializer +*/ +DS.FixtureSerializer = DS.Serializer.extend({ + + /** + @method deserializeValue + @param value + @param attributeType + */ + deserializeValue: function(value, attributeType) { + return value; + }, + + /** + @method serializeValue + @param value + @param attributeType + */ + serializeValue: function(value, attributeType) { + return value; + }, + + /** + @method addId + @param data + @param key + @param id + */ + addId: function(data, key, id) { + data[key] = id; + }, + + /** + @method addAttribute + @param hash + @param key + @param value + */ + addAttribute: function(hash, key, value) { + hash[key] = value; + }, + + /** + @method addBelongsTo + @param hash + @param record + @param key + @param relationship + */ + addBelongsTo: function(hash, record, key, relationship) { + var id = get(record, relationship.key+'.id'); + if (!Ember.isNone(id)) { hash[key] = id; } + }, + + /** + @method addHasMany + @param hash + @param record + @param key + @param relationship + */ + addHasMany: function(hash, record, key, relationship) { + var ids = get(record, relationship.key).map(function(item) { + return item.get('id'); + }); + + hash[relationship.key] = ids; + }, + + /** + @method extract + @param loader + @param fixture + @param type + @param record + */ + extract: function(loader, fixture, type, record) { + if (record) { loader.updateId(record, fixture); } + this.extractRecordRepresentation(loader, type, fixture); + }, + + /** + @method extractMany + @param loader + @param fixtures + @param type + @param records + */ + extractMany: function(loader, fixtures, type, records) { + var objects = fixtures, references = []; + if (records) { records = records.toArray(); } + + for (var i = 0; i < objects.length; i++) { + if (records) { loader.updateId(records[i], objects[i]); } + var reference = this.extractRecordRepresentation(loader, type, objects[i]); + references.push(reference); + } + + loader.populateArray(references); + }, + + /** + @method extractId + @param type + @param hash + */ + extractId: function(type, hash) { + var primaryKey = this._primaryKey(type); + + if (hash.hasOwnProperty(primaryKey)) { + // Ensure that we coerce IDs to strings so that record + // IDs remain consistent between application runs; especially + // if the ID is serialized and later deserialized from the URL, + // when type information will have been lost. + return hash[primaryKey]+''; + } else { + return null; + } + }, + + /** + @method extractAttribute + @param type + @param hash + @param attributeName + */ + extractAttribute: function(type, hash, attributeName) { + var key = this._keyForAttributeName(type, attributeName); + return hash[key]; + }, + + /** + @method extractHasMany + @param type + @param hash + @param key + */ + extractHasMany: function(type, hash, key) { + return hash[key]; + }, + + /** + @method extractBelongsTo + @param type + @param hash + @param key + */ + extractBelongsTo: function(type, hash, key) { + var val = hash[key]; + if (val != null) { + val = val + ''; + } + return val; + }, + + /** + @method extractBelongsToPolymorphic + @method type + @method hash + @method key + */ + extractBelongsToPolymorphic: function(type, hash, key) { + var keyForId = this.keyForPolymorphicId(key), + keyForType, + id = hash[keyForId]; + + if (id) { + keyForType = this.keyForPolymorphicType(key); + return {id: id, type: hash[keyForType]}; + } + + return null; + }, + + /** + @method keyForPolymorphicId + @param key + */ + keyForPolymorphicId: function(key) { + return key; + }, + + /** + @method keyForPolymorphicType + @param key + */ + keyForPolymorphicType: function(key) { + return key + '_type'; + } +}); + +})(); + + + +(function() { +/** + @module ember-data +*/ + +var get = Ember.get, fmt = Ember.String.fmt, + indexOf = Ember.EnumerableUtils.indexOf; + +/** + `DS.FixtureAdapter` is an adapter that loads records from memory. + Its primarily used for development and testing. You can also use + `DS.FixtureAdapter` while working on the API but are not ready to + integrate yet. It is a fully functioning adapter. All CRUD methods + are implemented. You can also implement query logic that a remote + system would do. Its possible to do develop your entire application + with `DS.FixtureAdapter`. + + @class FixtureAdapter + @namespace DS + @extends DS.Adapter +*/ +DS.FixtureAdapter = DS.Adapter.extend({ + + simulateRemoteResponse: true, + + latency: 50, + + serializer: DS.FixtureSerializer, + + /** + Implement this method in order to provide data associated with a type + + @method fixturesForType + @param type + */ + fixturesForType: function(type) { + if (type.FIXTURES) { + var fixtures = Ember.A(type.FIXTURES); + return fixtures.map(function(fixture){ + var fixtureIdType = typeof fixture.id; + if(fixtureIdType !== "number" && fixtureIdType !== "string"){ + throw new Error(fmt('the id property must be defined as a number or string for fixture %@', [fixture])); + } + fixture.id = fixture.id + ''; + return fixture; + }); + } + return null; + }, + + /** + Implement this method in order to query fixtures data + + @method queryFixtures + @param fixture + @param query + @param type + */ + queryFixtures: function(fixtures, query, type) { + Ember.assert('Not implemented: You must override the DS.FixtureAdapter::queryFixtures method to support querying the fixture store.'); + }, + + /** + @method updateFixtures + @param type + @param fixture + */ + updateFixtures: function(type, fixture) { + if(!type.FIXTURES) { + type.FIXTURES = []; + } + + var fixtures = type.FIXTURES; + + this.deleteLoadedFixture(type, fixture); + + fixtures.push(fixture); + }, + + /** + Implement this method in order to provide provide json for CRUD methods + + @method mockJSON + @param type + @param record + */ + mockJSON: function(type, record) { + return this.serialize(record, { includeId: true }); + }, + + /** + @method generateIdForRecord + @param store + @param record + */ + generateIdForRecord: function(store, record) { + return Ember.guidFor(record); + }, + + /** + @method find + @param store + @param type + @param id + */ + find: function(store, type, id) { + var fixtures = this.fixturesForType(type), + fixture; + + Ember.warn("Unable to find fixtures for model type " + type.toString(), fixtures); + + if (fixtures) { + fixture = Ember.A(fixtures).findProperty('id', id); + } + + if (fixture) { + this.simulateRemoteCall(function() { + this.didFindRecord(store, type, fixture, id); + }, this); + } + }, + + /** + @method findMany + @param store + @param type + @param ids + */ + findMany: function(store, type, ids) { + var fixtures = this.fixturesForType(type); + + Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures); + + if (fixtures) { + fixtures = fixtures.filter(function(item) { + return indexOf(ids, item.id) !== -1; + }); + } + + if (fixtures) { + this.simulateRemoteCall(function() { + this.didFindMany(store, type, fixtures); + }, this); + } + }, + + /** + @method findAll + @param store + @param type + */ + findAll: function(store, type) { + var fixtures = this.fixturesForType(type); + + Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures); + + this.simulateRemoteCall(function() { + this.didFindAll(store, type, fixtures); + }, this); + }, + + /** + @method findQuery + @param store + @param type + @param query + @param array + */ + findQuery: function(store, type, query, array) { + var fixtures = this.fixturesForType(type); + + Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures); + + fixtures = this.queryFixtures(fixtures, query, type); + + if (fixtures) { + this.simulateRemoteCall(function() { + this.didFindQuery(store, type, fixtures, array); + }, this); + } + }, + + /** + @method createRecord + @param store + @param type + @param record + */ + createRecord: function(store, type, record) { + var fixture = this.mockJSON(type, record); + + this.updateFixtures(type, fixture); + + this.simulateRemoteCall(function() { + this.didCreateRecord(store, type, record, fixture); + }, this); + }, + + /** + @method updateRecord + @param store + @param type + @param record + */ + updateRecord: function(store, type, record) { + var fixture = this.mockJSON(type, record); + + this.updateFixtures(type, fixture); + + this.simulateRemoteCall(function() { + this.didUpdateRecord(store, type, record, fixture); + }, this); + }, + + /** + @method deleteRecord + @param store + @param type + @param record + */ + deleteRecord: function(store, type, record) { + var fixture = this.mockJSON(type, record); + + this.deleteLoadedFixture(type, fixture); + + this.simulateRemoteCall(function() { + this.didDeleteRecord(store, type, record); + }, this); + }, + + /* + @method deleteLoadedFixture + @private + @param type + @param record + */ + deleteLoadedFixture: function(type, record) { + var existingFixture = this.findExistingFixture(type, record); + + if(existingFixture) { + var index = indexOf(type.FIXTURES, existingFixture); + type.FIXTURES.splice(index, 1); + return true; + } + }, + + /* + @method findExistingFixture + @private + @param type + @param record + */ + findExistingFixture: function(type, record) { + var fixtures = this.fixturesForType(type); + var id = this.extractId(type, record); + + return this.findFixtureById(fixtures, id); + }, + + /* + @method findFixtureById + @private + @param type + @param record + */ + findFixtureById: function(fixtures, id) { + return Ember.A(fixtures).find(function(r) { + if(''+get(r, 'id') === ''+id) { + return true; + } else { + return false; + } + }); + }, + + /* + @method simulateRemoteCall + @private + @param callback + @param context + */ + simulateRemoteCall: function(callback, context) { + if (get(this, 'simulateRemoteResponse')) { + // Schedule with setTimeout + Ember.run.later(context, callback, get(this, 'latency')); + } else { + // Asynchronous, but at the of the runloop with zero latency + Ember.run.once(context, callback); + } + } +}); + +})(); + + + +(function() { +/** + @module ember-data +*/ + +var get = Ember.get; + +/** + @class RESTSerializer + @namespace DS + @extends DS.Serializer +*/ +DS.RESTSerializer = DS.JSONSerializer.extend({ + + /** + @method keyForAttributeName + @param type + @param name + */ + keyForAttributeName: function(type, name) { + return Ember.String.decamelize(name); + }, + + /** + @method keyForBelongsTo + @param type + @param name + */ + keyForBelongsTo: function(type, name) { + var key = this.keyForAttributeName(type, name); + + if (this.embeddedType(type, name)) { + return key; + } + + return key + "_id"; + }, + + /** + @method keyForHasMany + @param type + @param name + */ + keyForHasMany: function(type, name) { + var key = this.keyForAttributeName(type, name); + + if (this.embeddedType(type, name)) { + return key; + } + + return this.singularize(key) + "_ids"; + }, + + /** + @method keyForPolymorphicId + @param key + */ + keyForPolymorphicId: function(key) { + return key; + }, + + /** + @method keyForPolymorphicType + @param key + */ + keyForPolymorphicType: function(key) { + return key.replace(/_id$/, '_type'); + }, + + /** + @method extractValidationErrors + @param type + @param json + */ + extractValidationErrors: function(type, json) { + var errors = {}; + + get(type, 'attributes').forEach(function(name) { + var key = this._keyForAttributeName(type, name); + if (json['errors'].hasOwnProperty(key)) { + errors[name] = json['errors'][key]; + } + }, this); + + return errors; + } +}); + +})(); + + + +(function() { +/** + @module ember-data +*/ + +var get = Ember.get, set = Ember.set; + +DS.rejectionHandler = function(reason) { + Ember.Logger.assert([reason, reason.message, reason.stack]); + + throw reason; +}; + +/** + The REST adapter allows your store to communicate with an HTTP server by + transmitting JSON via XHR. Most Ember.js apps that consume a JSON API + should use the REST adapter. + + This adapter is designed around the idea that the JSON exchanged with + the server should be conventional. + + ## JSON Structure + + The REST adapter expects the JSON returned from your server to follow + these conventions. + + ### Object Root + + The JSON payload should be an object that contains the record inside a + root property. For example, in response to a `GET` request for + `/posts/1`, the JSON should look like this: + + ```js + { + "post": { + title: "I'm Running to Reform the W3C's Tag", + author: "Yehuda Katz" + } + } + ``` + + ### Conventional Names + + Attribute names in your JSON payload should be the underscored versions of + the attributes in your Ember.js models. + + For example, if you have a `Person` model: + + ```js + App.Person = DS.Model.extend({ + firstName: DS.attr('string'), + lastName: DS.attr('string'), + occupation: DS.attr('string') + }); + ``` + + The JSON returned should look like this: + + ```js + { + "person": { + "first_name": "Barack", + "last_name": "Obama", + "occupation": "President" + } + } + ``` + + ## Customization + + ### Endpoint path customization + + Endpoint paths can be prefixed with a `namespace` by setting the namespace + property on the adapter: + + ```js + DS.RESTAdapter.reopen({ + namespace: 'api/1' + }); + ``` + Requests for `App.Person` would now target `/api/1/people/1`. + + ### Host customization + + An adapter can target other hosts by setting the `url` property. + + ```js + DS.RESTAdapter.reopen({ + url: 'https://api.example.com' + }); + ``` + + ### Headers customization + + Some APIs require HTTP headers, eg to provide an API key. An array of + headers can be added to the adapter which are passed with every request: + + ```js + DS.RESTAdapter.reopen({ + headers: { + "API_KEY": "secret key", + "ANOTHER_HEADER": "asdsada" + } + }); + ``` + + @class RESTAdapter + @constructor + @namespace DS + @extends DS.Adapter +*/ +DS.RESTAdapter = DS.Adapter.extend({ + namespace: null, + bulkCommit: false, + since: 'since', + + serializer: DS.RESTSerializer, + + /** + Called on each record before saving. If false is returned, the record + will not be saved. + + By default, this method returns `true` except when the record is embedded. + + @method shouldSave + @property {DS.Model} record + @return {Boolean} `true` to save, `false` to not. Defaults to true. + */ + shouldSave: function(record) { + var reference = get(record, '_reference'); + + return !reference.parent; + }, + + /** + @method dirtyRecordsForRecordChange + @param {Ember.OrderedSet} dirtySet + @param {DS.Model} record + */ + dirtyRecordsForRecordChange: function(dirtySet, record) { + this._dirtyTree(dirtySet, record); + }, + + /** + @method dirtyRecordsForHasManyChange + @param {Ember.OrderedSet} dirtySet + @param {DS.Model} record + @param {DS.RelationshipChange} relationship + */ + dirtyRecordsForHasManyChange: function(dirtySet, record, relationship) { + var embeddedType = get(this, 'serializer').embeddedType(record.constructor, relationship.secondRecordName); + + if (embeddedType === 'always') { + relationship.childReference.parent = relationship.parentReference; + this._dirtyTree(dirtySet, record); + } + }, + + /** + @method _dirtyTree + @private + @param {Ember.OrderedSet} dirtySet + @param {DS.Model} record + */ + _dirtyTree: function(dirtySet, record) { + dirtySet.add(record); + + get(this, 'serializer').eachEmbeddedRecord(record, function(embeddedRecord, embeddedType) { + if (embeddedType !== 'always') { return; } + if (dirtySet.has(embeddedRecord)) { return; } + this._dirtyTree(dirtySet, embeddedRecord); + }, this); + + var reference = record.get('_reference'); + + if (reference.parent) { + var store = get(record, 'store'); + var parent = store.recordForReference(reference.parent); + this._dirtyTree(dirtySet, parent); + } + }, + + /** + Serializes the record and sends it to the server. + + By default, the record is serialized with the adapter's `serialize` + method and assigned to a root obtained by the `rootForType` method. + + The url is created with `buildURL` and then called as a 'POST' request + with the adapter's `ajax` method. + + If successful, the adapter's `didCreateRecord` method is called, + otherwise `didError` + + @method createRecord + @property {DS.Store} store + @property {DS.Model} type the DS.Model class of the record + @property {DS.Model} record + */ + createRecord: function(store, type, record) { + var root = this.rootForType(type); + var adapter = this; + var data = {}; + + data[root] = this.serialize(record, { includeId: true }); + + return this.ajax(this.buildURL(root), "POST", { + data: data + }).then(function(json){ + adapter.didCreateRecord(store, type, record, json); + }, function(xhr) { + adapter.didError(store, type, record, xhr); + throw xhr; + }).then(null, DS.rejectionHandler); + }, + + /** + @method createRecords + @param store + @param type + @param records + */ + createRecords: function(store, type, records) { + var adapter = this; + + if (get(this, 'bulkCommit') === false) { + return this._super(store, type, records); + } + + var root = this.rootForType(type), + plural = this.pluralize(root); + + var data = {}; + data[plural] = []; + records.forEach(function(record) { + data[plural].push(this.serialize(record, { includeId: true })); + }, this); + + return this.ajax(this.buildURL(root), "POST", { + data: data + }).then(function(json) { + adapter.didCreateRecords(store, type, records, json); + }).then(null, DS.rejectionHandler); + }, + + /** + @method updateRecord + @param store + @param type + @param record + */ + updateRecord: function(store, type, record) { + var id, root, adapter, data; + + id = get(record, 'id'); + root = this.rootForType(type); + adapter = this; + + data = {}; + data[root] = this.serialize(record); + + return this.ajax(this.buildURL(root, id, record), "PUT",{ + data: data + }).then(function(json){ + adapter.didUpdateRecord(store, type, record, json); + }, function(xhr) { + adapter.didError(store, type, record, xhr); + throw xhr; + }).then(null, DS.rejectionHandler); + }, + + /** + @method updateRecords + @param store + @param type + @param records + */ + updateRecords: function(store, type, records) { + var root, plural, adapter, data; + + if (get(this, 'bulkCommit') === false) { + return this._super(store, type, records); + } + + root = this.rootForType(type); + plural = this.pluralize(root); + adapter = this; + + data = {}; + + data[plural] = []; + + records.forEach(function(record) { + data[plural].push(this.serialize(record, { includeId: true })); + }, this); + + return this.ajax(this.buildURL(root, "bulk"), "PUT", { + data: data + }).then(function(json) { + adapter.didUpdateRecords(store, type, records, json); + }).then(null, DS.rejectionHandler); + }, + + /** + @method deleteRecord + @param store + @param type + @param record + */ + deleteRecord: function(store, type, record) { + var id, root, adapter; + + id = get(record, 'id'); + root = this.rootForType(type); + adapter = this; + + return this.ajax(this.buildURL(root, id, record), "DELETE").then(function(json){ + adapter.didDeleteRecord(store, type, record, json); + }, function(xhr){ + adapter.didError(store, type, record, xhr); + throw xhr; + }).then(null, DS.rejectionHandler); + }, + + /** + @method deleteRecords + @param store + @param type + @param records + */ + deleteRecords: function(store, type, records) { + var root, plural, serializer, adapter, data; + + if (get(this, 'bulkCommit') === false) { + return this._super(store, type, records); + } + + root = this.rootForType(type); + plural = this.pluralize(root); + serializer = get(this, 'serializer'); + adapter = this; + + data = {}; + + data[plural] = []; + records.forEach(function(record) { + data[plural].push(serializer.serializeId( get(record, 'id') )); + }); + + return this.ajax(this.buildURL(root, 'bulk'), "DELETE", { + data: data + }).then(function(json){ + adapter.didDeleteRecords(store, type, records, json); + }).then(null, DS.rejectionHandler); + }, + + /** + @method find + @param store + @param type + @param id + */ + find: function(store, type, id) { + var root = this.rootForType(type), adapter = this; + + return this.ajax(this.buildURL(root, id), "GET"). + then(function(json){ + adapter.didFindRecord(store, type, json, id); + }).then(null, DS.rejectionHandler); + }, + + /** + @method findAll + @param store + @param type + @param since + */ + findAll: function(store, type, since) { + var root, adapter; + + root = this.rootForType(type); + adapter = this; + + return this.ajax(this.buildURL(root), "GET",{ + data: this.sinceQuery(since) + }).then(function(json) { + adapter.didFindAll(store, type, json); + }).then(null, DS.rejectionHandler); + }, + + /** + @method findQuery + @param store + @param type + @param query + @param recordArray + */ + findQuery: function(store, type, query, recordArray) { + var root = this.rootForType(type), + adapter = this; + + return this.ajax(this.buildURL(root), "GET", { + data: query + }).then(function(json){ + adapter.didFindQuery(store, type, json, recordArray); + }).then(null, DS.rejectionHandler); + }, + + /** + @method findMany + @param store + @param type + @param ids + @param owner + */ + findMany: function(store, type, ids, owner) { + var root = this.rootForType(type), + adapter = this; + + ids = this.serializeIds(ids); + + return this.ajax(this.buildURL(root), "GET", { + data: {ids: ids} + }).then(function(json) { + adapter.didFindMany(store, type, json); + }).then(null, DS.rejectionHandler); + }, + + /** + This method serializes a list of IDs using `serializeId` + + @method serializeIds + @private + @param ids + @return {Array} an array of serialized IDs + */ + serializeIds: function(ids) { + var serializer = get(this, 'serializer'); + + return Ember.EnumerableUtils.map(ids, function(id) { + return serializer.serializeId(id); + }); + }, + + /** + @method didError + @private + @param store + @param type + @param record + @param xhr + */ + didError: function(store, type, record, xhr) { + if (xhr.status === 422) { + var json = JSON.parse(xhr.responseText), + serializer = get(this, 'serializer'), + errors = serializer.extractValidationErrors(type, json); + + store.recordWasInvalid(record, errors); + } else { + this._super.apply(this, arguments); + } + }, + + /** + @method ajax + @private + @param url + @param type + @param hash + */ + ajax: function(url, type, hash) { + var adapter = this; + + return new Ember.RSVP.Promise(function(resolve, reject) { + hash = hash || {}; + hash.url = url; + hash.type = type; + hash.dataType = 'json'; + hash.context = adapter; + + if (hash.data && type !== 'GET') { + hash.contentType = 'application/json; charset=utf-8'; + hash.data = JSON.stringify(hash.data); + } + + if (adapter.headers !== undefined) { + var headers = adapter.headers; + hash.beforeSend = function (xhr) { + Ember.keys(headers).forEach(function(key) { + xhr.setRequestHeader(key, headers[key]); + }); + }; + } + + hash.success = function(json) { + Ember.run(null, resolve, json); + }; + + hash.error = function(jqXHR, textStatus, errorThrown) { + if (jqXHR) { + jqXHR.then = null; + } + + Ember.run(null, reject, jqXHR); + }; + + Ember.$.ajax(hash); + }); + }, + + /** + @property url + @default '' + */ + url: "", + + /** + @method rootForType + @private + @param type + */ + rootForType: function(type) { + var serializer = get(this, 'serializer'); + return serializer.rootForType(type); + }, + + /** + @method pluralize + @private + @param string + */ + pluralize: function(string) { + var serializer = get(this, 'serializer'); + return serializer.pluralize(string); + }, + + /** + @method buildURL + @private + @param root + @param suffix + @param record + */ + buildURL: function(root, suffix, record) { + var url = [this.url]; + + Ember.assert("Namespace URL (" + this.namespace + ") must not start with slash", !this.namespace || this.namespace.toString().charAt(0) !== "/"); + Ember.assert("Root URL (" + root + ") must not start with slash", !root || root.toString().charAt(0) !== "/"); + Ember.assert("URL suffix (" + suffix + ") must not start with slash", !suffix || suffix.toString().charAt(0) !== "/"); + + if (!Ember.isNone(this.namespace)) { + url.push(this.namespace); + } + + url.push(this.pluralize(root)); + if (suffix !== undefined) { + url.push(suffix); + } + + return url.join("/"); + }, + + /** + @method sinceQuery + @private + @param since + */ + sinceQuery: function(since) { + var query = {}; + query[get(this, 'since')] = since; + return since ? query : null; + } +}); + +})(); + + + +(function() { +/** + @module ember-data +*/ + +})(); + + + +(function() { +DS.Model.reopen({ + + /** + Provides info about the model for debugging purposes + by grouping the properties into more semantic groups. + + Meant to be used by debugging tools such as the Chrome Ember Extension. + + - Groups all attributes in "Attributes" group. + - Groups all belongsTo relationships in "Belongs To" group. + - Groups all hasMany relationships in "Has Many" group. + - Groups all flags in "Flags" group. + - Flags relationship CPs as expensive properties. + */ + _debugInfo: function() { + var attributes = ['id'], + relationships = { belongsTo: [], hasMany: [] }, + expensiveProperties = []; + + this.eachAttribute(function(name, meta) { + attributes.push(name); + }, this); + + this.eachRelationship(function(name, relationship) { + relationships[relationship.kind].push(name); + expensiveProperties.push(name); + }); + + var groups = [ + { + name: 'Attributes', + properties: attributes, + expand: true, + }, + { + name: 'Belongs To', + properties: relationships.belongsTo, + expand: true + }, + { + name: 'Has Many', + properties: relationships.hasMany, + expand: true + }, + { + name: 'Flags', + properties: ['isLoaded', 'isDirty', 'isSaving', 'isDeleted', 'isError', 'isNew', 'isValid'] + } + ]; + + return { + propertyInfo: { + // include all other mixins / properties (not just the grouped ones) + includeOtherProperties: true, + groups: groups, + // don't pre-calculate unless cached + expensiveProperties: expensiveProperties + } + }; + } + +}); + +})(); + + + +(function() { +/** + @module ember-data +*/ + +})(); + + + +(function() { +//Copyright (C) 2011 by Living Social, Inc. + +//Permission is hereby granted, free of charge, to any person obtaining a copy of +//this software and associated documentation files (the "Software"), to deal in +//the Software without restriction, including without limitation the rights to +//use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +//of the Software, and to permit persons to whom the Software is furnished to do +//so, subject to the following conditions: + +//The above copyright notice and this permission notice shall be included in all +//copies or substantial portions of the Software. + +//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +//SOFTWARE. + +/** + Ember Data + + @module ember-data + @main ember-data +*/ + +})(); + + +})(); diff --git a/vendor/assets/ember/development/ember-template-compiler.js b/vendor/assets/ember/development/ember-template-compiler.js new file mode 100644 index 000000000..780391b49 --- /dev/null +++ b/vendor/assets/ember/development/ember-template-compiler.js @@ -0,0 +1,338 @@ +// Fetched from channel: tags/v1.6.1, with url http://builds.emberjs.com/tags/v1.6.1/ember-template-compiler.js +// Fetched on: 2024-01-19T09:48:19Z +/*! + * @overview Ember - JavaScript Application Framework + * @copyright Copyright 2011-2014 Tilde Inc. and contributors + * Portions Copyright 2006-2011 Strobe Inc. + * Portions Copyright 2008-2011 Apple Inc. All rights reserved. + * @license Licensed under MIT license + * See https://raw.github.com/emberjs/ember.js/master/LICENSE + * @version 1.6.1 + */ + + +(function() { +var Ember = { assert: function() {}, FEATURES: { isEnabled: function() {} } }; +/** +@module ember +@submodule ember-handlebars-compiler +*/ + + + +// ES6Todo: you'll need to import debugger once debugger is es6'd. +if (typeof Ember.assert === 'undefined') { Ember.assert = function(){}; }; +if (typeof Ember.FEATURES === 'undefined') { Ember.FEATURES = { isEnabled: function(){} }; }; + +var objectCreate = Object.create || function(parent) { + function F() {} + F.prototype = parent; + return new F(); +}; + +// set up for circular references later +var View, Component; + +// ES6Todo: when ember-debug is es6'ed import this. +// var emberAssert = Ember.assert; +var Handlebars = (Ember.imports && Ember.imports.Handlebars) || (this && this.Handlebars); +if (!Handlebars && typeof require === 'function') { + Handlebars = require('handlebars'); +} + +Ember.assert("Ember Handlebars requires Handlebars version 1.0 or 1.1. Include " + + "a SCRIPT tag in the HTML HEAD linking to the Handlebars file " + + "before you link to Ember.", Handlebars); + +Ember.assert("Ember Handlebars requires Handlebars version 1.0 or 1.1, " + + "COMPILER_REVISION expected: 4, got: " + Handlebars.COMPILER_REVISION + + " - Please note: Builds of master may have other COMPILER_REVISION values.", + Handlebars.COMPILER_REVISION === 4); + +/** + Prepares the Handlebars templating library for use inside Ember's view + system. + + The `Ember.Handlebars` object is the standard Handlebars library, extended to + use Ember's `get()` method instead of direct property access, which allows + computed properties to be used inside templates. + + To create an `Ember.Handlebars` template, call `Ember.Handlebars.compile()`. + This will return a function that can be used by `Ember.View` for rendering. + + @class Handlebars + @namespace Ember +*/ +var EmberHandlebars = Ember.Handlebars = objectCreate(Handlebars); + +/** + Register a bound helper or custom view helper. + + ## Simple bound helper example + + ```javascript + Ember.Handlebars.helper('capitalize', function(value) { + return value.toUpperCase(); + }); + ``` + + The above bound helper can be used inside of templates as follows: + + ```handlebars + {{capitalize name}} + ``` + + In this case, when the `name` property of the template's context changes, + the rendered value of the helper will update to reflect this change. + + For more examples of bound helpers, see documentation for + `Ember.Handlebars.registerBoundHelper`. + + ## Custom view helper example + + Assuming a view subclass named `App.CalendarView` were defined, a helper + for rendering instances of this view could be registered as follows: + + ```javascript + Ember.Handlebars.helper('calendar', App.CalendarView): + ``` + + The above bound helper can be used inside of templates as follows: + + ```handlebars + {{calendar}} + ``` + + Which is functionally equivalent to: + + ```handlebars + {{view App.CalendarView}} + ``` + + Options in the helper will be passed to the view in exactly the same + manner as with the `view` helper. + + @method helper + @for Ember.Handlebars + @param {String} name + @param {Function|Ember.View} function or view class constructor + @param {String} dependentKeys* +*/ +EmberHandlebars.helper = function(name, value) { + if (!View) { View = requireModule('ember-views/views/view')['View']; } // ES6TODO: stupid circular dep + if (!Component) { Component = requireModule('ember-views/views/component')['default']; } // ES6TODO: stupid circular dep + + Ember.assert("You tried to register a component named '" + name + "', but component names must include a '-'", !Component.detect(value) || name.match(/-/)); + + if (View.detect(value)) { + EmberHandlebars.registerHelper(name, EmberHandlebars.makeViewHelper(value)); + } else { + EmberHandlebars.registerBoundHelper.apply(null, arguments); + } +}; + +/** + Returns a helper function that renders the provided ViewClass. + + Used internally by Ember.Handlebars.helper and other methods + involving helper/component registration. + + @private + @method makeViewHelper + @for Ember.Handlebars + @param {Function} ViewClass view class constructor + @since 1.2.0 +*/ +EmberHandlebars.makeViewHelper = function(ViewClass) { + return function(options) { + Ember.assert("You can only pass attributes (such as name=value) not bare values to a helper for a View found in '" + ViewClass.toString() + "'", arguments.length < 2); + return EmberHandlebars.helpers.view.call(this, ViewClass, options); + }; +}; + +/** +@class helpers +@namespace Ember.Handlebars +*/ +EmberHandlebars.helpers = objectCreate(Handlebars.helpers); + +/** + Override the the opcode compiler and JavaScript compiler for Handlebars. + + @class Compiler + @namespace Ember.Handlebars + @private + @constructor +*/ +EmberHandlebars.Compiler = function() {}; + +// Handlebars.Compiler doesn't exist in runtime-only +if (Handlebars.Compiler) { + EmberHandlebars.Compiler.prototype = objectCreate(Handlebars.Compiler.prototype); +} + +EmberHandlebars.Compiler.prototype.compiler = EmberHandlebars.Compiler; + +/** + @class JavaScriptCompiler + @namespace Ember.Handlebars + @private + @constructor +*/ +EmberHandlebars.JavaScriptCompiler = function() {}; + +// Handlebars.JavaScriptCompiler doesn't exist in runtime-only +if (Handlebars.JavaScriptCompiler) { + EmberHandlebars.JavaScriptCompiler.prototype = objectCreate(Handlebars.JavaScriptCompiler.prototype); + EmberHandlebars.JavaScriptCompiler.prototype.compiler = EmberHandlebars.JavaScriptCompiler; +} + + +EmberHandlebars.JavaScriptCompiler.prototype.namespace = "Ember.Handlebars"; + +EmberHandlebars.JavaScriptCompiler.prototype.initializeBuffer = function() { + return "''"; +}; + +/** + Override the default buffer for Ember Handlebars. By default, Handlebars + creates an empty String at the beginning of each invocation and appends to + it. Ember's Handlebars overrides this to append to a single shared buffer. + + @private + @method appendToBuffer + @param string {String} +*/ +EmberHandlebars.JavaScriptCompiler.prototype.appendToBuffer = function(string) { + return "data.buffer.push("+string+");"; +}; + +// Hacks ahead: +// Handlebars presently has a bug where the `blockHelperMissing` hook +// doesn't get passed the name of the missing helper name, but rather +// gets passed the value of that missing helper evaluated on the current +// context, which is most likely `undefined` and totally useless. +// +// So we alter the compiled template function to pass the name of the helper +// instead, as expected. +// +// This can go away once the following is closed: +// https://github.com/wycats/handlebars.js/issues/634 + +var DOT_LOOKUP_REGEX = /helpers\.(.*?)\)/, + BRACKET_STRING_LOOKUP_REGEX = /helpers\['(.*?)'/, + INVOCATION_SPLITTING_REGEX = /(.*blockHelperMissing\.call\(.*)(stack[0-9]+)(,.*)/; + +EmberHandlebars.JavaScriptCompiler.stringifyLastBlockHelperMissingInvocation = function(source) { + var helperInvocation = source[source.length - 1], + helperName = (DOT_LOOKUP_REGEX.exec(helperInvocation) || BRACKET_STRING_LOOKUP_REGEX.exec(helperInvocation))[1], + matches = INVOCATION_SPLITTING_REGEX.exec(helperInvocation); + + source[source.length - 1] = matches[1] + "'" + helperName + "'" + matches[3]; +}; + +var stringifyBlockHelperMissing = EmberHandlebars.JavaScriptCompiler.stringifyLastBlockHelperMissingInvocation; + +var originalBlockValue = EmberHandlebars.JavaScriptCompiler.prototype.blockValue; +EmberHandlebars.JavaScriptCompiler.prototype.blockValue = function() { + originalBlockValue.apply(this, arguments); + stringifyBlockHelperMissing(this.source); +}; + +var originalAmbiguousBlockValue = EmberHandlebars.JavaScriptCompiler.prototype.ambiguousBlockValue; +EmberHandlebars.JavaScriptCompiler.prototype.ambiguousBlockValue = function() { + originalAmbiguousBlockValue.apply(this, arguments); + stringifyBlockHelperMissing(this.source); +}; + +/** + Rewrite simple mustaches from `{{foo}}` to `{{bind "foo"}}`. This means that + all simple mustaches in Ember's Handlebars will also set up an observer to + keep the DOM up to date when the underlying property changes. + + @private + @method mustache + @for Ember.Handlebars.Compiler + @param mustache +*/ +EmberHandlebars.Compiler.prototype.mustache = function(mustache) { + if (!(mustache.params.length || mustache.hash)) { + var id = new Handlebars.AST.IdNode([{ part: '_triageMustache' }]); + + // Update the mustache node to include a hash value indicating whether the original node + // was escaped. This will allow us to properly escape values when the underlying value + // changes and we need to re-render the value. + if (!mustache.escaped) { + mustache.hash = mustache.hash || new Handlebars.AST.HashNode([]); + mustache.hash.pairs.push(["unescaped", new Handlebars.AST.StringNode("true")]); + } + mustache = new Handlebars.AST.MustacheNode([id].concat([mustache.id]), mustache.hash, !mustache.escaped); + } + + return Handlebars.Compiler.prototype.mustache.call(this, mustache); +}; + +/** + Used for precompilation of Ember Handlebars templates. This will not be used + during normal app execution. + + @method precompile + @for Ember.Handlebars + @static + @param {String} string The template to precompile + @param {Boolean} asObject optional parameter, defaulting to true, of whether or not the + compiled template should be returned as an Object or a String +*/ +EmberHandlebars.precompile = function(string, asObject) { + var ast = Handlebars.parse(string); + + var options = { + knownHelpers: { + action: true, + unbound: true, + 'bind-attr': true, + template: true, + view: true, + _triageMustache: true + }, + data: true, + stringParams: true + }; + + asObject = asObject === undefined ? true : asObject; + + var environment = new EmberHandlebars.Compiler().compile(ast, options); + return new EmberHandlebars.JavaScriptCompiler().compile(environment, options, undefined, asObject); +}; + +// We don't support this for Handlebars runtime-only +if (Handlebars.compile) { + /** + The entry point for Ember Handlebars. This replaces the default + `Handlebars.compile` and turns on template-local data and String + parameters. + + @method compile + @for Ember.Handlebars + @static + @param {String} string The template to compile + @return {Function} + */ + EmberHandlebars.compile = function(string) { + var ast = Handlebars.parse(string); + var options = { data: true, stringParams: true }; + var environment = new EmberHandlebars.Compiler().compile(ast, options); + var templateSpec = new EmberHandlebars.JavaScriptCompiler().compile(environment, options, undefined, true); + + var template = EmberHandlebars.template(templateSpec); + template.isMethod = false; //Make sure we don't wrap templates with ._super + + return template; + }; +} + + + +exports.precompile = EmberHandlebars.precompile; +exports.EmberHandlebars = EmberHandlebars; +})(); diff --git a/vendor/assets/ember/development/ember.js b/vendor/assets/ember/development/ember.js new file mode 100644 index 000000000..5ac39fa92 --- /dev/null +++ b/vendor/assets/ember/development/ember.js @@ -0,0 +1,46764 @@ +// Fetched from channel: tags/v1.6.1, with url http://builds.emberjs.com/tags/v1.6.1/ember.js +// Fetched on: 2024-01-19T09:48:13Z +/*! + * @overview Ember - JavaScript Application Framework + * @copyright Copyright 2011-2014 Tilde Inc. and contributors + * Portions Copyright 2006-2011 Strobe Inc. + * Portions Copyright 2008-2011 Apple Inc. All rights reserved. + * @license Licensed under MIT license + * See https://raw.github.com/emberjs/ember.js/master/LICENSE + * @version 1.6.1 + */ + + +(function() { +var define, requireModule, require, requirejs, Ember; + +(function() { + Ember = this.Ember = this.Ember || {}; + if (typeof Ember === 'undefined') { Ember = {} }; + + if (typeof Ember.__loader === 'undefined') { + var registry = {}, seen = {}; + + define = function(name, deps, callback) { + registry[name] = { deps: deps, callback: callback }; + }; + + requirejs = require = requireModule = function(name) { + if (seen.hasOwnProperty(name)) { return seen[name]; } + seen[name] = {}; + + if (!registry[name]) { + throw new Error("Could not find module " + name); + } + + var mod = registry[name], + deps = mod.deps, + callback = mod.callback, + reified = [], + exports; + + for (var i=0, l=deps.length; i\s*\(([^\)]+)\)/gm, '{anonymous}($1)').split('\n'); + stack.shift(); + } else { + // Firefox + stack = error.stack.replace(/(?:\n@:0)?\s+$/m, ''). + replace(/^\(/gm, '{anonymous}(').split('\n'); + } + + stackStr = "\n " + stack.slice(2).join("\n "); + message = message + stackStr; + } + + Logger.warn("DEPRECATION: "+message); + }; + + + + /** + Alias an old, deprecated method with its new counterpart. + + Display a deprecation warning with the provided message and a stack trace + (Chrome and Firefox only) when the assigned method is called. + + Ember build tools will not remove calls to `Ember.deprecateFunc()`, though + no warnings will be shown in production. + + ```javascript + Ember.oldMethod = Ember.deprecateFunc('Please use the new, updated method', Ember.newMethod); + ``` + + @method deprecateFunc + @param {String} message A description of the deprecation. + @param {Function} func The new function called to replace its deprecated counterpart. + @return {Function} a new function that wrapped the original function with a deprecation warning + */ + Ember.deprecateFunc = function(message, func) { + return function() { + Ember.deprecate(message); + return func.apply(this, arguments); + }; + }; + + + /** + Run a function meant for debugging. Ember build tools will remove any calls to + `Ember.runInDebug()` when doing a production build. + + ```javascript + Ember.runInDebug(function() { + Ember.Handlebars.EachView.reopen({ + didInsertElement: function() { + console.log('I\'m happy'); + } + }); + }); + ``` + + @method runInDebug + @param {Function} func The function to be executed. + @since 1.5.0 + */ + Ember.runInDebug = function(func) { + func() + }; + + // Inform the developer about the Ember Inspector if not installed. + if (!Ember.testing) { + var isFirefox = typeof InstallTrigger !== 'undefined'; + var isChrome = !!window.chrome && !window.opera; + + if (typeof window !== 'undefined' && (isFirefox || isChrome) && window.addEventListener) { + window.addEventListener("load", function() { + if (document.documentElement && document.documentElement.dataset && !document.documentElement.dataset.emberExtension) { + var downloadURL; + + if(isChrome) { + downloadURL = 'https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi'; + } else if(isFirefox) { + downloadURL = 'https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/'; + } + + Ember.debug('For more advanced debugging, install the Ember Inspector from ' + downloadURL); + } + }, false); + } + } + }); +})(); + +(function() { +define("ember-metal/array", + ["exports"], + function(__exports__) { + "use strict"; + /*jshint newcap:false*/ + /** + @module ember-metal + */ + + var ArrayPrototype = Array.prototype; + + // NOTE: There is a bug in jshint that doesn't recognize `Object()` without `new` + // as being ok unless both `newcap:false` and not `use strict`. + // https://github.com/jshint/jshint/issues/392 + + // Testing this is not ideal, but we want to use native functions + // if available, but not to use versions created by libraries like Prototype + var isNativeFunc = function(func) { + // This should probably work in all browsers likely to have ES5 array methods + return func && Function.prototype.toString.call(func).indexOf('[native code]') > -1; + }; + + // From: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/map + var map = isNativeFunc(ArrayPrototype.map) ? ArrayPrototype.map : function(fun /*, thisp */) { + //"use strict"; + + if (this === void 0 || this === null) { + throw new TypeError(); + } + + var t = Object(this); + var len = t.length >>> 0; + if (typeof fun !== "function") { + throw new TypeError(); + } + + var res = new Array(len); + var thisp = arguments[1]; + for (var i = 0; i < len; i++) { + if (i in t) { + res[i] = fun.call(thisp, t[i], i, t); + } + } + + return res; + }; + + // From: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/foreach + var forEach = isNativeFunc(ArrayPrototype.forEach) ? ArrayPrototype.forEach : function(fun /*, thisp */) { + //"use strict"; + + if (this === void 0 || this === null) { + throw new TypeError(); + } + + var t = Object(this); + var len = t.length >>> 0; + if (typeof fun !== "function") { + throw new TypeError(); + } + + var thisp = arguments[1]; + for (var i = 0; i < len; i++) { + if (i in t) { + fun.call(thisp, t[i], i, t); + } + } + }; + + var indexOf = isNativeFunc(ArrayPrototype.indexOf) ? ArrayPrototype.indexOf : function (obj, fromIndex) { + if (fromIndex === null || fromIndex === undefined) { fromIndex = 0; } + else if (fromIndex < 0) { fromIndex = Math.max(0, this.length + fromIndex); } + for (var i = fromIndex, j = this.length; i < j; i++) { + if (this[i] === obj) { return i; } + } + return -1; + }; + + var filter = isNativeFunc(ArrayPrototype.filter) ? ArrayPrototype.filter : function (fn, context) { + var i, + value, + result = [], + length = this.length; + + for (i = 0; i < length; i++) { + if (this.hasOwnProperty(i)) { + value = this[i]; + if (fn.call(context, value, i, this)) { + result.push(value); + } + } + } + return result; + }; + + + if (Ember.SHIM_ES5) { + if (!ArrayPrototype.map) { + ArrayPrototype.map = map; + } + + if (!ArrayPrototype.forEach) { + ArrayPrototype.forEach = forEach; + } + + if (!ArrayPrototype.filter) { + ArrayPrototype.filter = filter; + } + + if (!ArrayPrototype.indexOf) { + ArrayPrototype.indexOf = indexOf; + } + } + + /** + Array polyfills to support ES5 features in older browsers. + + @namespace Ember + @property ArrayPolyfills + */ + __exports__.map = map; + __exports__.forEach = forEach; + __exports__.filter = filter; + __exports__.indexOf = indexOf; + }); +define("ember-metal/binding", + ["ember-metal/core","ember-metal/property_get","ember-metal/property_set","ember-metal/utils","ember-metal/map","ember-metal/observer","ember-metal/run_loop","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __exports__) { + "use strict"; + var Ember = __dependency1__["default"]; + // Ember.Logger, Ember.LOG_BINDINGS, assert + var get = __dependency2__.get; + var set = __dependency3__.set; + var trySet = __dependency3__.trySet; + var guidFor = __dependency4__.guidFor; + var Map = __dependency5__.Map; + var addObserver = __dependency6__.addObserver; + var removeObserver = __dependency6__.removeObserver; + var _suspendObserver = __dependency6__._suspendObserver; + var run = __dependency7__["default"]; + + // ES6TODO: where is Ember.lookup defined? + /** + @module ember-metal + */ + + // .......................................................... + // CONSTANTS + // + + /** + Debug parameter you can turn on. This will log all bindings that fire to + the console. This should be disabled in production code. Note that you + can also enable this from the console or temporarily. + + @property LOG_BINDINGS + @for Ember + @type Boolean + @default false + */ + Ember.LOG_BINDINGS = false || !!Ember.ENV.LOG_BINDINGS; + + var IS_GLOBAL = /^([A-Z$]|([0-9][A-Z$]))/; + + /** + Returns true if the provided path is global (e.g., `MyApp.fooController.bar`) + instead of local (`foo.bar.baz`). + + @method isGlobalPath + @for Ember + @private + @param {String} path + @return Boolean + */ + function isGlobalPath(path) { + return IS_GLOBAL.test(path); + }; + + function getWithGlobals(obj, path) { + return get(isGlobalPath(path) ? Ember.lookup : obj, path); + } + + // .......................................................... + // BINDING + // + + var Binding = function(toPath, fromPath) { + this._direction = 'fwd'; + this._from = fromPath; + this._to = toPath; + this._directionMap = Map.create(); + }; + + /** + @class Binding + @namespace Ember + */ + + Binding.prototype = { + /** + This copies the Binding so it can be connected to another object. + + @method copy + @return {Ember.Binding} `this` + */ + copy: function () { + var copy = new Binding(this._to, this._from); + if (this._oneWay) { copy._oneWay = true; } + return copy; + }, + + // .......................................................... + // CONFIG + // + + /** + This will set `from` property path to the specified value. It will not + attempt to resolve this property path to an actual object until you + connect the binding. + + The binding will search for the property path starting at the root object + you pass when you `connect()` the binding. It follows the same rules as + `get()` - see that method for more information. + + @method from + @param {String} path the property path to connect to + @return {Ember.Binding} `this` + */ + from: function(path) { + this._from = path; + return this; + }, + + /** + This will set the `to` property path to the specified value. It will not + attempt to resolve this property path to an actual object until you + connect the binding. + + The binding will search for the property path starting at the root object + you pass when you `connect()` the binding. It follows the same rules as + `get()` - see that method for more information. + + @method to + @param {String|Tuple} path A property path or tuple + @return {Ember.Binding} `this` + */ + to: function(path) { + this._to = path; + return this; + }, + + /** + Configures the binding as one way. A one-way binding will relay changes + on the `from` side to the `to` side, but not the other way around. This + means that if you change the `to` side directly, the `from` side may have + a different value. + + @method oneWay + @return {Ember.Binding} `this` + */ + oneWay: function() { + this._oneWay = true; + return this; + }, + + /** + @method toString + @return {String} string representation of binding + */ + toString: function() { + var oneWay = this._oneWay ? '[oneWay]' : ''; + return "Ember.Binding<" + guidFor(this) + ">(" + this._from + " -> " + this._to + ")" + oneWay; + }, + + // .......................................................... + // CONNECT AND SYNC + // + + /** + Attempts to connect this binding instance so that it can receive and relay + changes. This method will raise an exception if you have not set the + from/to properties yet. + + @method connect + @param {Object} obj The root object for this binding. + @return {Ember.Binding} `this` + */ + connect: function(obj) { + Ember.assert('Must pass a valid object to Ember.Binding.connect()', !!obj); + + var fromPath = this._from, toPath = this._to; + trySet(obj, toPath, getWithGlobals(obj, fromPath)); + + // add an observer on the object to be notified when the binding should be updated + addObserver(obj, fromPath, this, this.fromDidChange); + + // if the binding is a two-way binding, also set up an observer on the target + if (!this._oneWay) { addObserver(obj, toPath, this, this.toDidChange); } + + this._readyToSync = true; + + return this; + }, + + /** + Disconnects the binding instance. Changes will no longer be relayed. You + will not usually need to call this method. + + @method disconnect + @param {Object} obj The root object you passed when connecting the binding. + @return {Ember.Binding} `this` + */ + disconnect: function(obj) { + Ember.assert('Must pass a valid object to Ember.Binding.disconnect()', !!obj); + + var twoWay = !this._oneWay; + + // remove an observer on the object so we're no longer notified of + // changes that should update bindings. + removeObserver(obj, this._from, this, this.fromDidChange); + + // if the binding is two-way, remove the observer from the target as well + if (twoWay) { removeObserver(obj, this._to, this, this.toDidChange); } + + this._readyToSync = false; // disable scheduled syncs... + return this; + }, + + // .......................................................... + // PRIVATE + // + + /* called when the from side changes */ + fromDidChange: function(target) { + this._scheduleSync(target, 'fwd'); + }, + + /* called when the to side changes */ + toDidChange: function(target) { + this._scheduleSync(target, 'back'); + }, + + _scheduleSync: function(obj, dir) { + var directionMap = this._directionMap; + var existingDir = directionMap.get(obj); + + // if we haven't scheduled the binding yet, schedule it + if (!existingDir) { + run.schedule('sync', this, this._sync, obj); + directionMap.set(obj, dir); + } + + // If both a 'back' and 'fwd' sync have been scheduled on the same object, + // default to a 'fwd' sync so that it remains deterministic. + if (existingDir === 'back' && dir === 'fwd') { + directionMap.set(obj, 'fwd'); + } + }, + + _sync: function(obj) { + var log = Ember.LOG_BINDINGS; + + // don't synchronize destroyed objects or disconnected bindings + if (obj.isDestroyed || !this._readyToSync) { return; } + + // get the direction of the binding for the object we are + // synchronizing from + var directionMap = this._directionMap; + var direction = directionMap.get(obj); + + var fromPath = this._from, toPath = this._to; + + directionMap.remove(obj); + + // if we're synchronizing from the remote object... + if (direction === 'fwd') { + var fromValue = getWithGlobals(obj, this._from); + if (log) { + Ember.Logger.log(' ', this.toString(), '->', fromValue, obj); + } + if (this._oneWay) { + trySet(obj, toPath, fromValue); + } else { + _suspendObserver(obj, toPath, this, this.toDidChange, function () { + trySet(obj, toPath, fromValue); + }); + } + // if we're synchronizing *to* the remote object + } else if (direction === 'back') { + var toValue = get(obj, this._to); + if (log) { + Ember.Logger.log(' ', this.toString(), '<-', toValue, obj); + } + _suspendObserver(obj, fromPath, this, this.fromDidChange, function () { + trySet(isGlobalPath(fromPath) ? Ember.lookup : obj, fromPath, toValue); + }); + } + } + + }; + + function mixinProperties(to, from) { + for (var key in from) { + if (from.hasOwnProperty(key)) { + to[key] = from[key]; + } + } + } + + mixinProperties(Binding, { + + /* + See `Ember.Binding.from`. + + @method from + @static + */ + from: function() { + var C = this, binding = new C(); + return binding.from.apply(binding, arguments); + }, + + /* + See `Ember.Binding.to`. + + @method to + @static + */ + to: function() { + var C = this, binding = new C(); + return binding.to.apply(binding, arguments); + }, + + /** + Creates a new Binding instance and makes it apply in a single direction. + A one-way binding will relay changes on the `from` side object (supplied + as the `from` argument) the `to` side, but not the other way around. + This means that if you change the "to" side directly, the "from" side may have + a different value. + + See `Binding.oneWay`. + + @method oneWay + @param {String} from from path. + @param {Boolean} [flag] (Optional) passing nothing here will make the + binding `oneWay`. You can instead pass `false` to disable `oneWay`, making the + binding two way again. + @return {Ember.Binding} `this` + */ + oneWay: function(from, flag) { + var C = this, binding = new C(null, from); + return binding.oneWay(flag); + } + + }); + + /** + An `Ember.Binding` connects the properties of two objects so that whenever + the value of one property changes, the other property will be changed also. + + ## Automatic Creation of Bindings with `/^*Binding/`-named Properties + + You do not usually create Binding objects directly but instead describe + bindings in your class or object definition using automatic binding + detection. + + Properties ending in a `Binding` suffix will be converted to `Ember.Binding` + instances. The value of this property should be a string representing a path + to another object or a custom binding instanced created using Binding helpers + (see "One Way Bindings"): + + ``` + valueBinding: "MyApp.someController.title" + ``` + + This will create a binding from `MyApp.someController.title` to the `value` + property of your object instance automatically. Now the two values will be + kept in sync. + + ## One Way Bindings + + One especially useful binding customization you can use is the `oneWay()` + helper. This helper tells Ember that you are only interested in + receiving changes on the object you are binding from. For example, if you + are binding to a preference and you want to be notified if the preference + has changed, but your object will not be changing the preference itself, you + could do: + + ``` + bigTitlesBinding: Ember.Binding.oneWay("MyApp.preferencesController.bigTitles") + ``` + + This way if the value of `MyApp.preferencesController.bigTitles` changes the + `bigTitles` property of your object will change also. However, if you + change the value of your `bigTitles` property, it will not update the + `preferencesController`. + + One way bindings are almost twice as fast to setup and twice as fast to + execute because the binding only has to worry about changes to one side. + + You should consider using one way bindings anytime you have an object that + may be created frequently and you do not intend to change a property; only + to monitor it for changes (such as in the example above). + + ## Adding Bindings Manually + + All of the examples above show you how to configure a custom binding, but the + result of these customizations will be a binding template, not a fully active + Binding instance. The binding will actually become active only when you + instantiate the object the binding belongs to. It is useful however, to + understand what actually happens when the binding is activated. + + For a binding to function it must have at least a `from` property and a `to` + property. The `from` property path points to the object/key that you want to + bind from while the `to` path points to the object/key you want to bind to. + + When you define a custom binding, you are usually describing the property + you want to bind from (such as `MyApp.someController.value` in the examples + above). When your object is created, it will automatically assign the value + you want to bind `to` based on the name of your binding key. In the + examples above, during init, Ember objects will effectively call + something like this on your binding: + + ```javascript + binding = Ember.Binding.from(this.valueBinding).to("value"); + ``` + + This creates a new binding instance based on the template you provide, and + sets the to path to the `value` property of the new object. Now that the + binding is fully configured with a `from` and a `to`, it simply needs to be + connected to become active. This is done through the `connect()` method: + + ```javascript + binding.connect(this); + ``` + + Note that when you connect a binding you pass the object you want it to be + connected to. This object will be used as the root for both the from and + to side of the binding when inspecting relative paths. This allows the + binding to be automatically inherited by subclassed objects as well. + + Now that the binding is connected, it will observe both the from and to side + and relay changes. + + If you ever needed to do so (you almost never will, but it is useful to + understand this anyway), you could manually create an active binding by + using the `Ember.bind()` helper method. (This is the same method used by + to setup your bindings on objects): + + ```javascript + Ember.bind(MyApp.anotherObject, "value", "MyApp.someController.value"); + ``` + + Both of these code fragments have the same effect as doing the most friendly + form of binding creation like so: + + ```javascript + MyApp.anotherObject = Ember.Object.create({ + valueBinding: "MyApp.someController.value", + + // OTHER CODE FOR THIS OBJECT... + }); + ``` + + Ember's built in binding creation method makes it easy to automatically + create bindings for you. You should always use the highest-level APIs + available, even if you understand how it works underneath. + + @class Binding + @namespace Ember + @since Ember 0.9 + */ + // Ember.Binding = Binding; ES6TODO: where to put this? + + + /** + Global helper method to create a new binding. Just pass the root object + along with a `to` and `from` path to create and connect the binding. + + @method bind + @for Ember + @param {Object} obj The root object of the transform. + @param {String} to The path to the 'to' side of the binding. + Must be relative to obj. + @param {String} from The path to the 'from' side of the binding. + Must be relative to obj or a global path. + @return {Ember.Binding} binding instance + */ + function bind(obj, to, from) { + return new Binding(to, from).connect(obj); + }; + + /** + @method oneWay + @for Ember + @param {Object} obj The root object of the transform. + @param {String} to The path to the 'to' side of the binding. + Must be relative to obj. + @param {String} from The path to the 'from' side of the binding. + Must be relative to obj or a global path. + @return {Ember.Binding} binding instance + */ + function oneWay(obj, to, from) { + return new Binding(to, from).oneWay().connect(obj); + }; + + __exports__.Binding = Binding; + __exports__.bind = bind; + __exports__.oneWay = oneWay; + __exports__.isGlobalPath = isGlobalPath; + }); +define("ember-metal/chains", + ["ember-metal/core","ember-metal/property_get","ember-metal/utils","ember-metal/array","ember-metal/watch_key","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __exports__) { + "use strict"; + var Ember = __dependency1__["default"]; + // warn, assert, etc; + var get = __dependency2__.get; + var normalizeTuple = __dependency2__.normalizeTuple; + var meta = __dependency3__.meta; + var META_KEY = __dependency3__.META_KEY; + var forEach = __dependency4__.forEach; + var watchKey = __dependency5__.watchKey; + var unwatchKey = __dependency5__.unwatchKey; + + var metaFor = meta, + warn = Ember.warn, + FIRST_KEY = /^([^\.]+)/; + + function firstKey(path) { + return path.match(FIRST_KEY)[0]; + } + + var pendingQueue = []; + + // attempts to add the pendingQueue chains again. If some of them end up + // back in the queue and reschedule is true, schedules a timeout to try + // again. + function flushPendingChains() { + if (pendingQueue.length === 0) { return; } // nothing to do + + var queue = pendingQueue; + pendingQueue = []; + + forEach.call(queue, function(q) { q[0].add(q[1]); }); + + warn('Watching an undefined global, Ember expects watched globals to be setup by the time the run loop is flushed, check for typos', pendingQueue.length === 0); + }; + + + function addChainWatcher(obj, keyName, node) { + if (!obj || ('object' !== typeof obj)) { return; } // nothing to do + + var m = metaFor(obj), nodes = m.chainWatchers; + + if (!m.hasOwnProperty('chainWatchers')) { + nodes = m.chainWatchers = {}; + } + + if (!nodes[keyName]) { nodes[keyName] = []; } + nodes[keyName].push(node); + watchKey(obj, keyName, m); + } + + function removeChainWatcher(obj, keyName, node) { + if (!obj || 'object' !== typeof obj) { return; } // nothing to do + + var m = obj[META_KEY]; + if (m && !m.hasOwnProperty('chainWatchers')) { return; } // nothing to do + + var nodes = m && m.chainWatchers; + + if (nodes && nodes[keyName]) { + nodes = nodes[keyName]; + for (var i = 0, l = nodes.length; i < l; i++) { + if (nodes[i] === node) { nodes.splice(i, 1); } + } + } + unwatchKey(obj, keyName, m); + }; + + // A ChainNode watches a single key on an object. If you provide a starting + // value for the key then the node won't actually watch it. For a root node + // pass null for parent and key and object for value. + function ChainNode(parent, key, value) { + this._parent = parent; + this._key = key; + + // _watching is true when calling get(this._parent, this._key) will + // return the value of this node. + // + // It is false for the root of a chain (because we have no parent) + // and for global paths (because the parent node is the object with + // the observer on it) + this._watching = value===undefined; + + this._value = value; + this._paths = {}; + if (this._watching) { + this._object = parent.value(); + if (this._object) { addChainWatcher(this._object, this._key, this); } + } + + // Special-case: the EachProxy relies on immediate evaluation to + // establish its observers. + // + // TODO: Replace this with an efficient callback that the EachProxy + // can implement. + if (this._parent && this._parent._key === '@each') { + this.value(); + } + }; + + var ChainNodePrototype = ChainNode.prototype; + + function lazyGet(obj, key) { + if (!obj) return undefined; + + var meta = obj[META_KEY]; + // check if object meant only to be a prototype + if (meta && meta.proto === obj) return undefined; + + if (key === "@each") return get(obj, key); + + // if a CP only return cached value + var desc = meta && meta.descs[key]; + if (desc && desc._cacheable) { + if (key in meta.cache) { + return meta.cache[key]; + } else { + return undefined; + } + } + + return get(obj, key); + } + + ChainNodePrototype.value = function() { + if (this._value === undefined && this._watching) { + var obj = this._parent.value(); + this._value = lazyGet(obj, this._key); + } + return this._value; + }; + + ChainNodePrototype.destroy = function() { + if (this._watching) { + var obj = this._object; + if (obj) { removeChainWatcher(obj, this._key, this); } + this._watching = false; // so future calls do nothing + } + }; + + // copies a top level object only + ChainNodePrototype.copy = function(obj) { + var ret = new ChainNode(null, null, obj), + paths = this._paths, path; + for (path in paths) { + if (paths[path] <= 0) { continue; } // this check will also catch non-number vals. + ret.add(path); + } + return ret; + }; + + // called on the root node of a chain to setup watchers on the specified + // path. + ChainNodePrototype.add = function(path) { + var obj, tuple, key, src, paths; + + paths = this._paths; + paths[path] = (paths[path] || 0) + 1; + + obj = this.value(); + tuple = normalizeTuple(obj, path); + + // the path was a local path + if (tuple[0] && tuple[0] === obj) { + path = tuple[1]; + key = firstKey(path); + path = path.slice(key.length+1); + + // global path, but object does not exist yet. + // put into a queue and try to connect later. + } else if (!tuple[0]) { + pendingQueue.push([this, path]); + tuple.length = 0; + return; + + // global path, and object already exists + } else { + src = tuple[0]; + key = path.slice(0, 0-(tuple[1].length+1)); + path = tuple[1]; + } + + tuple.length = 0; + this.chain(key, path, src); + }; + + // called on the root node of a chain to teardown watcher on the specified + // path + ChainNodePrototype.remove = function(path) { + var obj, tuple, key, src, paths; + + paths = this._paths; + if (paths[path] > 0) { paths[path]--; } + + obj = this.value(); + tuple = normalizeTuple(obj, path); + if (tuple[0] === obj) { + path = tuple[1]; + key = firstKey(path); + path = path.slice(key.length+1); + } else { + src = tuple[0]; + key = path.slice(0, 0-(tuple[1].length+1)); + path = tuple[1]; + } + + tuple.length = 0; + this.unchain(key, path); + }; + + ChainNodePrototype.count = 0; + + ChainNodePrototype.chain = function(key, path, src) { + var chains = this._chains, node; + if (!chains) { chains = this._chains = {}; } + + node = chains[key]; + if (!node) { node = chains[key] = new ChainNode(this, key, src); } + node.count++; // count chains... + + // chain rest of path if there is one + if (path && path.length>0) { + key = firstKey(path); + path = path.slice(key.length+1); + node.chain(key, path); // NOTE: no src means it will observe changes... + } + }; + + ChainNodePrototype.unchain = function(key, path) { + var chains = this._chains, node = chains[key]; + + // unchain rest of path first... + if (path && path.length>1) { + key = firstKey(path); + path = path.slice(key.length+1); + node.unchain(key, path); + } + + // delete node if needed. + node.count--; + if (node.count<=0) { + delete chains[node._key]; + node.destroy(); + } + + }; + + ChainNodePrototype.willChange = function(events) { + var chains = this._chains; + if (chains) { + for(var key in chains) { + if (!chains.hasOwnProperty(key)) { continue; } + chains[key].willChange(events); + } + } + + if (this._parent) { this._parent.chainWillChange(this, this._key, 1, events); } + }; + + ChainNodePrototype.chainWillChange = function(chain, path, depth, events) { + if (this._key) { path = this._key + '.' + path; } + + if (this._parent) { + this._parent.chainWillChange(this, path, depth+1, events); + } else { + if (depth > 1) { + events.push(this.value(), path); + } + path = 'this.' + path; + if (this._paths[path] > 0) { + events.push(this.value(), path); + } + } + }; + + ChainNodePrototype.chainDidChange = function(chain, path, depth, events) { + if (this._key) { path = this._key + '.' + path; } + if (this._parent) { + this._parent.chainDidChange(this, path, depth+1, events); + } else { + if (depth > 1) { + events.push(this.value(), path); + } + path = 'this.' + path; + if (this._paths[path] > 0) { + events.push(this.value(), path); + } + } + }; + + ChainNodePrototype.didChange = function(events) { + // invalidate my own value first. + if (this._watching) { + var obj = this._parent.value(); + if (obj !== this._object) { + removeChainWatcher(this._object, this._key, this); + this._object = obj; + addChainWatcher(obj, this._key, this); + } + this._value = undefined; + + // Special-case: the EachProxy relies on immediate evaluation to + // establish its observers. + if (this._parent && this._parent._key === '@each') + this.value(); + } + + // then notify chains... + var chains = this._chains; + if (chains) { + for(var key in chains) { + if (!chains.hasOwnProperty(key)) { continue; } + chains[key].didChange(events); + } + } + + // if no events are passed in then we only care about the above wiring update + if (events === null) { return; } + + // and finally tell parent about my path changing... + if (this._parent) { this._parent.chainDidChange(this, this._key, 1, events); } + }; + + function finishChains(obj) { + // We only create meta if we really have to + var m = obj[META_KEY], chains = m && m.chains; + if (chains) { + if (chains.value() !== obj) { + metaFor(obj).chains = chains = chains.copy(obj); + } else { + chains.didChange(null); + } + } + }; + + __exports__.flushPendingChains = flushPendingChains; + __exports__.removeChainWatcher = removeChainWatcher; + __exports__.ChainNode = ChainNode; + __exports__.finishChains = finishChains; + }); +define("ember-metal/computed", + ["ember-metal/core","ember-metal/property_get","ember-metal/property_set","ember-metal/utils","ember-metal/enumerable_utils","ember-metal/platform","ember-metal/watching","ember-metal/expand_properties","ember-metal/error","ember-metal/properties","ember-metal/property_events","ember-metal/is_empty","ember-metal/is_none","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __dependency11__, __dependency12__, __dependency13__, __exports__) { + "use strict"; + var Ember = __dependency1__["default"]; + var get = __dependency2__.get; + var set = __dependency3__.set; + var meta = __dependency4__.meta; + var META_KEY = __dependency4__.META_KEY; + var guidFor = __dependency4__.guidFor; + var typeOf = __dependency4__.typeOf; + var inspect = __dependency4__.inspect; + var EnumerableUtils = __dependency5__["default"]; + var create = __dependency6__.create; + var watch = __dependency7__.watch; + var unwatch = __dependency7__.unwatch; + var expandProperties = __dependency8__["default"]; + var EmberError = __dependency9__["default"]; + var Descriptor = __dependency10__.Descriptor; + var defineProperty = __dependency10__.defineProperty; + var propertyWillChange = __dependency11__.propertyWillChange; + var propertyDidChange = __dependency11__.propertyDidChange; + var isEmpty = __dependency12__["default"]; + var isNone = __dependency13__.isNone; + + /** + @module ember-metal + */ + + Ember.warn("The CP_DEFAULT_CACHEABLE flag has been removed and computed properties are always cached by default. Use `volatile` if you don't want caching.", Ember.ENV.CP_DEFAULT_CACHEABLE !== false); + + + var metaFor = meta, + a_slice = [].slice, + o_create = create; + + function UNDEFINED() { } + + var lengthPattern = /\.(length|\[\])$/; + + // .......................................................... + // DEPENDENT KEYS + // + + // data structure: + // meta.deps = { + // 'depKey': { + // 'keyName': count, + // } + // } + + /* + This function returns a map of unique dependencies for a + given object and key. + */ + function keysForDep(depsMeta, depKey) { + var keys = depsMeta[depKey]; + if (!keys) { + // if there are no dependencies yet for a the given key + // create a new empty list of dependencies for the key + keys = depsMeta[depKey] = {}; + } else if (!depsMeta.hasOwnProperty(depKey)) { + // otherwise if the dependency list is inherited from + // a superclass, clone the hash + keys = depsMeta[depKey] = o_create(keys); + } + return keys; + } + + function metaForDeps(meta) { + return keysForDep(meta, 'deps'); + } + + function addDependentKeys(desc, obj, keyName, meta) { + // the descriptor has a list of dependent keys, so + // add all of its dependent keys. + var depKeys = desc._dependentKeys, depsMeta, idx, len, depKey, keys; + if (!depKeys) return; + + depsMeta = metaForDeps(meta); + + for(idx = 0, len = depKeys.length; idx < len; idx++) { + depKey = depKeys[idx]; + // Lookup keys meta for depKey + keys = keysForDep(depsMeta, depKey); + // Increment the number of times depKey depends on keyName. + keys[keyName] = (keys[keyName] || 0) + 1; + // Watch the depKey + watch(obj, depKey, meta); + } + } + + function removeDependentKeys(desc, obj, keyName, meta) { + // the descriptor has a list of dependent keys, so + // add all of its dependent keys. + var depKeys = desc._dependentKeys, depsMeta, idx, len, depKey, keys; + if (!depKeys) return; + + depsMeta = metaForDeps(meta); + + for(idx = 0, len = depKeys.length; idx < len; idx++) { + depKey = depKeys[idx]; + // Lookup keys meta for depKey + keys = keysForDep(depsMeta, depKey); + // Increment the number of times depKey depends on keyName. + keys[keyName] = (keys[keyName] || 0) - 1; + // Watch the depKey + unwatch(obj, depKey, meta); + } + } + + // .......................................................... + // COMPUTED PROPERTY + // + + /** + A computed property transforms an objects function into a property. + + By default the function backing the computed property will only be called + once and the result will be cached. You can specify various properties + that your computed property is dependent on. This will force the cached + result to be recomputed if the dependencies are modified. + + In the following example we declare a computed property (by calling + `.property()` on the fullName function) and setup the properties + dependencies (depending on firstName and lastName). The fullName function + will be called once (regardless of how many times it is accessed) as long + as it's dependencies have not been changed. Once firstName or lastName are updated + any future calls (or anything bound) to fullName will incorporate the new + values. + + ```javascript + var Person = Ember.Object.extend({ + // these will be supplied by `create` + firstName: null, + lastName: null, + + fullName: function() { + var firstName = this.get('firstName'); + var lastName = this.get('lastName'); + + return firstName + ' ' + lastName; + }.property('firstName', 'lastName') + }); + + var tom = Person.create({ + firstName: 'Tom', + lastName: 'Dale' + }); + + tom.get('fullName') // 'Tom Dale' + ``` + + You can also define what Ember should do when setting a computed property. + If you try to set a computed property, it will be invoked with the key and + value you want to set it to. You can also accept the previous value as the + third parameter. + + ```javascript + var Person = Ember.Object.extend({ + // these will be supplied by `create` + firstName: null, + lastName: null, + + fullName: function(key, value, oldValue) { + // getter + if (arguments.length === 1) { + var firstName = this.get('firstName'); + var lastName = this.get('lastName'); + + return firstName + ' ' + lastName; + + // setter + } else { + var name = value.split(' '); + + this.set('firstName', name[0]); + this.set('lastName', name[1]); + + return value; + } + }.property('firstName', 'lastName') + }); + + var person = Person.create(); + + person.set('fullName', 'Peter Wagenet'); + person.get('firstName'); // 'Peter' + person.get('lastName'); // 'Wagenet' + ``` + + @class ComputedProperty + @namespace Ember + @extends Ember.Descriptor + @constructor + */ + function ComputedProperty(func, opts) { + func.__ember_arity__ = func.length; + this.func = func; + + this._cacheable = (opts && opts.cacheable !== undefined) ? opts.cacheable : true; + this._dependentKeys = opts && opts.dependentKeys; + this._readOnly = opts && (opts.readOnly !== undefined || !!opts.readOnly) || false; + } + + ComputedProperty.prototype = new Descriptor(); + + var ComputedPropertyPrototype = ComputedProperty.prototype; + ComputedPropertyPrototype._dependentKeys = undefined; + ComputedPropertyPrototype._suspended = undefined; + ComputedPropertyPrototype._meta = undefined; + + /** + Properties are cacheable by default. Computed property will automatically + cache the return value of your function until one of the dependent keys changes. + + Call `volatile()` to set it into non-cached mode. When in this mode + the computed property will not automatically cache the return value. + + However, if a property is properly observable, there is no reason to disable + caching. + + @method cacheable + @param {Boolean} aFlag optional set to `false` to disable caching + @return {Ember.ComputedProperty} this + @chainable + */ + ComputedPropertyPrototype.cacheable = function(aFlag) { + this._cacheable = aFlag !== false; + return this; + }; + + /** + Call on a computed property to set it into non-cached mode. When in this + mode the computed property will not automatically cache the return value. + + ```javascript + var outsideService = Ember.Object.extend({ + value: function() { + return OutsideService.getValue(); + }.property().volatile() + }).create(); + ``` + + @method volatile + @return {Ember.ComputedProperty} this + @chainable + */ + ComputedPropertyPrototype.volatile = function() { + return this.cacheable(false); + }; + + /** + Call on a computed property to set it into read-only mode. When in this + mode the computed property will throw an error when set. + + ```javascript + var Person = Ember.Object.extend({ + guid: function() { + return 'guid-guid-guid'; + }.property().readOnly() + }); + + var person = Person.create(); + + person.set('guid', 'new-guid'); // will throw an exception + ``` + + @method readOnly + @return {Ember.ComputedProperty} this + @chainable + */ + ComputedPropertyPrototype.readOnly = function(readOnly) { + this._readOnly = readOnly === undefined || !!readOnly; + return this; + }; + + /** + Sets the dependent keys on this computed property. Pass any number of + arguments containing key paths that this computed property depends on. + + ```javascript + var President = Ember.Object.extend({ + fullName: computed(function() { + return this.get('firstName') + ' ' + this.get('lastName'); + + // Tell Ember that this computed property depends on firstName + // and lastName + }).property('firstName', 'lastName') + }); + + var president = President.create({ + firstName: 'Barack', + lastName: 'Obama', + }); + + president.get('fullName'); // 'Barack Obama' + ``` + + @method property + @param {String} path* zero or more property paths + @return {Ember.ComputedProperty} this + @chainable + */ + ComputedPropertyPrototype.property = function() { + var args; + + var addArg = function (property) { + args.push(property); + }; + + args = []; + for (var i = 0, l = arguments.length; i < l; i++) { + expandProperties(arguments[i], addArg); + } + + this._dependentKeys = args; + return this; + }; + + /** + In some cases, you may want to annotate computed properties with additional + metadata about how they function or what values they operate on. For example, + computed property functions may close over variables that are then no longer + available for introspection. + + You can pass a hash of these values to a computed property like this: + + ``` + person: function() { + var personId = this.get('personId'); + return App.Person.create({ id: personId }); + }.property().meta({ type: App.Person }) + ``` + + The hash that you pass to the `meta()` function will be saved on the + computed property descriptor under the `_meta` key. Ember runtime + exposes a public API for retrieving these values from classes, + via the `metaForProperty()` function. + + @method meta + @param {Hash} meta + @chainable + */ + + ComputedPropertyPrototype.meta = function(meta) { + if (arguments.length === 0) { + return this._meta || {}; + } else { + this._meta = meta; + return this; + } + }; + + /* impl descriptor API */ + ComputedPropertyPrototype.didChange = function(obj, keyName) { + // _suspended is set via a CP.set to ensure we don't clear + // the cached value set by the setter + if (this._cacheable && this._suspended !== obj) { + var meta = metaFor(obj); + if (meta.cache[keyName] !== undefined) { + meta.cache[keyName] = undefined; + removeDependentKeys(this, obj, keyName, meta); + } + } + }; + + function finishChains(chainNodes) + { + for (var i=0, l=chainNodes.length; i 1) { + args = a_slice.call(arguments, 0, -1); + func = a_slice.call(arguments, -1)[0]; + } + + if (typeof func !== "function") { + throw new EmberError("Computed Property declared without a property function"); + } + + var cp = new ComputedProperty(func); + + if (args) { + cp.property.apply(cp, args); + } + + return cp; + }; + + /** + Returns the cached value for a property, if one exists. + This can be useful for peeking at the value of a computed + property that is generated lazily, without accidentally causing + it to be created. + + @method cacheFor + @for Ember + @param {Object} obj the object whose property you want to check + @param {String} key the name of the property whose cached value you want + to return + @return {Object} the cached value + */ + function cacheFor(obj, key) { + var meta = obj[META_KEY], + cache = meta && meta.cache, + ret = cache && cache[key]; + + if (ret === UNDEFINED) { return undefined; } + return ret; + }; + + cacheFor.set = function(cache, key, value) { + if (value === undefined) { + cache[key] = UNDEFINED; + } else { + cache[key] = value; + } + }; + + cacheFor.get = function(cache, key) { + var ret = cache[key]; + if (ret === UNDEFINED) { return undefined; } + return ret; + }; + + cacheFor.remove = function(cache, key) { + cache[key] = undefined; + }; + + function getProperties(self, propertyNames) { + var ret = {}; + for(var i = 0; i < propertyNames.length; i++) { + ret[propertyNames[i]] = get(self, propertyNames[i]); + } + return ret; + } + + function registerComputed(name, macro) { + computed[name] = function(dependentKey) { + var args = a_slice.call(arguments); + return computed(dependentKey, function() { + return macro.apply(this, args); + }); + }; + }; + + function registerComputedWithProperties(name, macro) { + computed[name] = function() { + var properties = a_slice.call(arguments); + + var computedFunc = computed(function() { + return macro.apply(this, [getProperties(this, properties)]); + }); + + return computedFunc.property.apply(computedFunc, properties); + }; + }; + + /** + A computed property that returns true if the value of the dependent + property is null, an empty string, empty array, or empty function. + + Example + + ```javascript + var ToDoList = Ember.Object.extend({ + done: Ember.computed.empty('todos') + }); + + var todoList = ToDoList.create({ + todos: ['Unit Test', 'Documentation', 'Release'] + }); + + todoList.get('done'); // false + todoList.get('todos').clear(); + todoList.get('done'); // true + ``` + + @since 1.6.0 + @method computed.empty + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which negate + the original value for property + */ + computed.empty = function (dependentKey) { + return computed(dependentKey + '.length', function () { + return isEmpty(get(this, dependentKey)); + }); + }; + + /** + A computed property that returns true if the value of the dependent + property is NOT null, an empty string, empty array, or empty function. + + Note: When using `computed.notEmpty` to watch an array make sure to + use the `array.[]` syntax so the computed can subscribe to transitions + from empty to non-empty states. + + Example + + ```javascript + var Hamster = Ember.Object.extend({ + hasStuff: Ember.computed.notEmpty('backpack.[]') + }); + + var hamster = Hamster.create({ backpack: ['Food', 'Sleeping Bag', 'Tent'] }); + + hamster.get('hasStuff'); // true + hamster.get('backpack').clear(); // [] + hamster.get('hasStuff'); // false + ``` + + @method computed.notEmpty + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which returns true if + original value for property is not empty. + */ + registerComputed('notEmpty', function(dependentKey) { + return !isEmpty(get(this, dependentKey)); + }); + + /** + A computed property that returns true if the value of the dependent + property is null or undefined. This avoids errors from JSLint complaining + about use of ==, which can be technically confusing. + + Example + + ```javascript + var Hamster = Ember.Object.extend({ + isHungry: Ember.computed.none('food') + }); + + var hamster = Hamster.create(); + + hamster.get('isHungry'); // true + hamster.set('food', 'Banana'); + hamster.get('isHungry'); // false + hamster.set('food', null); + hamster.get('isHungry'); // true + ``` + + @method computed.none + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which + returns true if original value for property is null or undefined. + */ + registerComputed('none', function(dependentKey) { + return isNone(get(this, dependentKey)); + }); + + /** + A computed property that returns the inverse boolean value + of the original value for the dependent property. + + Example + + ```javascript + var User = Ember.Object.extend({ + isAnonymous: Ember.computed.not('loggedIn') + }); + + var user = User.create({loggedIn: false}); + + user.get('isAnonymous'); // true + user.set('loggedIn', true); + user.get('isAnonymous'); // false + ``` + + @method computed.not + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which returns + inverse of the original value for property + */ + registerComputed('not', function(dependentKey) { + return !get(this, dependentKey); + }); + + /** + A computed property that converts the provided dependent property + into a boolean value. + + ```javascript + var Hamster = Ember.Object.extend({ + hasBananas: Ember.computed.bool('numBananas') + }); + + var hamster = Hamster.create(); + + hamster.get('hasBananas'); // false + hamster.set('numBananas', 0); + hamster.get('hasBananas'); // false + hamster.set('numBananas', 1); + hamster.get('hasBananas'); // true + hamster.set('numBananas', null); + hamster.get('hasBananas'); // false + ``` + + @method computed.bool + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which converts + to boolean the original value for property + */ + registerComputed('bool', function(dependentKey) { + return !!get(this, dependentKey); + }); + + /** + A computed property which matches the original value for the + dependent property against a given RegExp, returning `true` + if they values matches the RegExp and `false` if it does not. + + Example + + ```javascript + var User = Ember.Object.extend({ + hasValidEmail: Ember.computed.match('email', /^.+@.+\..+$/) + }); + + var user = User.create({loggedIn: false}); + + user.get('hasValidEmail'); // false + user.set('email', ''); + user.get('hasValidEmail'); // false + user.set('email', 'ember_hamster@example.com'); + user.get('hasValidEmail'); // true + ``` + + @method computed.match + @for Ember + @param {String} dependentKey + @param {RegExp} regexp + @return {Ember.ComputedProperty} computed property which match + the original value for property against a given RegExp + */ + registerComputed('match', function(dependentKey, regexp) { + var value = get(this, dependentKey); + return typeof value === 'string' ? regexp.test(value) : false; + }); + + /** + A computed property that returns true if the provided dependent property + is equal to the given value. + + Example + + ```javascript + var Hamster = Ember.Object.extend({ + napTime: Ember.computed.equal('state', 'sleepy') + }); + + var hamster = Hamster.create(); + + hamster.get('napTime'); // false + hamster.set('state', 'sleepy'); + hamster.get('napTime'); // true + hamster.set('state', 'hungry'); + hamster.get('napTime'); // false + ``` + + @method computed.equal + @for Ember + @param {String} dependentKey + @param {String|Number|Object} value + @return {Ember.ComputedProperty} computed property which returns true if + the original value for property is equal to the given value. + */ + registerComputed('equal', function(dependentKey, value) { + return get(this, dependentKey) === value; + }); + + /** + A computed property that returns true if the provied dependent property + is greater than the provided value. + + Example + + ```javascript + var Hamster = Ember.Object.extend({ + hasTooManyBananas: Ember.computed.gt('numBananas', 10) + }); + + var hamster = Hamster.create(); + + hamster.get('hasTooManyBananas'); // false + hamster.set('numBananas', 3); + hamster.get('hasTooManyBananas'); // false + hamster.set('numBananas', 11); + hamster.get('hasTooManyBananas'); // true + ``` + + @method computed.gt + @for Ember + @param {String} dependentKey + @param {Number} value + @return {Ember.ComputedProperty} computed property which returns true if + the original value for property is greater then given value. + */ + registerComputed('gt', function(dependentKey, value) { + return get(this, dependentKey) > value; + }); + + /** + A computed property that returns true if the provided dependent property + is greater than or equal to the provided value. + + Example + + ```javascript + var Hamster = Ember.Object.extend({ + hasTooManyBananas: Ember.computed.gte('numBananas', 10) + }); + + var hamster = Hamster.create(); + + hamster.get('hasTooManyBananas'); // false + hamster.set('numBananas', 3); + hamster.get('hasTooManyBananas'); // false + hamster.set('numBananas', 10); + hamster.get('hasTooManyBananas'); // true + ``` + + @method computed.gte + @for Ember + @param {String} dependentKey + @param {Number} value + @return {Ember.ComputedProperty} computed property which returns true if + the original value for property is greater or equal then given value. + */ + registerComputed('gte', function(dependentKey, value) { + return get(this, dependentKey) >= value; + }); + + /** + A computed property that returns true if the provided dependent property + is less than the provided value. + + Example + + ```javascript + var Hamster = Ember.Object.extend({ + needsMoreBananas: Ember.computed.lt('numBananas', 3) + }); + + var hamster = Hamster.create(); + + hamster.get('needsMoreBananas'); // true + hamster.set('numBananas', 3); + hamster.get('needsMoreBananas'); // false + hamster.set('numBananas', 2); + hamster.get('needsMoreBananas'); // true + ``` + + @method computed.lt + @for Ember + @param {String} dependentKey + @param {Number} value + @return {Ember.ComputedProperty} computed property which returns true if + the original value for property is less then given value. + */ + registerComputed('lt', function(dependentKey, value) { + return get(this, dependentKey) < value; + }); + + /** + A computed property that returns true if the provided dependent property + is less than or equal to the provided value. + + Example + + ```javascript + var Hamster = Ember.Object.extend({ + needsMoreBananas: Ember.computed.lte('numBananas', 3) + }); + + var hamster = Hamster.create(); + + hamster.get('needsMoreBananas'); // true + hamster.set('numBananas', 5); + hamster.get('needsMoreBananas'); // false + hamster.set('numBananas', 3); + hamster.get('needsMoreBananas'); // true + ``` + + @method computed.lte + @for Ember + @param {String} dependentKey + @param {Number} value + @return {Ember.ComputedProperty} computed property which returns true if + the original value for property is less or equal then given value. + */ + registerComputed('lte', function(dependentKey, value) { + return get(this, dependentKey) <= value; + }); + + /** + A computed property that performs a logical `and` on the + original values for the provided dependent properties. + + Example + + ```javascript + var Hamster = Ember.Object.extend({ + readyForCamp: Ember.computed.and('hasTent', 'hasBackpack') + }); + + var hamster = Hamster.create(); + + hamster.get('readyForCamp'); // false + hamster.set('hasTent', true); + hamster.get('readyForCamp'); // false + hamster.set('hasBackpack', true); + hamster.get('readyForCamp'); // true + ``` + + @method computed.and + @for Ember + @param {String} dependentKey* + @return {Ember.ComputedProperty} computed property which performs + a logical `and` on the values of all the original values for properties. + */ + registerComputedWithProperties('and', function(properties) { + for (var key in properties) { + if (properties.hasOwnProperty(key) && !properties[key]) { + return false; + } + } + return true; + }); + + /** + A computed property which performs a logical `or` on the + original values for the provided dependent properties. + + Example + + ```javascript + var Hamster = Ember.Object.extend({ + readyForRain: Ember.computed.or('hasJacket', 'hasUmbrella') + }); + + var hamster = Hamster.create(); + + hamster.get('readyForRain'); // false + hamster.set('hasJacket', true); + hamster.get('readyForRain'); // true + ``` + + @method computed.or + @for Ember + @param {String} dependentKey* + @return {Ember.ComputedProperty} computed property which performs + a logical `or` on the values of all the original values for properties. + */ + registerComputedWithProperties('or', function(properties) { + for (var key in properties) { + if (properties.hasOwnProperty(key) && properties[key]) { + return true; + } + } + return false; + }); + + /** + A computed property that returns the first truthy value + from a list of dependent properties. + + Example + + ```javascript + var Hamster = Ember.Object.extend({ + hasClothes: Ember.computed.any('hat', 'shirt') + }); + + var hamster = Hamster.create(); + + hamster.get('hasClothes'); // null + hamster.set('shirt', 'Hawaiian Shirt'); + hamster.get('hasClothes'); // 'Hawaiian Shirt' + ``` + + @method computed.any + @for Ember + @param {String} dependentKey* + @return {Ember.ComputedProperty} computed property which returns + the first truthy value of given list of properties. + */ + registerComputedWithProperties('any', function(properties) { + for (var key in properties) { + if (properties.hasOwnProperty(key) && properties[key]) { + return properties[key]; + } + } + return null; + }); + + /** + A computed property that returns the array of values + for the provided dependent properties. + + Example + + ```javascript + var Hamster = Ember.Object.extend({ + clothes: Ember.computed.collect('hat', 'shirt') + }); + + var hamster = Hamster.create(); + + hamster.get('clothes'); // [null, null] + hamster.set('hat', 'Camp Hat'); + hamster.set('shirt', 'Camp Shirt'); + hamster.get('clothes'); // ['Camp Hat', 'Camp Shirt'] + ``` + + @method computed.collect + @for Ember + @param {String} dependentKey* + @return {Ember.ComputedProperty} computed property which maps + values of all passed properties in to an array. + */ + registerComputedWithProperties('collect', function(properties) { + var res = []; + for (var key in properties) { + if (properties.hasOwnProperty(key)) { + if (isNone(properties[key])) { + res.push(null); + } else { + res.push(properties[key]); + } + } + } + return res; + }); + + /** + Creates a new property that is an alias for another property + on an object. Calls to `get` or `set` this property behave as + though they were called on the original property. + + ```javascript + var Person = Ember.Object.extend({ + name: 'Alex Matchneer', + nomen: Ember.computed.alias('name') + }); + + var alex = Person.create(); + + alex.get('nomen'); // 'Alex Matchneer' + alex.get('name'); // 'Alex Matchneer' + + alex.set('nomen', '@machty'); + alex.get('name'); // '@machty' + ``` + + @method computed.alias + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which creates an + alias to the original value for property. + */ + computed.alias = function(dependentKey) { + return computed(dependentKey, function(key, value) { + if (arguments.length > 1) { + set(this, dependentKey, value); + return get(this, dependentKey); + } else { + return get(this, dependentKey); + } + }); + }; + + /** + Where `computed.alias` aliases `get` and `set`, and allows for bidirectional + data flow, `computed.oneWay` only provides an aliased `get`. The `set` will + not mutate the upstream property, rather causes the current property to + become the value set. This causes the downstream property to permanently + diverge from the upstream property. + + Example + + ```javascript + var User = Ember.Object.extend({ + firstName: null, + lastName: null, + nickName: Ember.computed.oneWay('firstName') + }); + + var teddy = User.create({ + firstName: 'Teddy', + lastName: 'Zeenny' + }); + + teddy.get('nickName'); // 'Teddy' + teddy.set('nickName', 'TeddyBear'); // 'TeddyBear' + teddy.get('firstName'); // 'Teddy' + ``` + + @method computed.oneWay + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which creates a + one way computed property to the original value for property. + */ + computed.oneWay = function(dependentKey) { + return computed(dependentKey, function() { + return get(this, dependentKey); + }); + }; + + + /** + Where `computed.oneWay` provides oneWay bindings, `computed.readOnly` provides + a readOnly one way binding. Very often when using `computed.oneWay` one does + not also want changes to propogate back up, as they will replace the value. + + This prevents the reverse flow, and also throws an exception when it occurs. + + Example + + ```javascript + var User = Ember.Object.extend({ + firstName: null, + lastName: null, + nickName: Ember.computed.readOnly('firstName') + }); + + var teddy = User.create({ + firstName: 'Teddy', + lastName: 'Zeenny' + }); + + teddy.get('nickName'); // 'Teddy' + teddy.set('nickName', 'TeddyBear'); // throws Exception + // throw new Ember.Error('Cannot Set: nickName on: ' );` + teddy.get('firstName'); // 'Teddy' + ``` + + @method computed.readOnly + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which creates a + one way computed property to the original value for property. + @since 1.5.0 + */ + computed.readOnly = function(dependentKey) { + return computed(dependentKey, function() { + return get(this, dependentKey); + }).readOnly(); + }; + /** + A computed property that acts like a standard getter and setter, + but returns the value at the provided `defaultPath` if the + property itself has not been set to a value + + Example + + ```javascript + var Hamster = Ember.Object.extend({ + wishList: Ember.computed.defaultTo('favoriteFood') + }); + + var hamster = Hamster.create({ favoriteFood: 'Banana' }); + + hamster.get('wishList'); // 'Banana' + hamster.set('wishList', 'More Unit Tests'); + hamster.get('wishList'); // 'More Unit Tests' + hamster.get('favoriteFood'); // 'Banana' + ``` + + @method computed.defaultTo + @for Ember + @param {String} defaultPath + @return {Ember.ComputedProperty} computed property which acts like + a standard getter and setter, but defaults to the value from `defaultPath`. + */ + // ES6TODO: computed should have its own export path so you can do import {defaultTo} from computed + computed.defaultTo = function(defaultPath) { + return computed(function(key, newValue, cachedValue) { + if (arguments.length === 1) { + return get(this, defaultPath); + } + return newValue != null ? newValue : get(this, defaultPath); + }); + }; + + __exports__.ComputedProperty = ComputedProperty; + __exports__.computed = computed; + __exports__.cacheFor = cacheFor; + }); +define("ember-metal/core", + ["exports"], + function(__exports__) { + "use strict"; + /*globals Em:true ENV EmberENV MetamorphENV:true */ + + /** + @module ember + @submodule ember-metal + */ + + /** + All Ember methods and functions are defined inside of this namespace. You + generally should not add new properties to this namespace as it may be + overwritten by future versions of Ember. + + You can also use the shorthand `Em` instead of `Ember`. + + Ember-Runtime is a framework that provides core functions for Ember including + cross-platform functions, support for property observing and objects. Its + focus is on small size and performance. You can use this in place of or + along-side other cross-platform libraries such as jQuery. + + The core Runtime framework is based on the jQuery API with a number of + performance optimizations. + + @class Ember + @static + @version 1.6.1 + */ + + if ('undefined' === typeof Ember) { + // Create core object. Make it act like an instance of Ember.Namespace so that + // objects assigned to it are given a sane string representation. + Ember = {}; + } + + // Default imports, exports and lookup to the global object; + var imports = Ember.imports = Ember.imports || this; + var exports = Ember.exports = Ember.exports || this; + var lookup = Ember.lookup = Ember.lookup || this; + + // aliases needed to keep minifiers from removing the global context + exports.Em = exports.Ember = Ember; + + // Make sure these are set whether Ember was already defined or not + + Ember.isNamespace = true; + + Ember.toString = function() { return "Ember"; }; + + + /** + @property VERSION + @type String + @default '1.6.1' + @static + */ + Ember.VERSION = '1.6.1'; + + /** + Standard environmental variables. You can define these in a global `EmberENV` + variable before loading Ember to control various configuration settings. + + For backwards compatibility with earlier versions of Ember the global `ENV` + variable will be used if `EmberENV` is not defined. + + @property ENV + @type Hash + */ + + if (Ember.ENV) { + // do nothing if Ember.ENV is already setup + } else if ('undefined' !== typeof EmberENV) { + Ember.ENV = EmberENV; + } else if('undefined' !== typeof ENV) { + Ember.ENV = ENV; + } else { + Ember.ENV = {}; + } + + Ember.config = Ember.config || {}; + + // We disable the RANGE API by default for performance reasons + if ('undefined' === typeof Ember.ENV.DISABLE_RANGE_API) { + Ember.ENV.DISABLE_RANGE_API = true; + } + + if ("undefined" === typeof MetamorphENV) { + exports.MetamorphENV = {}; + } + + MetamorphENV.DISABLE_RANGE_API = Ember.ENV.DISABLE_RANGE_API; + + /** + Hash of enabled Canary features. Add to before creating your application. + + You can also define `ENV.FEATURES` if you need to enable features flagged at runtime. + + @class FEATURES + @namespace Ember + @static + @since 1.1.0 + */ + + Ember.FEATURES = Ember.ENV.FEATURES || {}; + + /** + Test that a feature is enabled. Parsed by Ember's build tools to leave + experimental features out of beta/stable builds. + + You can define the following configuration options: + + * `ENV.ENABLE_ALL_FEATURES` - force all features to be enabled. + * `ENV.ENABLE_OPTIONAL_FEATURES` - enable any features that have not been explicitly + enabled/disabled. + + @method isEnabled + @param {String} feature + @return {Boolean} + @for Ember.FEATURES + @since 1.1.0 + */ + + Ember.FEATURES.isEnabled = function(feature) { + var featureValue = Ember.FEATURES[feature]; + + if (Ember.ENV.ENABLE_ALL_FEATURES) { + return true; + } else if (featureValue === true || featureValue === false || featureValue === undefined) { + return featureValue; + } else if (Ember.ENV.ENABLE_OPTIONAL_FEATURES) { + return true; + } else { + return false; + } + }; + + // .......................................................... + // BOOTSTRAP + // + + /** + Determines whether Ember should enhance some built-in object prototypes to + provide a more friendly API. If enabled, a few methods will be added to + `Function`, `String`, and `Array`. `Object.prototype` will not be enhanced, + which is the one that causes most trouble for people. + + In general we recommend leaving this option set to true since it rarely + conflicts with other code. If you need to turn it off however, you can + define an `ENV.EXTEND_PROTOTYPES` config to disable it. + + @property EXTEND_PROTOTYPES + @type Boolean + @default true + @for Ember + */ + Ember.EXTEND_PROTOTYPES = Ember.ENV.EXTEND_PROTOTYPES; + + if (typeof Ember.EXTEND_PROTOTYPES === 'undefined') { + Ember.EXTEND_PROTOTYPES = true; + } + + /** + Determines whether Ember logs a full stack trace during deprecation warnings + + @property LOG_STACKTRACE_ON_DEPRECATION + @type Boolean + @default true + */ + Ember.LOG_STACKTRACE_ON_DEPRECATION = (Ember.ENV.LOG_STACKTRACE_ON_DEPRECATION !== false); + + /** + Determines whether Ember should add ECMAScript 5 shims to older browsers. + + @property SHIM_ES5 + @type Boolean + @default Ember.EXTEND_PROTOTYPES + */ + Ember.SHIM_ES5 = (Ember.ENV.SHIM_ES5 === false) ? false : Ember.EXTEND_PROTOTYPES; + + /** + Determines whether Ember logs info about version of used libraries + + @property LOG_VERSION + @type Boolean + @default true + */ + Ember.LOG_VERSION = (Ember.ENV.LOG_VERSION === false) ? false : true; + + /** + Empty function. Useful for some operations. Always returns `this`. + + @method K + @private + @return {Object} + */ + Ember.K = function() { return this; }; + + + // Stub out the methods defined by the ember-debug package in case it's not loaded + + if ('undefined' === typeof Ember.assert) { Ember.assert = Ember.K; } + if ('undefined' === typeof Ember.warn) { Ember.warn = Ember.K; } + if ('undefined' === typeof Ember.debug) { Ember.debug = Ember.K; } + if ('undefined' === typeof Ember.runInDebug) { Ember.runInDebug = Ember.K; } + if ('undefined' === typeof Ember.deprecate) { Ember.deprecate = Ember.K; } + if ('undefined' === typeof Ember.deprecateFunc) { + Ember.deprecateFunc = function(_, func) { return func; }; + } + + /** + Previously we used `Ember.$.uuid`, however `$.uuid` has been removed from + jQuery master. We'll just bootstrap our own uuid now. + + @property uuid + @type Number + @private + */ + Ember.uuid = 0; + + __exports__["default"] = Ember; + }); +define("ember-metal/enumerable_utils", + ["ember-metal/array","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var map, forEach, indexOf, splice, filter; + + var map = __dependency1__.map; + var forEach = __dependency1__.forEach; + var indexOf = __dependency1__.indexOf; + var filter = __dependency1__.filter; + + // ES6TODO: doesn't array polyfills already do this? + map = Array.prototype.map || map; + forEach = Array.prototype.forEach || forEach; + indexOf = Array.prototype.indexOf || indexOf; + filter = Array.prototype.filter || filter; + splice = Array.prototype.splice; + + /** + * Defines some convenience methods for working with Enumerables. + * `Ember.EnumerableUtils` uses `Ember.ArrayPolyfills` when necessary. + * + * @class EnumerableUtils + * @namespace Ember + * @static + * */ + var utils = { + /** + * Calls the map function on the passed object with a specified callback. This + * uses `Ember.ArrayPolyfill`'s-map method when necessary. + * + * @method map + * @param {Object} obj The object that should be mapped + * @param {Function} callback The callback to execute + * @param {Object} thisArg Value to use as this when executing *callback* + * + * @return {Array} An array of mapped values. + */ + map: function(obj, callback, thisArg) { + return obj.map ? obj.map.call(obj, callback, thisArg) : map.call(obj, callback, thisArg); + }, + + /** + * Calls the forEach function on the passed object with a specified callback. This + * uses `Ember.ArrayPolyfill`'s-forEach method when necessary. + * + * @method forEach + * @param {Object} obj The object to call forEach on + * @param {Function} callback The callback to execute + * @param {Object} thisArg Value to use as this when executing *callback* + * + */ + forEach: function(obj, callback, thisArg) { + return obj.forEach ? obj.forEach.call(obj, callback, thisArg) : forEach.call(obj, callback, thisArg); + }, + + /** + * Calls the filter function on the passed object with a specified callback. This + * uses `Ember.ArrayPolyfill`'s-filter method when necessary. + * + * @method filter + * @param {Object} obj The object to call filter on + * @param {Function} callback The callback to execute + * @param {Object} thisArg Value to use as this when executing *callback* + * + * @return {Array} An array containing the filtered values + * @since 1.4.0 + */ + filter: function(obj, callback, thisArg) { + return obj.filter ? obj.filter.call(obj, callback, thisArg) : filter.call(obj, callback, thisArg); + }, + + /** + * Calls the indexOf function on the passed object with a specified callback. This + * uses `Ember.ArrayPolyfill`'s-indexOf method when necessary. + * + * @method indexOf + * @param {Object} obj The object to call indexOn on + * @param {Function} callback The callback to execute + * @param {Object} index The index to start searching from + * + */ + indexOf: function(obj, element, index) { + return obj.indexOf ? obj.indexOf.call(obj, element, index) : indexOf.call(obj, element, index); + }, + + /** + * Returns an array of indexes of the first occurrences of the passed elements + * on the passed object. + * + * ```javascript + * var array = [1, 2, 3, 4, 5]; + * Ember.EnumerableUtils.indexesOf(array, [2, 5]); // [1, 4] + * + * var fubar = "Fubarr"; + * Ember.EnumerableUtils.indexesOf(fubar, ['b', 'r']); // [2, 4] + * ``` + * + * @method indexesOf + * @param {Object} obj The object to check for element indexes + * @param {Array} elements The elements to search for on *obj* + * + * @return {Array} An array of indexes. + * + */ + indexesOf: function(obj, elements) { + return elements === undefined ? [] : utils.map(elements, function(item) { + return utils.indexOf(obj, item); + }); + }, + + /** + * Adds an object to an array. If the array already includes the object this + * method has no effect. + * + * @method addObject + * @param {Array} array The array the passed item should be added to + * @param {Object} item The item to add to the passed array + * + * @return 'undefined' + */ + addObject: function(array, item) { + var index = utils.indexOf(array, item); + if (index === -1) { array.push(item); } + }, + + /** + * Removes an object from an array. If the array does not contain the passed + * object this method has no effect. + * + * @method removeObject + * @param {Array} array The array to remove the item from. + * @param {Object} item The item to remove from the passed array. + * + * @return 'undefined' + */ + removeObject: function(array, item) { + var index = utils.indexOf(array, item); + if (index !== -1) { array.splice(index, 1); } + }, + + _replace: function(array, idx, amt, objects) { + var args = [].concat(objects), chunk, ret = [], + // https://code.google.com/p/chromium/issues/detail?id=56588 + size = 60000, start = idx, ends = amt, count; + + while (args.length) { + count = ends > size ? size : ends; + if (count <= 0) { count = 0; } + + chunk = args.splice(0, size); + chunk = [start, count].concat(chunk); + + start += size; + ends -= count; + + ret = ret.concat(splice.apply(array, chunk)); + } + return ret; + }, + + /** + * Replaces objects in an array with the passed objects. + * + * ```javascript + * var array = [1,2,3]; + * Ember.EnumerableUtils.replace(array, 1, 2, [4, 5]); // [1, 4, 5] + * + * var array = [1,2,3]; + * Ember.EnumerableUtils.replace(array, 1, 1, [4, 5]); // [1, 4, 5, 3] + * + * var array = [1,2,3]; + * Ember.EnumerableUtils.replace(array, 10, 1, [4, 5]); // [1, 2, 3, 4, 5] + * ``` + * + * @method replace + * @param {Array} array The array the objects should be inserted into. + * @param {Number} idx Starting index in the array to replace. If *idx* >= + * length, then append to the end of the array. + * @param {Number} amt Number of elements that should be removed from the array, + * starting at *idx* + * @param {Array} objects An array of zero or more objects that should be + * inserted into the array at *idx* + * + * @return {Array} The modified array. + */ + replace: function(array, idx, amt, objects) { + if (array.replace) { + return array.replace(idx, amt, objects); + } else { + return utils._replace(array, idx, amt, objects); + } + }, + + /** + * Calculates the intersection of two arrays. This method returns a new array + * filled with the records that the two passed arrays share with each other. + * If there is no intersection, an empty array will be returned. + * + * ```javascript + * var array1 = [1, 2, 3, 4, 5]; + * var array2 = [1, 3, 5, 6, 7]; + * + * Ember.EnumerableUtils.intersection(array1, array2); // [1, 3, 5] + * + * var array1 = [1, 2, 3]; + * var array2 = [4, 5, 6]; + * + * Ember.EnumerableUtils.intersection(array1, array2); // [] + * ``` + * + * @method intersection + * @param {Array} array1 The first array + * @param {Array} array2 The second array + * + * @return {Array} The intersection of the two passed arrays. + */ + intersection: function(array1, array2) { + var intersection = []; + + utils.forEach(array1, function(element) { + if (utils.indexOf(array2, element) >= 0) { + intersection.push(element); + } + }); + + return intersection; + } + }; + + __exports__["default"] = utils; + }); +define("ember-metal/error", + ["ember-metal/platform","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var create = __dependency1__.create; + + var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack']; + + /** + A subclass of the JavaScript Error object for use in Ember. + + @class Error + @namespace Ember + @extends Error + @constructor + */ + var EmberError = function() { + var tmp = Error.apply(this, arguments); + + // Adds a `stack` property to the given error object that will yield the + // stack trace at the time captureStackTrace was called. + // When collecting the stack trace all frames above the topmost call + // to this function, including that call, will be left out of the + // stack trace. + // This is useful because we can hide Ember implementation details + // that are not very helpful for the user. + if (Error.captureStackTrace) { + Error.captureStackTrace(this, Ember.Error); + } + // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work. + for (var idx = 0; idx < errorProps.length; idx++) { + this[errorProps[idx]] = tmp[errorProps[idx]]; + } + }; + + EmberError.prototype = create(Error.prototype); + + __exports__["default"] = EmberError; + }); +define("ember-metal/events", + ["ember-metal/core","ember-metal/utils","ember-metal/platform","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __exports__) { + "use strict"; + /** + @module ember-metal + */ + var Ember = __dependency1__["default"]; + var meta = __dependency2__.meta; + var META_KEY = __dependency2__.META_KEY; + var tryFinally = __dependency2__.tryFinally; + var apply = __dependency2__.apply; + var applyStr = __dependency2__.applyStr; + var create = __dependency3__.create; + + var a_slice = [].slice, + metaFor = meta, + /* listener flags */ + ONCE = 1, SUSPENDED = 2; + + + /* + The event system uses a series of nested hashes to store listeners on an + object. When a listener is registered, or when an event arrives, these + hashes are consulted to determine which target and action pair to invoke. + + The hashes are stored in the object's meta hash, and look like this: + + // Object's meta hash + { + listeners: { // variable name: `listenerSet` + "foo:changed": [ // variable name: `actions` + target, method, flags + ] + } + } + + */ + + function indexOf(array, target, method) { + var index = -1; + // hashes are added to the end of the event array + // so it makes sense to start searching at the end + // of the array and search in reverse + for (var i = array.length - 3 ; i >=0; i -= 3) { + if (target === array[i] && method === array[i + 1]) { + index = i; break; + } + } + return index; + } + + function actionsFor(obj, eventName) { + var meta = metaFor(obj, true), + actions; + + if (!meta.listeners) { meta.listeners = {}; } + + if (!meta.hasOwnProperty('listeners')) { + // setup inherited copy of the listeners object + meta.listeners = create(meta.listeners); + } + + actions = meta.listeners[eventName]; + + // if there are actions, but the eventName doesn't exist in our listeners, then copy them from the prototype + if (actions && !meta.listeners.hasOwnProperty(eventName)) { + actions = meta.listeners[eventName] = meta.listeners[eventName].slice(); + } else if (!actions) { + actions = meta.listeners[eventName] = []; + } + + return actions; + } + + function listenersUnion(obj, eventName, otherActions) { + var meta = obj[META_KEY], + actions = meta && meta.listeners && meta.listeners[eventName]; + + if (!actions) { return; } + for (var i = actions.length - 3; i >= 0; i -= 3) { + var target = actions[i], + method = actions[i+1], + flags = actions[i+2], + actionIndex = indexOf(otherActions, target, method); + + if (actionIndex === -1) { + otherActions.push(target, method, flags); + } + } + } + + function listenersDiff(obj, eventName, otherActions) { + var meta = obj[META_KEY], + actions = meta && meta.listeners && meta.listeners[eventName], + diffActions = []; + + if (!actions) { return; } + for (var i = actions.length - 3; i >= 0; i -= 3) { + var target = actions[i], + method = actions[i+1], + flags = actions[i+2], + actionIndex = indexOf(otherActions, target, method); + + if (actionIndex !== -1) { continue; } + + otherActions.push(target, method, flags); + diffActions.push(target, method, flags); + } + + return diffActions; + } + + /** + Add an event listener + + @method addListener + @for Ember + @param obj + @param {String} eventName + @param {Object|Function} targetOrMethod A target object or a function + @param {Function|String} method A function or the name of a function to be called on `target` + @param {Boolean} once A flag whether a function should only be called once + */ + function addListener(obj, eventName, target, method, once) { + Ember.assert("You must pass at least an object and event name to Ember.addListener", !!obj && !!eventName); + + if (!method && 'function' === typeof target) { + method = target; + target = null; + } + + var actions = actionsFor(obj, eventName), + actionIndex = indexOf(actions, target, method), + flags = 0; + + if (once) flags |= ONCE; + + if (actionIndex !== -1) { return; } + + actions.push(target, method, flags); + + if ('function' === typeof obj.didAddListener) { + obj.didAddListener(eventName, target, method); + } + } + + /** + Remove an event listener + + Arguments should match those passed to `Ember.addListener`. + + @method removeListener + @for Ember + @param obj + @param {String} eventName + @param {Object|Function} targetOrMethod A target object or a function + @param {Function|String} method A function or the name of a function to be called on `target` + */ + function removeListener(obj, eventName, target, method) { + Ember.assert("You must pass at least an object and event name to Ember.removeListener", !!obj && !!eventName); + + if (!method && 'function' === typeof target) { + method = target; + target = null; + } + + function _removeListener(target, method) { + var actions = actionsFor(obj, eventName), + actionIndex = indexOf(actions, target, method); + + // action doesn't exist, give up silently + if (actionIndex === -1) { return; } + + actions.splice(actionIndex, 3); + + if ('function' === typeof obj.didRemoveListener) { + obj.didRemoveListener(eventName, target, method); + } + } + + if (method) { + _removeListener(target, method); + } else { + var meta = obj[META_KEY], + actions = meta && meta.listeners && meta.listeners[eventName]; + + if (!actions) { return; } + for (var i = actions.length - 3; i >= 0; i -= 3) { + _removeListener(actions[i], actions[i+1]); + } + } + } + + /** + Suspend listener during callback. + + This should only be used by the target of the event listener + when it is taking an action that would cause the event, e.g. + an object might suspend its property change listener while it is + setting that property. + + @method suspendListener + @for Ember + + @private + @param obj + @param {String} eventName + @param {Object|Function} targetOrMethod A target object or a function + @param {Function|String} method A function or the name of a function to be called on `target` + @param {Function} callback + */ + function suspendListener(obj, eventName, target, method, callback) { + if (!method && 'function' === typeof target) { + method = target; + target = null; + } + + var actions = actionsFor(obj, eventName), + actionIndex = indexOf(actions, target, method); + + if (actionIndex !== -1) { + actions[actionIndex+2] |= SUSPENDED; // mark the action as suspended + } + + function tryable() { return callback.call(target); } + function finalizer() { if (actionIndex !== -1) { actions[actionIndex+2] &= ~SUSPENDED; } } + + return tryFinally(tryable, finalizer); + } + + /** + Suspends multiple listeners during a callback. + + @method suspendListeners + @for Ember + + @private + @param obj + @param {Array} eventName Array of event names + @param {Object|Function} targetOrMethod A target object or a function + @param {Function|String} method A function or the name of a function to be called on `target` + @param {Function} callback + */ + function suspendListeners(obj, eventNames, target, method, callback) { + if (!method && 'function' === typeof target) { + method = target; + target = null; + } + + var suspendedActions = [], + actionsList = [], + eventName, actions, i, l; + + for (i=0, l=eventNames.length; i= 0; i -= 3) { // looping in reverse for once listeners + var target = actions[i], method = actions[i+1], flags = actions[i+2]; + if (!method) { continue; } + if (flags & SUSPENDED) { continue; } + if (flags & ONCE) { removeListener(obj, eventName, target, method); } + if (!target) { target = obj; } + if ('string' === typeof method) { + if (params) { + applyStr(target, method, params); + } else { + target[method](); + } + } else { + if (params) { + apply(target, method, params); + } else { + method.call(target); + } + } + } + return true; + } + + /** + @private + @method hasListeners + @for Ember + @param obj + @param {String} eventName + */ + function hasListeners(obj, eventName) { + var meta = obj[META_KEY], + actions = meta && meta.listeners && meta.listeners[eventName]; + + return !!(actions && actions.length); + } + + /** + @private + @method listenersFor + @for Ember + @param obj + @param {String} eventName + */ + function listenersFor(obj, eventName) { + var ret = []; + var meta = obj[META_KEY], + actions = meta && meta.listeners && meta.listeners[eventName]; + + if (!actions) { return ret; } + + for (var i = 0, l = actions.length; i < l; i += 3) { + var target = actions[i], + method = actions[i+1]; + ret.push([target, method]); + } + + return ret; + } + + /** + Define a property as a function that should be executed when + a specified event or events are triggered. + + + ``` javascript + var Job = Ember.Object.extend({ + logCompleted: Ember.on('completed', function() { + console.log('Job completed!'); + }) + }); + + var job = Job.create(); + + Ember.sendEvent(job, 'completed'); // Logs 'Job completed!' + ``` + + @method on + @for Ember + @param {String} eventNames* + @param {Function} func + @return func + */ + function on(){ + var func = a_slice.call(arguments, -1)[0], + events = a_slice.call(arguments, 0, -1); + func.__ember_listens__ = events; + return func; + }; + + __exports__.on = on; + __exports__.addListener = addListener; + __exports__.removeListener = removeListener; + __exports__.suspendListener = suspendListener; + __exports__.suspendListeners = suspendListeners; + __exports__.sendEvent = sendEvent; + __exports__.hasListeners = hasListeners; + __exports__.watchedEvents = watchedEvents; + __exports__.listenersFor = listenersFor; + __exports__.listenersDiff = listenersDiff; + __exports__.listenersUnion = listenersUnion; + }); +define("ember-metal/expand_properties", + ["ember-metal/error","ember-metal/enumerable_utils","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + var EmberError = __dependency1__["default"]; + var EnumerableUtils = __dependency2__["default"]; + + /** + @module ember-metal + */ + + var forEach = EnumerableUtils.forEach, + BRACE_EXPANSION = /^((?:[^\.]*\.)*)\{(.*)\}$/; + + /** + Expands `pattern`, invoking `callback` for each expansion. + + The only pattern supported is brace-expansion, anything else will be passed + once to `callback` directly. Brace expansion can only appear at the end of a + pattern, for an example see the last call below. + + Example + ```js + function echo(arg){ console.log(arg); } + + Ember.expandProperties('foo.bar', echo); //=> 'foo.bar' + Ember.expandProperties('{foo,bar}', echo); //=> 'foo', 'bar' + Ember.expandProperties('foo.{bar,baz}', echo); //=> 'foo.bar', 'foo.baz' + Ember.expandProperties('{foo,bar}.baz', echo); //=> '{foo,bar}.baz' + ``` + + @method + @private + @param {string} pattern The property pattern to expand. + @param {function} callback The callback to invoke. It is invoked once per + expansion, and is passed the expansion. + */ + function expandProperties(pattern, callback) { + var match, prefix, list; + + if (pattern.indexOf(' ') > -1) { + throw new EmberError('Brace expanded properties cannot contain spaces, ' + + 'e.g. `user.{firstName, lastName}` should be `user.{firstName,lastName}`'); + } + + if (match = BRACE_EXPANSION.exec(pattern)) { + prefix = match[1]; + list = match[2]; + + forEach(list.split(','), function (suffix) { + callback(prefix + suffix); + }); + } else { + callback(pattern); + } + }; + + __exports__["default"] = expandProperties; + }); +define("ember-metal/get_properties", + ["ember-metal/property_get","ember-metal/utils","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + var get = __dependency1__.get; + var typeOf = __dependency2__.typeOf; + + /** + To get multiple properties at once, call `Ember.getProperties` + with an object followed by a list of strings or an array: + + ```javascript + Ember.getProperties(record, 'firstName', 'lastName', 'zipCode'); + // { firstName: 'John', lastName: 'Doe', zipCode: '10011' } + ``` + + is equivalent to: + + ```javascript + Ember.getProperties(record, ['firstName', 'lastName', 'zipCode']); + // { firstName: 'John', lastName: 'Doe', zipCode: '10011' } + ``` + + @method getProperties + @param obj + @param {String...|Array} list of keys to get + @return {Hash} + */ + function getProperties(obj) { + var ret = {}, + propertyNames = arguments, + i = 1; + + if (arguments.length === 2 && typeOf(arguments[1]) === 'array') { + i = 0; + propertyNames = arguments[1]; + } + for(var len = propertyNames.length; i < len; i++) { + ret[propertyNames[i]] = get(obj, propertyNames[i]); + } + return ret; + }; + + __exports__["default"] = getProperties; + }); +define("ember-metal/instrumentation", + ["ember-metal/core","ember-metal/utils","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + var Ember = __dependency1__["default"]; + var tryCatchFinally = __dependency2__.tryCatchFinally; + + /** + The purpose of the Ember Instrumentation module is + to provide efficient, general-purpose instrumentation + for Ember. + + Subscribe to a listener by using `Ember.subscribe`: + + ```javascript + Ember.subscribe("render", { + before: function(name, timestamp, payload) { + + }, + + after: function(name, timestamp, payload) { + + } + }); + ``` + + If you return a value from the `before` callback, that same + value will be passed as a fourth parameter to the `after` + callback. + + Instrument a block of code by using `Ember.instrument`: + + ```javascript + Ember.instrument("render.handlebars", payload, function() { + // rendering logic + }, binding); + ``` + + Event names passed to `Ember.instrument` are namespaced + by periods, from more general to more specific. Subscribers + can listen for events by whatever level of granularity they + are interested in. + + In the above example, the event is `render.handlebars`, + and the subscriber listened for all events beginning with + `render`. It would receive callbacks for events named + `render`, `render.handlebars`, `render.container`, or + even `render.handlebars.layout`. + + @class Instrumentation + @namespace Ember + @static + */ + var subscribers = [], cache = {}; + + var populateListeners = function(name) { + var listeners = [], subscriber; + + for (var i=0, l=subscribers.length; i -1) { + list.splice(index, 1); + } + }, + + /** + @method isEmpty + @return {Boolean} + */ + isEmpty: function() { + return this.list.length === 0; + }, + + /** + @method has + @param obj + @return {Boolean} + */ + has: function(obj) { + var guid = guidFor(obj), + presenceSet = this.presenceSet; + + return guid in presenceSet; + }, + + /** + @method forEach + @param {Function} fn + @param self + */ + forEach: function(fn, self) { + // allow mutation during iteration + var list = this.toArray(); + + for (var i = 0, j = list.length; i < j; i++) { + fn.call(self, list[i]); + } + }, + + /** + @method toArray + @return {Array} + */ + toArray: function() { + return this.list.slice(); + }, + + /** + @method copy + @return {Ember.OrderedSet} + */ + copy: function() { + var set = new OrderedSet(); + + set.presenceSet = copy(this.presenceSet); + set.list = this.toArray(); + + return set; + } + }; + + /** + A Map stores values indexed by keys. Unlike JavaScript's + default Objects, the keys of a Map can be any JavaScript + object. + + Internally, a Map has two data structures: + + 1. `keys`: an OrderedSet of all of the existing keys + 2. `values`: a JavaScript Object indexed by the `Ember.guidFor(key)` + + When a key/value pair is added for the first time, we + add the key to the `keys` OrderedSet, and create or + replace an entry in `values`. When an entry is deleted, + we delete its entry in `keys` and `values`. + + @class Map + @namespace Ember + @private + @constructor + */ + var Map = Ember.Map = function() { + this.keys = OrderedSet.create(); + this.values = {}; + }; + + /** + @method create + @static + */ + Map.create = function() { + return new Map(); + }; + + Map.prototype = { + /** + This property will change as the number of objects in the map changes. + + @property length + @type number + @default 0 + */ + length: 0, + + + /** + Retrieve the value associated with a given key. + + @method get + @param {*} key + @return {*} the value associated with the key, or `undefined` + */ + get: function(key) { + var values = this.values, + guid = guidFor(key); + + return values[guid]; + }, + + /** + Adds a value to the map. If a value for the given key has already been + provided, the new value will replace the old value. + + @method set + @param {*} key + @param {*} value + */ + set: function(key, value) { + var keys = this.keys, + values = this.values, + guid = guidFor(key); + + keys.add(key); + values[guid] = value; + set(this, 'length', keys.list.length); + }, + + /** + Removes a value from the map for an associated key. + + @method remove + @param {*} key + @return {Boolean} true if an item was removed, false otherwise + */ + remove: function(key) { + // don't use ES6 "delete" because it will be annoying + // to use in browsers that are not ES6 friendly; + var keys = this.keys, + values = this.values, + guid = guidFor(key); + + if (values.hasOwnProperty(guid)) { + keys.remove(key); + delete values[guid]; + set(this, 'length', keys.list.length); + return true; + } else { + return false; + } + }, + + /** + Check whether a key is present. + + @method has + @param {*} key + @return {Boolean} true if the item was present, false otherwise + */ + has: function(key) { + var values = this.values, + guid = guidFor(key); + + return values.hasOwnProperty(guid); + }, + + /** + Iterate over all the keys and values. Calls the function once + for each key, passing in the key and value, in that order. + + The keys are guaranteed to be iterated over in insertion order. + + @method forEach + @param {Function} callback + @param {*} self if passed, the `this` value inside the + callback. By default, `this` is the map. + */ + forEach: function(callback, self) { + var keys = this.keys, + values = this.values; + + keys.forEach(function(key) { + var guid = guidFor(key); + callback.call(self, key, values[guid]); + }); + }, + + /** + @method copy + @return {Ember.Map} + */ + copy: function() { + return copyMap(this, new Map()); + } + }; + + /** + @class MapWithDefault + @namespace Ember + @extends Ember.Map + @private + @constructor + @param [options] + @param {*} [options.defaultValue] + */ + function MapWithDefault(options) { + Map.call(this); + this.defaultValue = options.defaultValue; + }; + + /** + @method create + @static + @param [options] + @param {*} [options.defaultValue] + @return {Ember.MapWithDefault|Ember.Map} If options are passed, returns + `Ember.MapWithDefault` otherwise returns `Ember.Map` + */ + MapWithDefault.create = function(options) { + if (options) { + return new MapWithDefault(options); + } else { + return new Map(); + } + }; + + MapWithDefault.prototype = create(Map.prototype); + + /** + Retrieve the value associated with a given key. + + @method get + @param {*} key + @return {*} the value associated with the key, or the default value + */ + MapWithDefault.prototype.get = function(key) { + var hasValue = this.has(key); + + if (hasValue) { + return Map.prototype.get.call(this, key); + } else { + var defaultValue = this.defaultValue(key); + this.set(key, defaultValue); + return defaultValue; + } + }; + + /** + @method copy + @return {Ember.MapWithDefault} + */ + MapWithDefault.prototype.copy = function() { + return copyMap(this, new MapWithDefault({ + defaultValue: this.defaultValue + })); + }; + + __exports__.OrderedSet = OrderedSet; + __exports__.Map = Map; + __exports__.MapWithDefault = MapWithDefault; + }); +define("ember-metal/merge", + ["exports"], + function(__exports__) { + "use strict"; + /** + Merge the contents of two objects together into the first object. + + ```javascript + Ember.merge({first: 'Tom'}, {last: 'Dale'}); // {first: 'Tom', last: 'Dale'} + var a = {first: 'Yehuda'}, b = {last: 'Katz'}; + Ember.merge(a, b); // a == {first: 'Yehuda', last: 'Katz'}, b == {last: 'Katz'} + ``` + + @method merge + @for Ember + @param {Object} original The object to merge into + @param {Object} updates The object to copy properties from + @return {Object} + */ + function merge(original, updates) { + for (var prop in updates) { + if (!updates.hasOwnProperty(prop)) { continue; } + original[prop] = updates[prop]; + } + return original; + }; + + __exports__["default"] = merge; + }); +define("ember-metal/mixin", + ["ember-metal/core","ember-metal/merge","ember-metal/array","ember-metal/platform","ember-metal/utils","ember-metal/expand_properties","ember-metal/properties","ember-metal/computed","ember-metal/binding","ember-metal/observer","ember-metal/events","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __dependency11__, __exports__) { + "use strict"; + /** + @module ember + @submodule ember-metal + */ + + var Ember = __dependency1__["default"]; + // warn, assert, wrap, et; + var merge = __dependency2__["default"]; + var map = __dependency3__.map; + var indexOf = __dependency3__.indexOf; + var forEach = __dependency3__.forEach; + var create = __dependency4__.create; + var guidFor = __dependency5__.guidFor; + var meta = __dependency5__.meta; + var META_KEY = __dependency5__.META_KEY; + var wrap = __dependency5__.wrap; + var makeArray = __dependency5__.makeArray; + var apply = __dependency5__.apply; + var expandProperties = __dependency6__["default"]; + var Descriptor = __dependency7__.Descriptor; + var defineProperty = __dependency7__.defineProperty; + var ComputedProperty = __dependency8__.ComputedProperty; + var Binding = __dependency9__.Binding; + var addObserver = __dependency10__.addObserver; + var removeObserver = __dependency10__.removeObserver; + var addBeforeObserver = __dependency10__.addBeforeObserver; + var removeBeforeObserver = __dependency10__.removeBeforeObserver; + var addListener = __dependency11__.addListener; + var removeListener = __dependency11__.removeListener; + + var REQUIRED, Alias, + a_map = map, + a_indexOf = indexOf, + a_forEach = forEach, + a_slice = [].slice, + o_create = create, + defineProperty = defineProperty, + metaFor = meta; + + function superFunction(){ + var ret, func = this.__nextSuper; + if (func) { + this.__nextSuper = null; + ret = apply(this, func, arguments); + this.__nextSuper = func; + } + return ret; + } + + function mixinsMeta(obj) { + var m = metaFor(obj, true), ret = m.mixins; + if (!ret) { + ret = m.mixins = {}; + } else if (!m.hasOwnProperty('mixins')) { + ret = m.mixins = o_create(ret); + } + return ret; + } + + function initMixin(mixin, args) { + if (args && args.length > 0) { + mixin.mixins = a_map.call(args, function(x) { + if (x instanceof Mixin) { return x; } + + // Note: Manually setup a primitive mixin here. This is the only + // way to actually get a primitive mixin. This way normal creation + // of mixins will give you combined mixins... + var mixin = new Mixin(); + mixin.properties = x; + return mixin; + }); + } + return mixin; + } + + function isMethod(obj) { + return 'function' === typeof obj && + obj.isMethod !== false && + obj !== Boolean && obj !== Object && obj !== Number && obj !== Array && obj !== Date && obj !== String; + } + + var CONTINUE = {}; + + function mixinProperties(mixinsMeta, mixin) { + var guid; + + if (mixin instanceof Mixin) { + guid = guidFor(mixin); + if (mixinsMeta[guid]) { return CONTINUE; } + mixinsMeta[guid] = mixin; + return mixin.properties; + } else { + return mixin; // apply anonymous mixin properties + } + } + + function concatenatedMixinProperties(concatProp, props, values, base) { + var concats; + + // reset before adding each new mixin to pickup concats from previous + concats = values[concatProp] || base[concatProp]; + if (props[concatProp]) { + concats = concats ? concats.concat(props[concatProp]) : props[concatProp]; + } + + return concats; + } + + function giveDescriptorSuper(meta, key, property, values, descs) { + var superProperty; + + // Computed properties override methods, and do not call super to them + if (values[key] === undefined) { + // Find the original descriptor in a parent mixin + superProperty = descs[key]; + } + + // If we didn't find the original descriptor in a parent mixin, find + // it on the original object. + superProperty = superProperty || meta.descs[key]; + + if (!superProperty || !(superProperty instanceof ComputedProperty)) { + return property; + } + + // Since multiple mixins may inherit from the same parent, we need + // to clone the computed property so that other mixins do not receive + // the wrapped version. + property = o_create(property); + property.func = wrap(property.func, superProperty.func); + + return property; + } + + function giveMethodSuper(obj, key, method, values, descs) { + var superMethod; + + // Methods overwrite computed properties, and do not call super to them. + if (descs[key] === undefined) { + // Find the original method in a parent mixin + superMethod = values[key]; + } + + // If we didn't find the original value in a parent mixin, find it in + // the original object + superMethod = superMethod || obj[key]; + + // Only wrap the new method if the original method was a function + if ('function' !== typeof superMethod) { + return method; + } + + return wrap(method, superMethod); + } + + function applyConcatenatedProperties(obj, key, value, values) { + var baseValue = values[key] || obj[key]; + + if (baseValue) { + if ('function' === typeof baseValue.concat) { + return baseValue.concat(value); + } else { + return makeArray(baseValue).concat(value); + } + } else { + return makeArray(value); + } + } + + function applyMergedProperties(obj, key, value, values) { + var baseValue = values[key] || obj[key]; + + if (!baseValue) { return value; } + + var newBase = merge({}, baseValue), + hasFunction = false; + + for (var prop in value) { + if (!value.hasOwnProperty(prop)) { continue; } + + var propValue = value[prop]; + if (isMethod(propValue)) { + // TODO: support for Computed Properties, etc? + hasFunction = true; + newBase[prop] = giveMethodSuper(obj, prop, propValue, baseValue, {}); + } else { + newBase[prop] = propValue; + } + } + + if (hasFunction) { + newBase._super = superFunction; + } + + return newBase; + } + + function addNormalizedProperty(base, key, value, meta, descs, values, concats, mergings) { + if (value instanceof Descriptor) { + if (value === REQUIRED && descs[key]) { return CONTINUE; } + + // Wrap descriptor function to implement + // __nextSuper() if needed + if (value.func) { + value = giveDescriptorSuper(meta, key, value, values, descs); + } + + descs[key] = value; + values[key] = undefined; + } else { + if ((concats && a_indexOf.call(concats, key) >= 0) || + key === 'concatenatedProperties' || + key === 'mergedProperties') { + value = applyConcatenatedProperties(base, key, value, values); + } else if ((mergings && a_indexOf.call(mergings, key) >= 0)) { + value = applyMergedProperties(base, key, value, values); + } else if (isMethod(value)) { + value = giveMethodSuper(base, key, value, values, descs); + } + + descs[key] = undefined; + values[key] = value; + } + } + + function mergeMixins(mixins, m, descs, values, base, keys) { + var mixin, props, key, concats, mergings, meta; + + function removeKeys(keyName) { + delete descs[keyName]; + delete values[keyName]; + } + + for(var i=0, l=mixins.length; i= 0) { + if (_detect(mixins[loc], targetMixin, seen)) { return true; } + } + return false; + } + + /** + @method detect + @param obj + @return {Boolean} + */ + MixinPrototype.detect = function(obj) { + if (!obj) { return false; } + if (obj instanceof Mixin) { return _detect(obj, this, {}); } + var m = obj[META_KEY], + mixins = m && m.mixins; + if (mixins) { + return !!mixins[guidFor(this)]; + } + return false; + }; + + MixinPrototype.without = function() { + var ret = new Mixin(this); + ret._without = a_slice.call(arguments); + return ret; + }; + + function _keys(ret, mixin, seen) { + if (seen[guidFor(mixin)]) { return; } + seen[guidFor(mixin)] = true; + + if (mixin.properties) { + var props = mixin.properties; + for (var key in props) { + if (props.hasOwnProperty(key)) { ret[key] = true; } + } + } else if (mixin.mixins) { + a_forEach.call(mixin.mixins, function(x) { _keys(ret, x, seen); }); + } + } + + MixinPrototype.keys = function() { + var keys = {}, seen = {}, ret = []; + _keys(keys, this, seen); + for(var key in keys) { + if (keys.hasOwnProperty(key)) { ret.push(key); } + } + return ret; + }; + + // returns the mixins currently applied to the specified object + // TODO: Make Ember.mixin + Mixin.mixins = function(obj) { + var m = obj[META_KEY], + mixins = m && m.mixins, ret = []; + + if (!mixins) { return ret; } + + for (var key in mixins) { + var mixin = mixins[key]; + + // skip primitive mixins since these are always anonymous + if (!mixin.properties) { ret.push(mixin); } + } + + return ret; + }; + + REQUIRED = new Descriptor(); + REQUIRED.toString = function() { return '(Required Property)'; }; + + /** + Denotes a required property for a mixin + + @method required + @for Ember + */ + function required() { + return REQUIRED; + }; + + Alias = function(methodName) { + this.methodName = methodName; + }; + Alias.prototype = new Descriptor(); + + /** + Makes a method available via an additional name. + + ```javascript + App.Person = Ember.Object.extend({ + name: function() { + return 'Tomhuda Katzdale'; + }, + moniker: Ember.aliasMethod('name') + }); + + var goodGuy = App.Person.create(); + + goodGuy.name(); // 'Tomhuda Katzdale' + goodGuy.moniker(); // 'Tomhuda Katzdale' + ``` + + @method aliasMethod + @for Ember + @param {String} methodName name of the method to alias + @return {Ember.Descriptor} + */ + function aliasMethod(methodName) { + return new Alias(methodName); + }; + + // .......................................................... + // OBSERVER HELPER + // + + /** + Specify a method that observes property changes. + + ```javascript + Ember.Object.extend({ + valueObserver: Ember.observer('value', function() { + // Executes whenever the "value" property changes + }) + }); + ``` + + In the future this method may become asynchronous. If you want to ensure + synchronous behavior, use `immediateObserver`. + + Also available as `Function.prototype.observes` if prototype extensions are + enabled. + + @method observer + @for Ember + @param {String} propertyNames* + @param {Function} func + @return func + */ + function observer() { + var func = a_slice.call(arguments, -1)[0]; + var paths; + + var addWatchedProperty = function (path) { paths.push(path); }; + var _paths = a_slice.call(arguments, 0, -1); + + if (typeof func !== "function") { + // revert to old, soft-deprecated argument ordering + + func = arguments[0]; + _paths = a_slice.call(arguments, 1); + } + + paths = []; + + for (var i=0; i<_paths.length; ++i) { + expandProperties(_paths[i], addWatchedProperty); + } + + if (typeof func !== "function") { + throw new Ember.Error("Ember.observer called without a function"); + } + + func.__ember_observes__ = paths; + return func; + }; + + /** + Specify a method that observes property changes. + + ```javascript + Ember.Object.extend({ + valueObserver: Ember.immediateObserver('value', function() { + // Executes whenever the "value" property changes + }) + }); + ``` + + In the future, `Ember.observer` may become asynchronous. In this event, + `Ember.immediateObserver` will maintain the synchronous behavior. + + Also available as `Function.prototype.observesImmediately` if prototype extensions are + enabled. + + @method immediateObserver + @for Ember + @param {String} propertyNames* + @param {Function} func + @return func + */ + function immediateObserver() { + for (var i=0, l=arguments.length; i this.changingFrom ? 'green' : 'red'; + // logic + } + }), + + friendsDidChange: Ember.observer('friends.@each.name', function(obj, keyName) { + // some logic + // obj.get(keyName) returns friends array + }) + }); + ``` + + Also available as `Function.prototype.observesBefore` if prototype extensions are + enabled. + + @method beforeObserver + @for Ember + @param {String} propertyNames* + @param {Function} func + @return func + */ + function beforeObserver() { + var func = a_slice.call(arguments, -1)[0]; + var paths; + + var addWatchedProperty = function(path) { paths.push(path); }; + + var _paths = a_slice.call(arguments, 0, -1); + + if (typeof func !== "function") { + // revert to old, soft-deprecated argument ordering + + func = arguments[0]; + _paths = a_slice.call(arguments, 1); + } + + paths = []; + + for (var i=0; i<_paths.length; ++i) { + expandProperties(_paths[i], addWatchedProperty); + } + + if (typeof func !== "function") { + throw new Ember.Error("Ember.beforeObserver called without a function"); + } + + func.__ember_observesBefore__ = paths; + return func; + }; + + __exports__.IS_BINDING = IS_BINDING; + __exports__.mixin = mixin; + __exports__.Mixin = Mixin; + __exports__.required = required; + __exports__.aliasMethod = aliasMethod; + __exports__.observer = observer; + __exports__.immediateObserver = immediateObserver; + __exports__.beforeObserver = beforeObserver; + }); +define("ember-metal/observer", + ["ember-metal/watching","ember-metal/array","ember-metal/events","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __exports__) { + "use strict"; + var watch = __dependency1__.watch; + var unwatch = __dependency1__.unwatch; + var map = __dependency2__.map; + var listenersFor = __dependency3__.listenersFor; + var addListener = __dependency3__.addListener; + var removeListener = __dependency3__.removeListener; + var suspendListeners = __dependency3__.suspendListeners; + var suspendListener = __dependency3__.suspendListener; + /** + @module ember-metal + */ + + var AFTER_OBSERVERS = ':change', + BEFORE_OBSERVERS = ':before'; + + function changeEvent(keyName) { + return keyName+AFTER_OBSERVERS; + } + + function beforeEvent(keyName) { + return keyName+BEFORE_OBSERVERS; + } + + /** + @method addObserver + @for Ember + @param obj + @param {String} path + @param {Object|Function} targetOrMethod + @param {Function|String} [method] + */ + function addObserver(obj, _path, target, method) { + addListener(obj, changeEvent(_path), target, method); + watch(obj, _path); + + return this; + }; + + function observersFor(obj, path) { + return listenersFor(obj, changeEvent(path)); + }; + + /** + @method removeObserver + @for Ember + @param obj + @param {String} path + @param {Object|Function} targetOrMethod + @param {Function|String} [method] + */ + function removeObserver(obj, _path, target, method) { + unwatch(obj, _path); + removeListener(obj, changeEvent(_path), target, method); + + return this; + }; + + /** + @method addBeforeObserver + @for Ember + @param obj + @param {String} path + @param {Object|Function} targetOrMethod + @param {Function|String} [method] + */ + function addBeforeObserver(obj, _path, target, method) { + addListener(obj, beforeEvent(_path), target, method); + watch(obj, _path); + + return this; + }; + + // Suspend observer during callback. + // + // This should only be used by the target of the observer + // while it is setting the observed path. + function _suspendBeforeObserver(obj, path, target, method, callback) { + return suspendListener(obj, beforeEvent(path), target, method, callback); + }; + + function _suspendObserver(obj, path, target, method, callback) { + return suspendListener(obj, changeEvent(path), target, method, callback); + }; + + function _suspendBeforeObservers(obj, paths, target, method, callback) { + var events = map.call(paths, beforeEvent); + return suspendListeners(obj, events, target, method, callback); + }; + + function _suspendObservers(obj, paths, target, method, callback) { + var events = map.call(paths, changeEvent); + return suspendListeners(obj, events, target, method, callback); + }; + + function beforeObserversFor(obj, path) { + return listenersFor(obj, beforeEvent(path)); + }; + + /** + @method removeBeforeObserver + @for Ember + @param obj + @param {String} path + @param {Object|Function} targetOrMethod + @param {Function|String} [method] + */ + function removeBeforeObserver(obj, _path, target, method) { + unwatch(obj, _path); + removeListener(obj, beforeEvent(_path), target, method); + + return this; + }; + + __exports__.addObserver = addObserver; + __exports__.observersFor = observersFor; + __exports__.removeObserver = removeObserver; + __exports__.addBeforeObserver = addBeforeObserver; + __exports__._suspendBeforeObserver = _suspendBeforeObserver; + __exports__._suspendObserver = _suspendObserver; + __exports__._suspendBeforeObservers = _suspendBeforeObservers; + __exports__._suspendObservers = _suspendObservers; + __exports__.beforeObserversFor = beforeObserversFor; + __exports__.removeBeforeObserver = removeBeforeObserver; + }); +define("ember-metal/observer_set", + ["ember-metal/utils","ember-metal/events","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + var guidFor = __dependency1__.guidFor; + var sendEvent = __dependency2__.sendEvent; + + /* + this.observerSet = { + [senderGuid]: { // variable name: `keySet` + [keyName]: listIndex + } + }, + this.observers = [ + { + sender: obj, + keyName: keyName, + eventName: eventName, + listeners: [ + [target, method, flags] + ] + }, + ... + ] + */ + function ObserverSet() { + this.clear(); + }; + + ObserverSet.prototype.add = function(sender, keyName, eventName) { + var observerSet = this.observerSet, + observers = this.observers, + senderGuid = guidFor(sender), + keySet = observerSet[senderGuid], + index; + + if (!keySet) { + observerSet[senderGuid] = keySet = {}; + } + index = keySet[keyName]; + if (index === undefined) { + index = observers.push({ + sender: sender, + keyName: keyName, + eventName: eventName, + listeners: [] + }) - 1; + keySet[keyName] = index; + } + return observers[index].listeners; + }; + + ObserverSet.prototype.flush = function() { + var observers = this.observers, i, len, observer, sender; + this.clear(); + for (i=0, len=observers.length; i < len; ++i) { + observer = observers[i]; + sender = observer.sender; + if (sender.isDestroying || sender.isDestroyed) { continue; } + sendEvent(sender, observer.eventName, [sender, observer.keyName], observer.listeners); + } + }; + + ObserverSet.prototype.clear = function() { + this.observerSet = {}; + this.observers = []; + }; + + __exports__["default"] = ObserverSet; + }); +define("ember-metal/platform", + ["ember-metal/core","exports"], + function(__dependency1__, __exports__) { + "use strict"; + /*globals Node */ + + var Ember = __dependency1__["default"]; + + /** + @module ember-metal + */ + + /** + Platform specific methods and feature detectors needed by the framework. + + @class platform + @namespace Ember + @static + */ + // TODO remove this + var platform = {}; + + /** + Identical to `Object.create()`. Implements if not available natively. + + @method create + @for Ember + */ + var create = Object.create; + + // IE8 has Object.create but it couldn't treat property descriptors. + if (create) { + if (create({a: 1}, {a: {value: 2}}).a !== 2) { + create = null; + } + } + + // STUB_OBJECT_CREATE allows us to override other libraries that stub + // Object.create different than we would prefer + if (!create || Ember.ENV.STUB_OBJECT_CREATE) { + var K = function() {}; + + create = function(obj, props) { + K.prototype = obj; + obj = new K(); + if (props) { + K.prototype = obj; + for (var prop in props) { + K.prototype[prop] = props[prop].value; + } + obj = new K(); + } + K.prototype = null; + + return obj; + }; + + create.isSimulated = true; + } + + var defineProperty = Object.defineProperty; + var canRedefineProperties, canDefinePropertyOnDOM; + + // Catch IE8 where Object.defineProperty exists but only works on DOM elements + if (defineProperty) { + try { + defineProperty({}, 'a',{get:function() {}}); + } catch (e) { + defineProperty = null; + } + } + + if (defineProperty) { + // Detects a bug in Android <3.2 where you cannot redefine a property using + // Object.defineProperty once accessors have already been set. + canRedefineProperties = (function() { + var obj = {}; + + defineProperty(obj, 'a', { + configurable: true, + enumerable: true, + get: function() { }, + set: function() { } + }); + + defineProperty(obj, 'a', { + configurable: true, + enumerable: true, + writable: true, + value: true + }); + + return obj.a === true; + })(); + + // This is for Safari 5.0, which supports Object.defineProperty, but not + // on DOM nodes. + canDefinePropertyOnDOM = (function() { + try { + defineProperty(document.createElement('div'), 'definePropertyOnDOM', {}); + return true; + } catch(e) { } + + return false; + })(); + + if (!canRedefineProperties) { + defineProperty = null; + } else if (!canDefinePropertyOnDOM) { + defineProperty = function(obj, keyName, desc) { + var isNode; + + if (typeof Node === "object") { + isNode = obj instanceof Node; + } else { + isNode = typeof obj === "object" && typeof obj.nodeType === "number" && typeof obj.nodeName === "string"; + } + + if (isNode) { + // TODO: Should we have a warning here? + return (obj[keyName] = desc.value); + } else { + return Object.defineProperty(obj, keyName, desc); + } + }; + } + } + + /** + @class platform + @namespace Ember + */ + + /** + Identical to `Object.defineProperty()`. Implements as much functionality + as possible if not available natively. + + @method defineProperty + @param {Object} obj The object to modify + @param {String} keyName property name to modify + @param {Object} desc descriptor hash + @return {void} + */ + platform.defineProperty = defineProperty; + + /** + Set to true if the platform supports native getters and setters. + + @property hasPropertyAccessors + @final + */ + platform.hasPropertyAccessors = true; + + if (!platform.defineProperty) { + platform.hasPropertyAccessors = false; + + platform.defineProperty = function(obj, keyName, desc) { + if (!desc.get) { obj[keyName] = desc.value; } + }; + + platform.defineProperty.isSimulated = true; + } + + if (Ember.ENV.MANDATORY_SETTER && !platform.hasPropertyAccessors) { + Ember.ENV.MANDATORY_SETTER = false; + } + + __exports__.create = create; + __exports__.platform = platform; + }); +define("ember-metal/properties", + ["ember-metal/core","ember-metal/utils","ember-metal/platform","ember-metal/property_events","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __exports__) { + "use strict"; + /** + @module ember-metal + */ + + var Ember = __dependency1__["default"]; + var META_KEY = __dependency2__.META_KEY; + var meta = __dependency2__.meta; + var platform = __dependency3__.platform; + var overrideChains = __dependency4__.overrideChains; + var metaFor = meta, + objectDefineProperty = platform.defineProperty; + + var MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER; + + // .......................................................... + // DESCRIPTOR + // + + /** + Objects of this type can implement an interface to respond to requests to + get and set. The default implementation handles simple properties. + + You generally won't need to create or subclass this directly. + + @class Descriptor + @namespace Ember + @private + @constructor + */ + function Descriptor() {}; + + // .......................................................... + // DEFINING PROPERTIES API + // + + var MANDATORY_SETTER_FUNCTION = Ember.MANDATORY_SETTER_FUNCTION = function(value) { + Ember.assert("You must use Ember.set() to access this property (of " + this + ")", false); + }; + + var DEFAULT_GETTER_FUNCTION = Ember.DEFAULT_GETTER_FUNCTION = function(name) { + return function() { + var meta = this[META_KEY]; + return meta && meta.values[name]; + }; + }; + + /** + NOTE: This is a low-level method used by other parts of the API. You almost + never want to call this method directly. Instead you should use + `Ember.mixin()` to define new properties. + + Defines a property on an object. This method works much like the ES5 + `Object.defineProperty()` method except that it can also accept computed + properties and other special descriptors. + + Normally this method takes only three parameters. However if you pass an + instance of `Ember.Descriptor` as the third param then you can pass an + optional value as the fourth parameter. This is often more efficient than + creating new descriptor hashes for each property. + + ## Examples + + ```javascript + // ES5 compatible mode + Ember.defineProperty(contact, 'firstName', { + writable: true, + configurable: false, + enumerable: true, + value: 'Charles' + }); + + // define a simple property + Ember.defineProperty(contact, 'lastName', undefined, 'Jolley'); + + // define a computed property + Ember.defineProperty(contact, 'fullName', Ember.computed(function() { + return this.firstName+' '+this.lastName; + }).property('firstName', 'lastName')); + ``` + + @private + @method defineProperty + @for Ember + @param {Object} obj the object to define this property on. This may be a prototype. + @param {String} keyName the name of the property + @param {Ember.Descriptor} [desc] an instance of `Ember.Descriptor` (typically a + computed property) or an ES5 descriptor. + You must provide this or `data` but not both. + @param {*} [data] something other than a descriptor, that will + become the explicit value of this property. + */ + function defineProperty(obj, keyName, desc, data, meta) { + var descs, existingDesc, watching, value; + + if (!meta) meta = metaFor(obj); + descs = meta.descs; + existingDesc = meta.descs[keyName]; + watching = meta.watching[keyName] > 0; + + if (existingDesc instanceof Descriptor) { + existingDesc.teardown(obj, keyName); + } + + if (desc instanceof Descriptor) { + value = desc; + + descs[keyName] = desc; + if (MANDATORY_SETTER && watching) { + objectDefineProperty(obj, keyName, { + configurable: true, + enumerable: true, + writable: true, + value: undefined // make enumerable + }); + } else { + obj[keyName] = undefined; // make enumerable + } + } else { + descs[keyName] = undefined; // shadow descriptor in proto + if (desc == null) { + value = data; + + if (MANDATORY_SETTER && watching) { + meta.values[keyName] = data; + objectDefineProperty(obj, keyName, { + configurable: true, + enumerable: true, + set: MANDATORY_SETTER_FUNCTION, + get: DEFAULT_GETTER_FUNCTION(keyName) + }); + } else { + obj[keyName] = data; + } + } else { + value = desc; + + // compatibility with ES5 + objectDefineProperty(obj, keyName, desc); + } + } + + // if key is being watched, override chains that + // were initialized with the prototype + if (watching) { overrideChains(obj, keyName, meta); } + + // The `value` passed to the `didDefineProperty` hook is + // either the descriptor or data, whichever was passed. + if (obj.didDefineProperty) { obj.didDefineProperty(obj, keyName, value); } + + return this; + }; + + __exports__.Descriptor = Descriptor; + __exports__.defineProperty = defineProperty; + }); +define("ember-metal/property_events", + ["ember-metal/utils","ember-metal/events","ember-metal/observer_set","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __exports__) { + "use strict"; + var META_KEY = __dependency1__.META_KEY; + var guidFor = __dependency1__.guidFor; + var tryFinally = __dependency1__.tryFinally; + var sendEvent = __dependency2__.sendEvent; + var listenersUnion = __dependency2__.listenersUnion; + var listenersDiff = __dependency2__.listenersDiff; + var ObserverSet = __dependency3__["default"]; + + var beforeObserverSet = new ObserverSet(), + observerSet = new ObserverSet(), + deferred = 0; + + // .......................................................... + // PROPERTY CHANGES + // + + /** + This function is called just before an object property is about to change. + It will notify any before observers and prepare caches among other things. + + Normally you will not need to call this method directly but if for some + reason you can't directly watch a property you can invoke this method + manually along with `Ember.propertyDidChange()` which you should call just + after the property value changes. + + @method propertyWillChange + @for Ember + @param {Object} obj The object with the property that will change + @param {String} keyName The property key (or path) that will change. + @return {void} + */ + function propertyWillChange(obj, keyName) { + var m = obj[META_KEY], + watching = (m && m.watching[keyName] > 0) || keyName === 'length', + proto = m && m.proto, + desc = m && m.descs[keyName]; + + if (!watching) { return; } + if (proto === obj) { return; } + if (desc && desc.willChange) { desc.willChange(obj, keyName); } + dependentKeysWillChange(obj, keyName, m); + chainsWillChange(obj, keyName, m); + notifyBeforeObservers(obj, keyName); + } + + /** + This function is called just after an object property has changed. + It will notify any observers and clear caches among other things. + + Normally you will not need to call this method directly but if for some + reason you can't directly watch a property you can invoke this method + manually along with `Ember.propertyWillChange()` which you should call just + before the property value changes. + + @method propertyDidChange + @for Ember + @param {Object} obj The object with the property that will change + @param {String} keyName The property key (or path) that will change. + @return {void} + */ + function propertyDidChange(obj, keyName) { + var m = obj[META_KEY], + watching = (m && m.watching[keyName] > 0) || keyName === 'length', + proto = m && m.proto, + desc = m && m.descs[keyName]; + + if (proto === obj) { return; } + + // shouldn't this mean that we're watching this key? + if (desc && desc.didChange) { desc.didChange(obj, keyName); } + if (!watching && keyName !== 'length') { return; } + + dependentKeysDidChange(obj, keyName, m); + chainsDidChange(obj, keyName, m, false); + notifyObservers(obj, keyName); + } + + var WILL_SEEN, DID_SEEN; + + // called whenever a property is about to change to clear the cache of any dependent keys (and notify those properties of changes, etc...) + function dependentKeysWillChange(obj, depKey, meta) { + if (obj.isDestroying) { return; } + + var seen = WILL_SEEN, top = !seen; + if (top) { seen = WILL_SEEN = {}; } + iterDeps(propertyWillChange, obj, depKey, seen, meta); + if (top) { WILL_SEEN = null; } + } + + // called whenever a property has just changed to update dependent keys + function dependentKeysDidChange(obj, depKey, meta) { + if (obj.isDestroying) { return; } + + var seen = DID_SEEN, top = !seen; + if (top) { seen = DID_SEEN = {}; } + iterDeps(propertyDidChange, obj, depKey, seen, meta); + if (top) { DID_SEEN = null; } + } + + function iterDeps(method, obj, depKey, seen, meta) { + var guid = guidFor(obj); + if (!seen[guid]) seen[guid] = {}; + if (seen[guid][depKey]) return; + seen[guid][depKey] = true; + + var deps = meta.deps; + deps = deps && deps[depKey]; + if (deps) { + for(var key in deps) { + var desc = meta.descs[key]; + if (desc && desc._suspended === obj) continue; + method(obj, key); + } + } + } + + function chainsWillChange(obj, keyName, m) { + if (!(m.hasOwnProperty('chainWatchers') && + m.chainWatchers[keyName])) { + return; + } + + var nodes = m.chainWatchers[keyName], + events = [], + i, l; + + for(i = 0, l = nodes.length; i < l; i++) { + nodes[i].willChange(events); + } + + for (i = 0, l = events.length; i < l; i += 2) { + propertyWillChange(events[i], events[i+1]); + } + } + + function chainsDidChange(obj, keyName, m, suppressEvents) { + if (!(m && m.hasOwnProperty('chainWatchers') && + m.chainWatchers[keyName])) { + return; + } + + var nodes = m.chainWatchers[keyName], + events = suppressEvents ? null : [], + i, l; + + for(i = 0, l = nodes.length; i < l; i++) { + nodes[i].didChange(events); + } + + if (suppressEvents) { + return; + } + + for (i = 0, l = events.length; i < l; i += 2) { + propertyDidChange(events[i], events[i+1]); + } + } + + function overrideChains(obj, keyName, m) { + chainsDidChange(obj, keyName, m, true); + }; + + /** + @method beginPropertyChanges + @chainable + @private + */ + function beginPropertyChanges() { + deferred++; + } + + /** + @method endPropertyChanges + @private + */ + function endPropertyChanges() { + deferred--; + if (deferred<=0) { + beforeObserverSet.clear(); + observerSet.flush(); + } + } + + /** + Make a series of property changes together in an + exception-safe way. + + ```javascript + Ember.changeProperties(function() { + obj1.set('foo', mayBlowUpWhenSet); + obj2.set('bar', baz); + }); + ``` + + @method changeProperties + @param {Function} callback + @param [binding] + */ + function changeProperties(cb, binding) { + beginPropertyChanges(); + tryFinally(cb, endPropertyChanges, binding); + }; + + function notifyBeforeObservers(obj, keyName) { + if (obj.isDestroying) { return; } + + var eventName = keyName + ':before', listeners, diff; + if (deferred) { + listeners = beforeObserverSet.add(obj, keyName, eventName); + diff = listenersDiff(obj, eventName, listeners); + sendEvent(obj, eventName, [obj, keyName], diff); + } else { + sendEvent(obj, eventName, [obj, keyName]); + } + } + + function notifyObservers(obj, keyName) { + if (obj.isDestroying) { return; } + + var eventName = keyName + ':change', listeners; + if (deferred) { + listeners = observerSet.add(obj, keyName, eventName); + listenersUnion(obj, eventName, listeners); + } else { + sendEvent(obj, eventName, [obj, keyName]); + } + } + + __exports__.propertyWillChange = propertyWillChange; + __exports__.propertyDidChange = propertyDidChange; + __exports__.overrideChains = overrideChains; + __exports__.beginPropertyChanges = beginPropertyChanges; + __exports__.endPropertyChanges = endPropertyChanges; + __exports__.changeProperties = changeProperties; + }); +define("ember-metal/property_get", + ["ember-metal/core","ember-metal/utils","ember-metal/error","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __exports__) { + "use strict"; + /** + @module ember-metal + */ + + var Ember = __dependency1__["default"]; + var META_KEY = __dependency2__.META_KEY; + var EmberError = __dependency3__["default"]; + + var get; + + var MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER; + + var IS_GLOBAL_PATH = /^([A-Z$]|([0-9][A-Z$])).*[\.]/; + var HAS_THIS = 'this.'; + var FIRST_KEY = /^([^\.]+)/; + + // .......................................................... + // GET AND SET + // + // If we are on a platform that supports accessors we can use those. + // Otherwise simulate accessors by looking up the property directly on the + // object. + + /** + Gets the value of a property on an object. If the property is computed, + the function will be invoked. If the property is not defined but the + object implements the `unknownProperty` method then that will be invoked. + + If you plan to run on IE8 and older browsers then you should use this + method anytime you want to retrieve a property on an object that you don't + know for sure is private. (Properties beginning with an underscore '_' + are considered private.) + + On all newer browsers, you only need to use this method to retrieve + properties if the property might not be defined on the object and you want + to respect the `unknownProperty` handler. Otherwise you can ignore this + method. + + Note that if the object itself is `undefined`, this method will throw + an error. + + @method get + @for Ember + @param {Object} obj The object to retrieve from. + @param {String} keyName The property key to retrieve + @return {Object} the property value or `null`. + */ + get = function get(obj, keyName) { + // Helpers that operate with 'this' within an #each + if (keyName === '') { + return obj; + } + + if (!keyName && 'string'===typeof obj) { + keyName = obj; + obj = null; + } + + Ember.assert("Cannot call get with "+ keyName +" key.", !!keyName); + Ember.assert("Cannot call get with '"+ keyName +"' on an undefined object.", obj !== undefined); + + if (obj === null) { return _getPath(obj, keyName); } + + var meta = obj[META_KEY], desc = meta && meta.descs[keyName], ret; + + if (desc === undefined && keyName.indexOf('.') !== -1) { + return _getPath(obj, keyName); + } + + if (desc) { + return desc.get(obj, keyName); + } else { + if (MANDATORY_SETTER && meta && meta.watching[keyName] > 0) { + ret = meta.values[keyName]; + } else { + ret = obj[keyName]; + } + + if (ret === undefined && + 'object' === typeof obj && !(keyName in obj) && 'function' === typeof obj.unknownProperty) { + return obj.unknownProperty(keyName); + } + + return ret; + } + }; + + // Currently used only by Ember Data tests + if (Ember.config.overrideAccessors) { + Ember.get = get; + Ember.config.overrideAccessors(); + get = Ember.get; + } + + /** + Normalizes a target/path pair to reflect that actual target/path that should + be observed, etc. This takes into account passing in global property + paths (i.e. a path beginning with a captial letter not defined on the + target). + + @private + @method normalizeTuple + @for Ember + @param {Object} target The current target. May be `null`. + @param {String} path A path on the target or a global property path. + @return {Array} a temporary array with the normalized target/path pair. + */ + function normalizeTuple(target, path) { + var hasThis = path.indexOf(HAS_THIS) === 0, + isGlobal = !hasThis && IS_GLOBAL_PATH.test(path), + key; + + if (!target || isGlobal) target = Ember.lookup; + if (hasThis) path = path.slice(5); + + if (target === Ember.lookup) { + key = path.match(FIRST_KEY)[0]; + target = get(target, key); + path = path.slice(key.length+1); + } + + // must return some kind of path to be valid else other things will break. + if (!path || path.length===0) throw new EmberError('Path cannot be empty'); + + return [ target, path ]; + }; + + function _getPath(root, path) { + var hasThis, parts, tuple, idx, len; + + // If there is no root and path is a key name, return that + // property from the global object. + // E.g. get('Ember') -> Ember + if (root === null && path.indexOf('.') === -1) { return get(Ember.lookup, path); } + + // detect complicated paths and normalize them + hasThis = path.indexOf(HAS_THIS) === 0; + + if (!root || hasThis) { + tuple = normalizeTuple(root, path); + root = tuple[0]; + path = tuple[1]; + tuple.length = 0; + } + + parts = path.split("."); + len = parts.length; + for (idx = 0; root != null && idx < len; idx++) { + root = get(root, parts[idx], true); + if (root && root.isDestroyed) { return undefined; } + } + return root; + }; + + function getWithDefault(root, key, defaultValue) { + var value = get(root, key); + + if (value === undefined) { return defaultValue; } + return value; + }; + + __exports__["default"] = get; + __exports__.get = get; + __exports__.getWithDefault = getWithDefault; + __exports__.normalizeTuple = normalizeTuple; + __exports__._getPath = _getPath; + }); +define("ember-metal/property_set", + ["ember-metal/core","ember-metal/property_get","ember-metal/utils","ember-metal/property_events","ember-metal/properties","ember-metal/error","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __exports__) { + "use strict"; + var Ember = __dependency1__["default"]; + var getPath = __dependency2__._getPath; + var META_KEY = __dependency3__.META_KEY; + var propertyWillChange = __dependency4__.propertyWillChange; + var propertyDidChange = __dependency4__.propertyDidChange; + var defineProperty = __dependency5__.defineProperty; + var EmberError = __dependency6__["default"]; + + var MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER, + IS_GLOBAL = /^([A-Z$]|([0-9][A-Z$]))/; + + /** + Sets the value of a property on an object, respecting computed properties + and notifying observers and other listeners of the change. If the + property is not defined but the object implements the `setUnknownProperty` + method then that will be invoked as well. + + @method set + @for Ember + @param {Object} obj The object to modify. + @param {String} keyName The property key to set + @param {Object} value The value to set + @return {Object} the passed value. + */ + var set = function set(obj, keyName, value, tolerant) { + if (typeof obj === 'string') { + Ember.assert("Path '" + obj + "' must be global if no obj is given.", IS_GLOBAL.test(obj)); + value = keyName; + keyName = obj; + obj = null; + } + + Ember.assert("Cannot call set with "+ keyName +" key.", !!keyName); + + if (!obj) { + return setPath(obj, keyName, value, tolerant); + } + + var meta = obj[META_KEY], desc = meta && meta.descs[keyName], + isUnknown, currentValue; + + if (desc === undefined && keyName.indexOf('.') !== -1) { + return setPath(obj, keyName, value, tolerant); + } + + Ember.assert("You need to provide an object and key to `set`.", !!obj && keyName !== undefined); + Ember.assert('calling set on destroyed object', !obj.isDestroyed); + + if (desc !== undefined) { + desc.set(obj, keyName, value); + } else { + + if (typeof obj === 'object' && obj !== null && value !== undefined && obj[keyName] === value) { + return value; + } + + isUnknown = 'object' === typeof obj && !(keyName in obj); + + // setUnknownProperty is called if `obj` is an object, + // the property does not already exist, and the + // `setUnknownProperty` method exists on the object + if (isUnknown && 'function' === typeof obj.setUnknownProperty) { + obj.setUnknownProperty(keyName, value); + } else if (meta && meta.watching[keyName] > 0) { + if (MANDATORY_SETTER) { + currentValue = meta.values[keyName]; + } else { + currentValue = obj[keyName]; + } + // only trigger a change if the value has changed + if (value !== currentValue) { + propertyWillChange(obj, keyName); + if (MANDATORY_SETTER) { + if ((currentValue === undefined && !(keyName in obj)) || !obj.propertyIsEnumerable(keyName)) { + defineProperty(obj, keyName, null, value); // setup mandatory setter + } else { + meta.values[keyName] = value; + } + } else { + obj[keyName] = value; + } + propertyDidChange(obj, keyName); + } + } else { + obj[keyName] = value; + } + } + return value; + }; + + // Currently used only by Ember Data tests + // ES6TODO: Verify still true + if (Ember.config.overrideAccessors) { + Ember.set = set; + Ember.config.overrideAccessors(); + set = Ember.set; + } + + function setPath(root, path, value, tolerant) { + var keyName; + + // get the last part of the path + keyName = path.slice(path.lastIndexOf('.') + 1); + + // get the first part of the part + path = (path === keyName) ? keyName : path.slice(0, path.length-(keyName.length+1)); + + // unless the path is this, look up the first part to + // get the root + if (path !== 'this') { + root = getPath(root, path); + } + + if (!keyName || keyName.length === 0) { + throw new EmberError('Property set failed: You passed an empty path'); + } + + if (!root) { + if (tolerant) { return; } + else { throw new EmberError('Property set failed: object in path "'+path+'" could not be found or was destroyed.'); } + } + + return set(root, keyName, value); + } + + /** + Error-tolerant form of `Ember.set`. Will not blow up if any part of the + chain is `undefined`, `null`, or destroyed. + + This is primarily used when syncing bindings, which may try to update after + an object has been destroyed. + + @method trySet + @for Ember + @param {Object} obj The object to modify. + @param {String} path The property path to set + @param {Object} value The value to set + */ + function trySet(root, path, value) { + return set(root, path, value, true); + }; + + __exports__.set = set; + __exports__.trySet = trySet; + }); +define("ember-metal/run_loop", + ["ember-metal/core","ember-metal/utils","ember-metal/array","ember-metal/property_events","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __exports__) { + "use strict"; + var Ember = __dependency1__["default"]; + var apply = __dependency2__.apply; + var indexOf = __dependency3__.indexOf; + var beginPropertyChanges = __dependency4__.beginPropertyChanges; + var endPropertyChanges = __dependency4__.endPropertyChanges; + + var onBegin = function(current) { + run.currentRunLoop = current; + }; + + var onEnd = function(current, next) { + run.currentRunLoop = next; + }; + + // ES6TODO: should Backburner become es6? + var Backburner = requireModule('backburner').Backburner, + backburner = new Backburner(['sync', 'actions', 'destroy'], { + sync: { + before: beginPropertyChanges, + after: endPropertyChanges + }, + defaultQueue: 'actions', + onBegin: onBegin, + onEnd: onEnd, + onErrorTarget: Ember, + onErrorMethod: 'onerror' + }), + slice = [].slice, + concat = [].concat; + + // .......................................................... + // run - this is ideally the only public API the dev sees + // + + /** + Runs the passed target and method inside of a RunLoop, ensuring any + deferred actions including bindings and views updates are flushed at the + end. + + Normally you should not need to invoke this method yourself. However if + you are implementing raw event handlers when interfacing with other + libraries or plugins, you should probably wrap all of your code inside this + call. + + ```javascript + run(function() { + // code to be execute within a RunLoop + }); + ``` + + @class run + @namespace Ember + @static + @constructor + @param {Object} [target] target of method to call + @param {Function|String} method Method to invoke. + May be a function or a string. If you pass a string + then it will be looked up on the passed target. + @param {Object} [args*] Any additional arguments you wish to pass to the method. + @return {Object} return value from invoking the passed function. + */ + var run = function() { + return apply(backburner, backburner.run, arguments); + }; + + /** + If no run-loop is present, it creates a new one. If a run loop is + present it will queue itself to run on the existing run-loops action + queue. + + Please note: This is not for normal usage, and should be used sparingly. + + If invoked when not within a run loop: + + ```javascript + run.join(function() { + // creates a new run-loop + }); + ``` + + Alternatively, if called within an existing run loop: + + ```javascript + run(function() { + // creates a new run-loop + run.join(function() { + // joins with the existing run-loop, and queues for invocation on + // the existing run-loops action queue. + }); + }); + ``` + + @method join + @namespace Ember + @param {Object} [target] target of method to call + @param {Function|String} method Method to invoke. + May be a function or a string. If you pass a string + then it will be looked up on the passed target. + @param {Object} [args*] Any additional arguments you wish to pass to the method. + @return {Object} Return value from invoking the passed function. Please note, + when called within an existing loop, no return value is possible. + */ + run.join = function(target, method /* args */) { + if (!run.currentRunLoop) { + return apply(Ember, run, arguments); + } + + var args = slice.call(arguments); + args.unshift('actions'); + apply(run, run.schedule, args); + }; + + /** + Provides a useful utility for when integrating with non-Ember libraries + that provide asynchronous callbacks. + + Ember utilizes a run-loop to batch and coalesce changes. This works by + marking the start and end of Ember-related Javascript execution. + + When using events such as a View's click handler, Ember wraps the event + handler in a run-loop, but when integrating with non-Ember libraries this + can be tedious. + + For example, the following is rather verbose but is the correct way to combine + third-party events and Ember code. + + ```javascript + var that = this; + jQuery(window).on('resize', function(){ + run(function(){ + that.handleResize(); + }); + }); + ``` + + To reduce the boilerplate, the following can be used to construct a + run-loop-wrapped callback handler. + + ```javascript + jQuery(window).on('resize', run.bind(this, this.handleResize)); + ``` + + @method bind + @namespace run + @param {Object} [target] target of method to call + @param {Function|String} method Method to invoke. + May be a function or a string. If you pass a string + then it will be looked up on the passed target. + @param {Object} [args*] Any additional arguments you wish to pass to the method. + @return {Object} return value from invoking the passed function. Please note, + when called within an existing loop, no return value is possible. + @since 1.4.0 + */ + run.bind = function(target, method /* args*/) { + var args = slice.call(arguments); + return function() { + return apply(run, run.join, args.concat(slice.call(arguments))); + }; + }; + + run.backburner = backburner; + run.currentRunLoop = null; + run.queues = backburner.queueNames; + + /** + Begins a new RunLoop. Any deferred actions invoked after the begin will + be buffered until you invoke a matching call to `run.end()`. This is + a lower-level way to use a RunLoop instead of using `run()`. + + ```javascript + run.begin(); + // code to be execute within a RunLoop + run.end(); + ``` + + @method begin + @return {void} + */ + run.begin = function() { + backburner.begin(); + }; + + /** + Ends a RunLoop. This must be called sometime after you call + `run.begin()` to flush any deferred actions. This is a lower-level way + to use a RunLoop instead of using `run()`. + + ```javascript + run.begin(); + // code to be execute within a RunLoop + run.end(); + ``` + + @method end + @return {void} + */ + run.end = function() { + backburner.end(); + }; + + /** + Array of named queues. This array determines the order in which queues + are flushed at the end of the RunLoop. You can define your own queues by + simply adding the queue name to this array. Normally you should not need + to inspect or modify this property. + + @property queues + @type Array + @default ['sync', 'actions', 'destroy'] + */ + + /** + Adds the passed target/method and any optional arguments to the named + queue to be executed at the end of the RunLoop. If you have not already + started a RunLoop when calling this method one will be started for you + automatically. + + At the end of a RunLoop, any methods scheduled in this way will be invoked. + Methods will be invoked in an order matching the named queues defined in + the `run.queues` property. + + ```javascript + run.schedule('sync', this, function() { + // this will be executed in the first RunLoop queue, when bindings are synced + console.log("scheduled on sync queue"); + }); + + run.schedule('actions', this, function() { + // this will be executed in the 'actions' queue, after bindings have synced. + console.log("scheduled on actions queue"); + }); + + // Note the functions will be run in order based on the run queues order. + // Output would be: + // scheduled on sync queue + // scheduled on actions queue + ``` + + @method schedule + @param {String} queue The name of the queue to schedule against. + Default queues are 'sync' and 'actions' + @param {Object} [target] target object to use as the context when invoking a method. + @param {String|Function} method The method to invoke. If you pass a string it + will be resolved on the target object at the time the scheduled item is + invoked allowing you to change the target function. + @param {Object} [arguments*] Optional arguments to be passed to the queued method. + @return {void} + */ + run.schedule = function(queue, target, method) { + checkAutoRun(); + apply(backburner, backburner.schedule, arguments); + }; + + // Used by global test teardown + run.hasScheduledTimers = function() { + return backburner.hasTimers(); + }; + + // Used by global test teardown + run.cancelTimers = function () { + backburner.cancelTimers(); + }; + + /** + Immediately flushes any events scheduled in the 'sync' queue. Bindings + use this queue so this method is a useful way to immediately force all + bindings in the application to sync. + + You should call this method anytime you need any changed state to propagate + throughout the app immediately without repainting the UI (which happens + in the later 'render' queue added by the `ember-views` package). + + ```javascript + run.sync(); + ``` + + @method sync + @return {void} + */ + run.sync = function() { + if (backburner.currentInstance) { + backburner.currentInstance.queues.sync.flush(); + } + }; + + /** + Invokes the passed target/method and optional arguments after a specified + period if time. The last parameter of this method must always be a number + of milliseconds. + + You should use this method whenever you need to run some action after a + period of time instead of using `setTimeout()`. This method will ensure that + items that expire during the same script execution cycle all execute + together, which is often more efficient than using a real setTimeout. + + ```javascript + run.later(myContext, function() { + // code here will execute within a RunLoop in about 500ms with this == myContext + }, 500); + ``` + + @method later + @param {Object} [target] target of method to invoke + @param {Function|String} method The method to invoke. + If you pass a string it will be resolved on the + target at the time the method is invoked. + @param {Object} [args*] Optional arguments to pass to the timeout. + @param {Number} wait Number of milliseconds to wait. + @return {String} a string you can use to cancel the timer in + `run.cancel` later. + */ + run.later = function(target, method) { + return apply(backburner, backburner.later, arguments); + }; + + /** + Schedule a function to run one time during the current RunLoop. This is equivalent + to calling `scheduleOnce` with the "actions" queue. + + @method once + @param {Object} [target] The target of the method to invoke. + @param {Function|String} method The method to invoke. + If you pass a string it will be resolved on the + target at the time the method is invoked. + @param {Object} [args*] Optional arguments to pass to the timeout. + @return {Object} Timer information for use in cancelling, see `run.cancel`. + */ + run.once = function(target, method) { + checkAutoRun(); + var args = slice.call(arguments); + args.unshift('actions'); + return apply(backburner, backburner.scheduleOnce, args); + }; + + /** + Schedules a function to run one time in a given queue of the current RunLoop. + Calling this method with the same queue/target/method combination will have + no effect (past the initial call). + + Note that although you can pass optional arguments these will not be + considered when looking for duplicates. New arguments will replace previous + calls. + + ```javascript + run(function() { + var sayHi = function() { console.log('hi'); } + run.scheduleOnce('afterRender', myContext, sayHi); + run.scheduleOnce('afterRender', myContext, sayHi); + // sayHi will only be executed once, in the afterRender queue of the RunLoop + }); + ``` + + Also note that passing an anonymous function to `run.scheduleOnce` will + not prevent additional calls with an identical anonymous function from + scheduling the items multiple times, e.g.: + + ```javascript + function scheduleIt() { + run.scheduleOnce('actions', myContext, function() { console.log("Closure"); }); + } + scheduleIt(); + scheduleIt(); + // "Closure" will print twice, even though we're using `run.scheduleOnce`, + // because the function we pass to it is anonymous and won't match the + // previously scheduled operation. + ``` + + Available queues, and their order, can be found at `run.queues` + + @method scheduleOnce + @param {String} [queue] The name of the queue to schedule against. Default queues are 'sync' and 'actions'. + @param {Object} [target] The target of the method to invoke. + @param {Function|String} method The method to invoke. + If you pass a string it will be resolved on the + target at the time the method is invoked. + @param {Object} [args*] Optional arguments to pass to the timeout. + @return {Object} Timer information for use in cancelling, see `run.cancel`. + */ + run.scheduleOnce = function(queue, target, method) { + checkAutoRun(); + return apply(backburner, backburner.scheduleOnce, arguments); + }; + + /** + Schedules an item to run from within a separate run loop, after + control has been returned to the system. This is equivalent to calling + `run.later` with a wait time of 1ms. + + ```javascript + run.next(myContext, function() { + // code to be executed in the next run loop, + // which will be scheduled after the current one + }); + ``` + + Multiple operations scheduled with `run.next` will coalesce + into the same later run loop, along with any other operations + scheduled by `run.later` that expire right around the same + time that `run.next` operations will fire. + + Note that there are often alternatives to using `run.next`. + For instance, if you'd like to schedule an operation to happen + after all DOM element operations have completed within the current + run loop, you can make use of the `afterRender` run loop queue (added + by the `ember-views` package, along with the preceding `render` queue + where all the DOM element operations happen). Example: + + ```javascript + App.MyCollectionView = Ember.CollectionView.extend({ + didInsertElement: function() { + run.scheduleOnce('afterRender', this, 'processChildElements'); + }, + processChildElements: function() { + // ... do something with collectionView's child view + // elements after they've finished rendering, which + // can't be done within the CollectionView's + // `didInsertElement` hook because that gets run + // before the child elements have been added to the DOM. + } + }); + ``` + + One benefit of the above approach compared to using `run.next` is + that you will be able to perform DOM/CSS operations before unprocessed + elements are rendered to the screen, which may prevent flickering or + other artifacts caused by delaying processing until after rendering. + + The other major benefit to the above approach is that `run.next` + introduces an element of non-determinism, which can make things much + harder to test, due to its reliance on `setTimeout`; it's much harder + to guarantee the order of scheduled operations when they are scheduled + outside of the current run loop, i.e. with `run.next`. + + @method next + @param {Object} [target] target of method to invoke + @param {Function|String} method The method to invoke. + If you pass a string it will be resolved on the + target at the time the method is invoked. + @param {Object} [args*] Optional arguments to pass to the timeout. + @return {Object} Timer information for use in cancelling, see `run.cancel`. + */ + run.next = function() { + var args = slice.call(arguments); + args.push(1); + return apply(backburner, backburner.later, args); + }; + + /** + Cancels a scheduled item. Must be a value returned by `run.later()`, + `run.once()`, `run.next()`, `run.debounce()`, or + `run.throttle()`. + + ```javascript + var runNext = run.next(myContext, function() { + // will not be executed + }); + run.cancel(runNext); + + var runLater = run.later(myContext, function() { + // will not be executed + }, 500); + run.cancel(runLater); + + var runOnce = run.once(myContext, function() { + // will not be executed + }); + run.cancel(runOnce); + + var throttle = run.throttle(myContext, function() { + // will not be executed + }, 1, false); + run.cancel(throttle); + + var debounce = run.debounce(myContext, function() { + // will not be executed + }, 1); + run.cancel(debounce); + + var debounceImmediate = run.debounce(myContext, function() { + // will be executed since we passed in true (immediate) + }, 100, true); + // the 100ms delay until this method can be called again will be cancelled + run.cancel(debounceImmediate); + ``` + + @method cancel + @param {Object} timer Timer object to cancel + @return {Boolean} true if cancelled or false/undefined if it wasn't found + */ + run.cancel = function(timer) { + return backburner.cancel(timer); + }; + + /** + Delay calling the target method until the debounce period has elapsed + with no additional debounce calls. If `debounce` is called again before + the specified time has elapsed, the timer is reset and the entire period + must pass again before the target method is called. + + This method should be used when an event may be called multiple times + but the action should only be called once when the event is done firing. + A common example is for scroll events where you only want updates to + happen once scrolling has ceased. + + ```javascript + var myFunc = function() { console.log(this.name + ' ran.'); }; + var myContext = {name: 'debounce'}; + + run.debounce(myContext, myFunc, 150); + + // less than 150ms passes + + run.debounce(myContext, myFunc, 150); + + // 150ms passes + // myFunc is invoked with context myContext + // console logs 'debounce ran.' one time. + ``` + + Immediate allows you to run the function immediately, but debounce + other calls for this function until the wait time has elapsed. If + `debounce` is called again before the specified time has elapsed, + the timer is reset and the entire period must pass again before + the method can be called again. + + ```javascript + var myFunc = function() { console.log(this.name + ' ran.'); }; + var myContext = {name: 'debounce'}; + + run.debounce(myContext, myFunc, 150, true); + + // console logs 'debounce ran.' one time immediately. + // 100ms passes + + run.debounce(myContext, myFunc, 150, true); + + // 150ms passes and nothing else is logged to the console and + // the debouncee is no longer being watched + + run.debounce(myContext, myFunc, 150, true); + + // console logs 'debounce ran.' one time immediately. + // 150ms passes and nothing else is logged to the console and + // the debouncee is no longer being watched + + ``` + + @method debounce + @param {Object} [target] target of method to invoke + @param {Function|String} method The method to invoke. + May be a function or a string. If you pass a string + then it will be looked up on the passed target. + @param {Object} [args*] Optional arguments to pass to the timeout. + @param {Number} wait Number of milliseconds to wait. + @param {Boolean} immediate Trigger the function on the leading instead + of the trailing edge of the wait interval. Defaults to false. + @return {Array} Timer information for use in cancelling, see `run.cancel`. + */ + run.debounce = function() { + return apply(backburner, backburner.debounce, arguments); + }; + + /** + Ensure that the target method is never called more frequently than + the specified spacing period. The target method is called immediately. + + ```javascript + var myFunc = function() { console.log(this.name + ' ran.'); }; + var myContext = {name: 'throttle'}; + + run.throttle(myContext, myFunc, 150); + // myFunc is invoked with context myContext + // console logs 'throttle ran.' + + // 50ms passes + run.throttle(myContext, myFunc, 150); + + // 50ms passes + run.throttle(myContext, myFunc, 150); + + // 150ms passes + run.throttle(myContext, myFunc, 150); + // myFunc is invoked with context myContext + // console logs 'throttle ran.' + ``` + + @method throttle + @param {Object} [target] target of method to invoke + @param {Function|String} method The method to invoke. + May be a function or a string. If you pass a string + then it will be looked up on the passed target. + @param {Object} [args*] Optional arguments to pass to the timeout. + @param {Number} spacing Number of milliseconds to space out requests. + @param {Boolean} immediate Trigger the function on the leading instead + of the trailing edge of the wait interval. Defaults to true. + @return {Array} Timer information for use in cancelling, see `run.cancel`. + */ + run.throttle = function() { + return apply(backburner, backburner.throttle, arguments); + }; + + // Make sure it's not an autorun during testing + function checkAutoRun() { + if (!run.currentRunLoop) { + Ember.assert("You have turned on testing mode, which disabled the run-loop's autorun. You will need to wrap any code with asynchronous side-effects in an run", !Ember.testing); + } + } + + /** + Add a new named queue after the specified queue. + + The queue to add will only be added once. + + @method _addQueue + @param {String} name the name of the queue to add. + @param {String} after the name of the queue to add after. + @private + */ + run._addQueue = function(name, after) { + if (indexOf.call(run.queues, name) === -1) { + run.queues.splice(indexOf.call(run.queues, after)+1, 0, name); + } + } + + __exports__["default"] = run + }); +define("ember-metal/set_properties", + ["ember-metal/property_events","ember-metal/property_set","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + var changeProperties = __dependency1__.changeProperties; + var set = __dependency2__.set; + + /** + Set a list of properties on an object. These properties are set inside + a single `beginPropertyChanges` and `endPropertyChanges` batch, so + observers will be buffered. + + ```javascript + var anObject = Ember.Object.create(); + + anObject.setProperties({ + firstName: 'Stanley', + lastName: 'Stuart', + age: 21 + }); + ``` + + @method setProperties + @param self + @param {Object} hash + @return self + */ + function setProperties(self, hash) { + changeProperties(function() { + for(var prop in hash) { + if (hash.hasOwnProperty(prop)) { set(self, prop, hash[prop]); } + } + }); + return self; + }; + + __exports__["default"] = setProperties; + }); +define("ember-metal/utils", + ["ember-metal/core","ember-metal/platform","ember-metal/array","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __exports__) { + "use strict"; + var Ember = __dependency1__["default"]; + var platform = __dependency2__.platform; + var create = __dependency2__.create; + var forEach = __dependency3__.forEach; + + /** + @module ember-metal + */ + + /** + Prefix used for guids through out Ember. + @private + @property GUID_PREFIX + @for Ember + @type String + @final + */ + var GUID_PREFIX = 'ember'; + + + var o_defineProperty = platform.defineProperty, + o_create = create, + // Used for guid generation... + numberCache = [], + stringCache = {}, + uuid = 0; + + var MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER; + + /** + A unique key used to assign guids and other private metadata to objects. + If you inspect an object in your browser debugger you will often see these. + They can be safely ignored. + + On browsers that support it, these properties are added with enumeration + disabled so they won't show up when you iterate over your properties. + + @private + @property GUID_KEY + @for Ember + @type String + @final + */ + var GUID_KEY = '__ember' + (+ new Date()); + + var GUID_DESC = { + writable: false, + configurable: false, + enumerable: false, + value: null + }; + + /** + Generates a new guid, optionally saving the guid to the object that you + pass in. You will rarely need to use this method. Instead you should + call `Ember.guidFor(obj)`, which return an existing guid if available. + + @private + @method generateGuid + @for Ember + @param {Object} [obj] Object the guid will be used for. If passed in, the guid will + be saved on the object and reused whenever you pass the same object + again. + + If no object is passed, just generate a new guid. + @param {String} [prefix] Prefix to place in front of the guid. Useful when you want to + separate the guid into separate namespaces. + @return {String} the guid + */ + function generateGuid(obj, prefix) { + if (!prefix) prefix = GUID_PREFIX; + var ret = (prefix + (uuid++)); + if (obj) { + if (obj[GUID_KEY] === null) { + obj[GUID_KEY] = ret; + } else { + GUID_DESC.value = ret; + o_defineProperty(obj, GUID_KEY, GUID_DESC); + } + } + return ret; + } + + /** + Returns a unique id for the object. If the object does not yet have a guid, + one will be assigned to it. You can call this on any object, + `Ember.Object`-based or not, but be aware that it will add a `_guid` + property. + + You can also use this method on DOM Element objects. + + @private + @method guidFor + @for Ember + @param {Object} obj any object, string, number, Element, or primitive + @return {String} the unique guid for this instance. + */ + function guidFor(obj) { + + // special cases where we don't want to add a key to object + if (obj === undefined) return "(undefined)"; + if (obj === null) return "(null)"; + + var ret; + var type = typeof obj; + + // Don't allow prototype changes to String etc. to change the guidFor + switch(type) { + case 'number': + ret = numberCache[obj]; + if (!ret) ret = numberCache[obj] = 'nu'+obj; + return ret; + + case 'string': + ret = stringCache[obj]; + if (!ret) ret = stringCache[obj] = 'st'+(uuid++); + return ret; + + case 'boolean': + return obj ? '(true)' : '(false)'; + + default: + if (obj[GUID_KEY]) return obj[GUID_KEY]; + if (obj === Object) return '(Object)'; + if (obj === Array) return '(Array)'; + ret = 'ember' + (uuid++); + + if (obj[GUID_KEY] === null) { + obj[GUID_KEY] = ret; + } else { + GUID_DESC.value = ret; + o_defineProperty(obj, GUID_KEY, GUID_DESC); + } + return ret; + } + }; + + // .......................................................... + // META + // + + var META_DESC = { + writable: true, + configurable: false, + enumerable: false, + value: null + }; + + + /** + The key used to store meta information on object for property observing. + + @property META_KEY + @for Ember + @private + @final + @type String + */ + var META_KEY = '__ember_meta__'; + + var isDefinePropertySimulated = platform.defineProperty.isSimulated; + + function Meta(obj) { + this.descs = {}; + this.watching = {}; + this.cache = {}; + this.cacheMeta = {}; + this.source = obj; + } + + Meta.prototype = { + descs: null, + deps: null, + watching: null, + listeners: null, + cache: null, + cacheMeta: null, + source: null, + mixins: null, + bindings: null, + chains: null, + chainWatchers: null, + values: null, + proto: null + }; + + if (isDefinePropertySimulated) { + // on platforms that don't support enumerable false + // make meta fail jQuery.isPlainObject() to hide from + // jQuery.extend() by having a property that fails + // hasOwnProperty check. + Meta.prototype.__preventPlainObject__ = true; + + // Without non-enumerable properties, meta objects will be output in JSON + // unless explicitly suppressed + Meta.prototype.toJSON = function () { }; + } + + // Placeholder for non-writable metas. + var EMPTY_META = new Meta(null); + + if (MANDATORY_SETTER) { EMPTY_META.values = {}; } + + /** + Retrieves the meta hash for an object. If `writable` is true ensures the + hash is writable for this object as well. + + The meta object contains information about computed property descriptors as + well as any watched properties and other information. You generally will + not access this information directly but instead work with higher level + methods that manipulate this hash indirectly. + + @method meta + @for Ember + @private + + @param {Object} obj The object to retrieve meta for + @param {Boolean} [writable=true] Pass `false` if you do not intend to modify + the meta hash, allowing the method to avoid making an unnecessary copy. + @return {Object} the meta hash for an object + */ + function meta(obj, writable) { + + var ret = obj[META_KEY]; + if (writable===false) return ret || EMPTY_META; + + if (!ret) { + if (!isDefinePropertySimulated) o_defineProperty(obj, META_KEY, META_DESC); + + ret = new Meta(obj); + + if (MANDATORY_SETTER) { ret.values = {}; } + + obj[META_KEY] = ret; + + // make sure we don't accidentally try to create constructor like desc + ret.descs.constructor = null; + + } else if (ret.source !== obj) { + if (!isDefinePropertySimulated) o_defineProperty(obj, META_KEY, META_DESC); + + ret = o_create(ret); + ret.descs = o_create(ret.descs); + ret.watching = o_create(ret.watching); + ret.cache = {}; + ret.cacheMeta = {}; + ret.source = obj; + + if (MANDATORY_SETTER) { ret.values = o_create(ret.values); } + + obj[META_KEY] = ret; + } + return ret; + }; + + function getMeta(obj, property) { + var _meta = meta(obj, false); + return _meta[property]; + }; + + function setMeta(obj, property, value) { + var _meta = meta(obj, true); + _meta[property] = value; + return value; + }; + + /** + @deprecated + @private + + In order to store defaults for a class, a prototype may need to create + a default meta object, which will be inherited by any objects instantiated + from the class's constructor. + + However, the properties of that meta object are only shallow-cloned, + so if a property is a hash (like the event system's `listeners` hash), + it will by default be shared across all instances of that class. + + This method allows extensions to deeply clone a series of nested hashes or + other complex objects. For instance, the event system might pass + `['listeners', 'foo:change', 'ember157']` to `prepareMetaPath`, which will + walk down the keys provided. + + For each key, if the key does not exist, it is created. If it already + exists and it was inherited from its constructor, the constructor's + key is cloned. + + You can also pass false for `writable`, which will simply return + undefined if `prepareMetaPath` discovers any part of the path that + shared or undefined. + + @method metaPath + @for Ember + @param {Object} obj The object whose meta we are examining + @param {Array} path An array of keys to walk down + @param {Boolean} writable whether or not to create a new meta + (or meta property) if one does not already exist or if it's + shared with its constructor + */ + function metaPath(obj, path, writable) { + Ember.deprecate("Ember.metaPath is deprecated and will be removed from future releases."); + var _meta = meta(obj, writable), keyName, value; + + for (var i=0, l=path.length; i 3 ? slice.call(arguments, 3) : undefined; + if (!this.currentInstance) { createAutorun(this); } + return this.currentInstance.schedule(queueName, target, method, args, false, stack); + }, + + deferOnce: function(queueName, target, method /* , args */) { + if (!method) { + method = target; + target = null; + } + + if (isString(method)) { + method = target[method]; + } + + var stack = this.DEBUG ? new Error() : undefined, + args = arguments.length > 3 ? slice.call(arguments, 3) : undefined; + if (!this.currentInstance) { createAutorun(this); } + return this.currentInstance.schedule(queueName, target, method, args, true, stack); + }, + + setTimeout: function() { + var args = slice.call(arguments), + length = args.length, + method, wait, target, + methodOrTarget, methodOrWait, methodOrArgs; + + if (length === 0) { + return; + } else if (length === 1) { + method = args.shift(); + wait = 0; + } else if (length === 2) { + methodOrTarget = args[0]; + methodOrWait = args[1]; + + if (isFunction(methodOrWait) || isFunction(methodOrTarget[methodOrWait])) { + target = args.shift(); + method = args.shift(); + wait = 0; + } else if (isCoercableNumber(methodOrWait)) { + method = args.shift(); + wait = args.shift(); + } else { + method = args.shift(); + wait = 0; + } + } else { + var last = args[args.length - 1]; + + if (isCoercableNumber(last)) { + wait = args.pop(); + } else { + wait = 0; + } + + methodOrTarget = args[0]; + methodOrArgs = args[1]; + + if (isFunction(methodOrArgs) || (isString(methodOrArgs) && + methodOrTarget !== null && + methodOrArgs in methodOrTarget)) { + target = args.shift(); + method = args.shift(); + } else { + method = args.shift(); + } + } + + var executeAt = (+new Date()) + parseInt(wait, 10); + + if (isString(method)) { + method = target[method]; + } + + var onError = getOnError(this.options); + + function fn() { + if (onError) { + try { + method.apply(target, args); + } catch (e) { + onError(e); + } + } else { + method.apply(target, args); + } + } + + // find position to insert + var i = searchTimer(executeAt, timers); + + timers.splice(i, 0, executeAt, fn); + + updateLaterTimer(this, executeAt, wait); + + return fn; + }, + + throttle: function(target, method /* , args, wait, [immediate] */) { + var self = this, + args = arguments, + immediate = pop.call(args), + wait, + throttler, + index, + timer; + + if (isNumber(immediate) || isString(immediate)) { + wait = immediate; + immediate = true; + } else { + wait = pop.call(args); + } + + wait = parseInt(wait, 10); + + index = findThrottler(target, method, this._throttlers); + if (index > -1) { return this._throttlers[index]; } // throttled + + timer = global.setTimeout(function() { + if (!immediate) { + self.run.apply(self, args); + } + var index = findThrottler(target, method, self._throttlers); + if (index > -1) { + self._throttlers.splice(index, 1); + } + }, wait); + + if (immediate) { + self.run.apply(self, args); + } + + throttler = [target, method, timer]; + + this._throttlers.push(throttler); + + return throttler; + }, + + debounce: function(target, method /* , args, wait, [immediate] */) { + var self = this, + args = arguments, + immediate = pop.call(args), + wait, + index, + debouncee, + timer; + + if (isNumber(immediate) || isString(immediate)) { + wait = immediate; + immediate = false; + } else { + wait = pop.call(args); + } + + wait = parseInt(wait, 10); + // Remove debouncee + index = findDebouncee(target, method, this._debouncees); + + if (index > -1) { + debouncee = this._debouncees[index]; + this._debouncees.splice(index, 1); + clearTimeout(debouncee[2]); + } + + timer = global.setTimeout(function() { + if (!immediate) { + self.run.apply(self, args); + } + var index = findDebouncee(target, method, self._debouncees); + if (index > -1) { + self._debouncees.splice(index, 1); + } + }, wait); + + if (immediate && index === -1) { + self.run.apply(self, args); + } + + debouncee = [target, method, timer]; + + self._debouncees.push(debouncee); + + return debouncee; + }, + + cancelTimers: function() { + var clearItems = function(item) { + clearTimeout(item[2]); + }; + + each(this._throttlers, clearItems); + this._throttlers = []; + + each(this._debouncees, clearItems); + this._debouncees = []; + + if (this._laterTimer) { + clearTimeout(this._laterTimer); + this._laterTimer = null; + } + timers = []; + + if (this._autorun) { + clearTimeout(this._autorun); + this._autorun = null; + } + }, + + hasTimers: function() { + return !!timers.length || !!this._debouncees.length || !!this._throttlers.length || this._autorun; + }, + + cancel: function(timer) { + var timerType = typeof timer; + + if (timer && timerType === 'object' && timer.queue && timer.method) { // we're cancelling a deferOnce + return timer.queue.cancel(timer); + } else if (timerType === 'function') { // we're cancelling a setTimeout + for (var i = 0, l = timers.length; i < l; i += 2) { + if (timers[i + 1] === timer) { + timers.splice(i, 2); // remove the two elements + return true; + } + } + } else if (Object.prototype.toString.call(timer) === "[object Array]"){ // we're cancelling a throttle or debounce + return this._cancelItem(findThrottler, this._throttlers, timer) || + this._cancelItem(findDebouncee, this._debouncees, timer); + } else { + return; // timer was null or not a timer + } + }, + + _cancelItem: function(findMethod, array, timer){ + var item, + index; + + if (timer.length < 3) { return false; } + + index = findMethod(timer[0], timer[1], array); + + if(index > -1) { + + item = array[index]; + + if(item[2] === timer[2]){ + array.splice(index, 1); + clearTimeout(timer[2]); + return true; + } + } + + return false; + } + }; + + Backburner.prototype.schedule = Backburner.prototype.defer; + Backburner.prototype.scheduleOnce = Backburner.prototype.deferOnce; + Backburner.prototype.later = Backburner.prototype.setTimeout; + + if (needsIETryCatchFix) { + var originalRun = Backburner.prototype.run; + Backburner.prototype.run = wrapInTryCatch(originalRun); + + var originalEnd = Backburner.prototype.end; + Backburner.prototype.end = wrapInTryCatch(originalEnd); + } + + function wrapInTryCatch(func) { + return function () { + try { + return func.apply(this, arguments); + } catch (e) { + throw e; + } + }; + } + + function getOnError(options) { + return options.onError || (options.onErrorTarget && options.onErrorTarget[options.onErrorMethod]); + } + + + function createAutorun(backburner) { + backburner.begin(); + backburner._autorun = global.setTimeout(function() { + backburner._autorun = null; + backburner.end(); + }); + } + + function updateLaterTimer(self, executeAt, wait) { + if (!self._laterTimer || executeAt < self._laterTimerExpiresAt) { + self._laterTimer = global.setTimeout(function() { + self._laterTimer = null; + self._laterTimerExpiresAt = null; + executeTimers(self); + }, wait); + self._laterTimerExpiresAt = executeAt; + } + } + + function executeTimers(self) { + var now = +new Date(), + time, fns, i, l; + + self.run(function() { + i = searchTimer(now, timers); + + fns = timers.splice(0, i); + + for (i = 1, l = fns.length; i < l; i += 2) { + self.schedule(self.options.defaultQueue, null, fns[i]); + } + }); + + if (timers.length) { + updateLaterTimer(self, timers[0], timers[0] - now); + } + } + + function findDebouncee(target, method, debouncees) { + return findItem(target, method, debouncees); + } + + function findThrottler(target, method, throttlers) { + return findItem(target, method, throttlers); + } + + function findItem(target, method, collection) { + var item, + index = -1; + + for (var i = 0, l = collection.length; i < l; i++) { + item = collection[i]; + if (item[0] === target && item[1] === method) { + index = i; + break; + } + } + + return index; + } + + function searchTimer(time, timers) { + var start = 0, + end = timers.length - 2, + middle, l; + + while (start < end) { + // since timers is an array of pairs 'l' will always + // be an integer + l = (end - start) / 2; + + // compensate for the index in case even number + // of pairs inside timers + middle = start + l - (l % 2); + + if (time >= timers[middle]) { + start = middle + 2; + } else { + end = middle; + } + } + + return (time >= timers[start]) ? start + 2 : start; + } + + __exports__.Backburner = Backburner; + }); +define("backburner/deferred_action_queues", + ["backburner/utils","backburner/queue","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + var Utils = __dependency1__["default"]; + var Queue = __dependency2__.Queue; + + var each = Utils.each, + isString = Utils.isString; + + function DeferredActionQueues(queueNames, options) { + var queues = this.queues = {}; + this.queueNames = queueNames = queueNames || []; + + this.options = options; + + each(queueNames, function(queueName) { + queues[queueName] = new Queue(this, queueName, options); + }); + } + + DeferredActionQueues.prototype = { + queueNames: null, + queues: null, + options: null, + + schedule: function(queueName, target, method, args, onceFlag, stack) { + var queues = this.queues, + queue = queues[queueName]; + + if (!queue) { throw new Error("You attempted to schedule an action in a queue (" + queueName + ") that doesn't exist"); } + + if (onceFlag) { + return queue.pushUnique(target, method, args, stack); + } else { + return queue.push(target, method, args, stack); + } + }, + + invoke: function(target, method, args, _) { + if (args && args.length > 0) { + method.apply(target, args); + } else { + method.call(target); + } + }, + + invokeWithOnError: function(target, method, args, onError) { + try { + if (args && args.length > 0) { + method.apply(target, args); + } else { + method.call(target); + } + } catch(error) { + onError(error); + } + }, + + flush: function() { + var queues = this.queues, + queueNames = this.queueNames, + queueName, queue, queueItems, priorQueueNameIndex, + queueNameIndex = 0, numberOfQueues = queueNames.length, + options = this.options, + onError = options.onError || (options.onErrorTarget && options.onErrorTarget[options.onErrorMethod]), + invoke = onError ? this.invokeWithOnError : this.invoke; + + outerloop: + while (queueNameIndex < numberOfQueues) { + queueName = queueNames[queueNameIndex]; + queue = queues[queueName]; + queueItems = queue._queueBeingFlushed = queue._queue.slice(); + queue._queue = []; + + var queueOptions = queue.options, // TODO: write a test for this + before = queueOptions && queueOptions.before, + after = queueOptions && queueOptions.after, + target, method, args, stack, + queueIndex = 0, numberOfQueueItems = queueItems.length; + + if (numberOfQueueItems && before) { before(); } + + while (queueIndex < numberOfQueueItems) { + target = queueItems[queueIndex]; + method = queueItems[queueIndex+1]; + args = queueItems[queueIndex+2]; + stack = queueItems[queueIndex+3]; // Debugging assistance + + if (isString(method)) { method = target[method]; } + + // method could have been nullified / canceled during flush + if (method) { + invoke(target, method, args, onError); + } + + queueIndex += 4; + } + + queue._queueBeingFlushed = null; + if (numberOfQueueItems && after) { after(); } + + if ((priorQueueNameIndex = indexOfPriorQueueWithActions(this, queueNameIndex)) !== -1) { + queueNameIndex = priorQueueNameIndex; + continue outerloop; + } + + queueNameIndex++; + } + } + }; + + function indexOfPriorQueueWithActions(daq, currentQueueIndex) { + var queueName, queue; + + for (var i = 0, l = currentQueueIndex; i <= l; i++) { + queueName = daq.queueNames[i]; + queue = daq.queues[queueName]; + if (queue._queue.length) { return i; } + } + + return -1; + } + + __exports__.DeferredActionQueues = DeferredActionQueues; + }); +define("backburner/queue", + ["exports"], + function(__exports__) { + "use strict"; + function Queue(daq, name, options) { + this.daq = daq; + this.name = name; + this.globalOptions = options; + this.options = options[name]; + this._queue = []; + } + + Queue.prototype = { + daq: null, + name: null, + options: null, + onError: null, + _queue: null, + + push: function(target, method, args, stack) { + var queue = this._queue; + queue.push(target, method, args, stack); + return {queue: this, target: target, method: method}; + }, + + pushUnique: function(target, method, args, stack) { + var queue = this._queue, currentTarget, currentMethod, i, l; + + for (i = 0, l = queue.length; i < l; i += 4) { + currentTarget = queue[i]; + currentMethod = queue[i+1]; + + if (currentTarget === target && currentMethod === method) { + queue[i+2] = args; // replace args + queue[i+3] = stack; // replace stack + return {queue: this, target: target, method: method}; + } + } + + queue.push(target, method, args, stack); + return {queue: this, target: target, method: method}; + }, + + // TODO: remove me, only being used for Ember.run.sync + flush: function() { + var queue = this._queue, + globalOptions = this.globalOptions, + options = this.options, + before = options && options.before, + after = options && options.after, + onError = globalOptions.onError || (globalOptions.onErrorTarget && globalOptions.onErrorTarget[globalOptions.onErrorMethod]), + target, method, args, stack, i, l = queue.length; + + if (l && before) { before(); } + for (i = 0; i < l; i += 4) { + target = queue[i]; + method = queue[i+1]; + args = queue[i+2]; + stack = queue[i+3]; // Debugging assistance + + // TODO: error handling + if (args && args.length > 0) { + if (onError) { + try { + method.apply(target, args); + } catch (e) { + onError(e); + } + } else { + method.apply(target, args); + } + } else { + if (onError) { + try { + method.call(target); + } catch(e) { + onError(e); + } + } else { + method.call(target); + } + } + } + if (l && after) { after(); } + + // check if new items have been added + if (queue.length > l) { + this._queue = queue.slice(l); + this.flush(); + } else { + this._queue.length = 0; + } + }, + + cancel: function(actionToCancel) { + var queue = this._queue, currentTarget, currentMethod, i, l; + + for (i = 0, l = queue.length; i < l; i += 4) { + currentTarget = queue[i]; + currentMethod = queue[i+1]; + + if (currentTarget === actionToCancel.target && currentMethod === actionToCancel.method) { + queue.splice(i, 4); + return true; + } + } + + // if not found in current queue + // could be in the queue that is being flushed + queue = this._queueBeingFlushed; + if (!queue) { + return; + } + for (i = 0, l = queue.length; i < l; i += 4) { + currentTarget = queue[i]; + currentMethod = queue[i+1]; + + if (currentTarget === actionToCancel.target && currentMethod === actionToCancel.method) { + // don't mess with array during flush + // just nullify the method + queue[i+1] = null; + return true; + } + } + } + }; + + __exports__.Queue = Queue; + }); +define("backburner/utils", + ["exports"], + function(__exports__) { + "use strict"; + __exports__["default"] = { + each: function(collection, callback) { + for (var i = 0; i < collection.length; i++) { + callback(collection[i]); + } + }, + + isString: function(suspect) { + return typeof suspect === 'string'; + }, + + isFunction: function(suspect) { + return typeof suspect === 'function'; + }, + + isNumber: function(suspect) { + return typeof suspect === 'number'; + } + }; + }); + +define("ember-metal/watch_key", + ["ember-metal/core","ember-metal/utils","ember-metal/platform","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __exports__) { + "use strict"; + var Ember = __dependency1__["default"]; + var meta = __dependency2__.meta; + var typeOf = __dependency2__.typeOf; + var platform = __dependency3__.platform; + + var metaFor = meta, // utils.js + MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER, + o_defineProperty = platform.defineProperty; + + function watchKey(obj, keyName, meta) { + // can't watch length on Array - it is special... + if (keyName === 'length' && typeOf(obj) === 'array') { return; } + + var m = meta || metaFor(obj), watching = m.watching; + + // activate watching first time + if (!watching[keyName]) { + watching[keyName] = 1; + + if ('function' === typeof obj.willWatchProperty) { + obj.willWatchProperty(keyName); + } + + if (MANDATORY_SETTER && keyName in obj) { + m.values[keyName] = obj[keyName]; + o_defineProperty(obj, keyName, { + configurable: true, + enumerable: obj.propertyIsEnumerable(keyName), + set: Ember.MANDATORY_SETTER_FUNCTION, + get: Ember.DEFAULT_GETTER_FUNCTION(keyName) + }); + } + } else { + watching[keyName] = (watching[keyName] || 0) + 1; + } + }; + + function unwatchKey(obj, keyName, meta) { + var m = meta || metaFor(obj), watching = m.watching; + + if (watching[keyName] === 1) { + watching[keyName] = 0; + + if ('function' === typeof obj.didUnwatchProperty) { + obj.didUnwatchProperty(keyName); + } + + if (MANDATORY_SETTER && keyName in obj) { + o_defineProperty(obj, keyName, { + configurable: true, + enumerable: obj.propertyIsEnumerable(keyName), + set: function(val) { + // redefine to set as enumerable + o_defineProperty(obj, keyName, { + configurable: true, + writable: true, + enumerable: true, + value: val + }); + delete m.values[keyName]; + }, + get: Ember.DEFAULT_GETTER_FUNCTION(keyName) + }); + } + } else if (watching[keyName] > 1) { + watching[keyName]--; + } + }; + + __exports__.watchKey = watchKey; + __exports__.unwatchKey = unwatchKey; + }); +define("ember-metal/watch_path", + ["ember-metal/utils","ember-metal/chains","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + var meta = __dependency1__.meta; + var typeOf = __dependency1__.typeOf; + var ChainNode = __dependency2__.ChainNode; + + var metaFor = meta; + + // get the chains for the current object. If the current object has + // chains inherited from the proto they will be cloned and reconfigured for + // the current object. + function chainsFor(obj, meta) { + var m = meta || metaFor(obj), ret = m.chains; + if (!ret) { + ret = m.chains = new ChainNode(null, null, obj); + } else if (ret.value() !== obj) { + ret = m.chains = ret.copy(obj); + } + return ret; + } + + function watchPath(obj, keyPath, meta) { + // can't watch length on Array - it is special... + if (keyPath === 'length' && typeOf(obj) === 'array') { return; } + + var m = meta || metaFor(obj), watching = m.watching; + + if (!watching[keyPath]) { // activate watching first time + watching[keyPath] = 1; + chainsFor(obj, m).add(keyPath); + } else { + watching[keyPath] = (watching[keyPath] || 0) + 1; + } + }; + + function unwatchPath(obj, keyPath, meta) { + var m = meta || metaFor(obj), watching = m.watching; + + if (watching[keyPath] === 1) { + watching[keyPath] = 0; + chainsFor(obj, m).remove(keyPath); + } else if (watching[keyPath] > 1) { + watching[keyPath]--; + } + }; + + __exports__.watchPath = watchPath; + __exports__.unwatchPath = unwatchPath; + }); +define("ember-metal/watching", + ["ember-metal/utils","ember-metal/chains","ember-metal/watch_key","ember-metal/watch_path","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __exports__) { + "use strict"; + /** + @module ember-metal + */ + + var meta = __dependency1__.meta; + var META_KEY = __dependency1__.META_KEY; + var GUID_KEY = __dependency1__.GUID_KEY; + var typeOf = __dependency1__.typeOf; + var generateGuid = __dependency1__.generateGuid; + var removeChainWatcher = __dependency2__.removeChainWatcher; + var flushPendingChains = __dependency2__.flushPendingChains; + var watchKey = __dependency3__.watchKey; + var unwatchKey = __dependency3__.unwatchKey; + var watchPath = __dependency4__.watchPath; + var unwatchPath = __dependency4__.unwatchPath; + + var metaFor = meta; // utils.js + + // returns true if the passed path is just a keyName + function isKeyName(path) { + return path.indexOf('.') === -1; + } + + /** + Starts watching a property on an object. Whenever the property changes, + invokes `Ember.propertyWillChange` and `Ember.propertyDidChange`. This is the + primitive used by observers and dependent keys; usually you will never call + this method directly but instead use higher level methods like + `Ember.addObserver()` + + @private + @method watch + @for Ember + @param obj + @param {String} keyName + */ + function watch(obj, _keyPath, m) { + // can't watch length on Array - it is special... + if (_keyPath === 'length' && typeOf(obj) === 'array') { return; } + + if (isKeyName(_keyPath)) { + watchKey(obj, _keyPath, m); + } else { + watchPath(obj, _keyPath, m); + } + }; + + function isWatching(obj, key) { + var meta = obj[META_KEY]; + return (meta && meta.watching[key]) > 0; + }; + + watch.flushPending = flushPendingChains; + + function unwatch(obj, _keyPath, m) { + // can't watch length on Array - it is special... + if (_keyPath === 'length' && typeOf(obj) === 'array') { return; } + + if (isKeyName(_keyPath)) { + unwatchKey(obj, _keyPath, m); + } else { + unwatchPath(obj, _keyPath, m); + } + }; + + /** + Call on an object when you first beget it from another object. This will + setup any chained watchers on the object instance as needed. This method is + safe to call multiple times. + + @private + @method rewatch + @for Ember + @param obj + */ + function rewatch(obj) { + var m = obj[META_KEY], chains = m && m.chains; + + // make sure the object has its own guid. + if (GUID_KEY in obj && !obj.hasOwnProperty(GUID_KEY)) { + generateGuid(obj); + } + + // make sure any chained watchers update. + if (chains && chains.value() !== obj) { + m.chains = chains.copy(obj); + } + }; + + var NODE_STACK = []; + + /** + Tears down the meta on an object so that it can be garbage collected. + Multiple calls will have no effect. + + @method destroy + @for Ember + @param {Object} obj the object to destroy + @return {void} + */ + function destroy(obj) { + var meta = obj[META_KEY], node, nodes, key, nodeObject; + if (meta) { + obj[META_KEY] = null; + // remove chainWatchers to remove circular references that would prevent GC + node = meta.chains; + if (node) { + NODE_STACK.push(node); + // process tree + while (NODE_STACK.length > 0) { + node = NODE_STACK.pop(); + // push children + nodes = node._chains; + if (nodes) { + for (key in nodes) { + if (nodes.hasOwnProperty(key)) { + NODE_STACK.push(nodes[key]); + } + } + } + // remove chainWatcher in node object + if (node._watching) { + nodeObject = node._object; + if (nodeObject) { + removeChainWatcher(nodeObject, node._key, node); + } + } + } + } + } + }; + + __exports__.watch = watch; + __exports__.isWatching = isWatching; + __exports__.unwatch = unwatch; + __exports__.rewatch = rewatch; + __exports__.destroy = destroy; + }); +})(); + +(function() { +/** + @class RSVP + @module RSVP + */ +define("rsvp/all", + ["./promise","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Promise = __dependency1__["default"]; + + /** + This is a convenient alias for `RSVP.Promise.all`. + + @method all + @for RSVP + @param {Array} array Array of promises. + @param {String} label An optional label. This is useful + for tooling. + @static + */ + __exports__["default"] = function all(array, label) { + return Promise.all(array, label); + }; + }); +define("rsvp/all_settled", + ["./promise","./utils","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + var Promise = __dependency1__["default"]; + var isArray = __dependency2__.isArray; + var isNonThenable = __dependency2__.isNonThenable; + + /** + `RSVP.allSettled` is similar to `RSVP.all`, but instead of implementing + a fail-fast method, it waits until all the promises have returned and + shows you all the results. This is useful if you want to handle multiple + promises' failure states together as a set. + + Returns a promise that is fulfilled when all the given promises have been + settled. The return promise is fulfilled with an array of the states of + the promises passed into the `promises` array argument. + + Each state object will either indicate fulfillment or rejection, and + provide the corresponding value or reason. The states will take one of + the following formats: + + ```javascript + { state: 'fulfilled', value: value } + or + { state: 'rejected', reason: reason } + ``` + + Example: + + ```javascript + var promise1 = RSVP.Promise.resolve(1); + var promise2 = RSVP.Promise.reject(new Error('2')); + var promise3 = RSVP.Promise.reject(new Error('3')); + var promises = [ promise1, promise2, promise3 ]; + + RSVP.allSettled(promises).then(function(array){ + // array == [ + // { state: 'fulfilled', value: 1 }, + // { state: 'rejected', reason: Error }, + // { state: 'rejected', reason: Error } + // ] + // Note that for the second item, reason.message will be "2", and for the + // third item, reason.message will be "3". + }, function(error) { + // Not run. (This block would only be called if allSettled had failed, + // for instance if passed an incorrect argument type.) + }); + ``` + + @method allSettled + @for RSVP + @param {Array} promises + @param {String} label - optional string that describes the promise. + Useful for tooling. + @return {Promise} promise that is fulfilled with an array of the settled + states of the constituent promises. + @static + */ + + __exports__["default"] = function allSettled(entries, label) { + return new Promise(function(resolve, reject) { + if (!isArray(entries)) { + throw new TypeError('You must pass an array to allSettled.'); + } + + var remaining = entries.length; + var entry; + + if (remaining === 0) { + resolve([]); + return; + } + + var results = new Array(remaining); + + function fulfilledResolver(index) { + return function(value) { + resolveAll(index, fulfilled(value)); + }; + } + + function rejectedResolver(index) { + return function(reason) { + resolveAll(index, rejected(reason)); + }; + } + + function resolveAll(index, value) { + results[index] = value; + if (--remaining === 0) { + resolve(results); + } + } + + for (var index = 0; index < entries.length; index++) { + entry = entries[index]; + + if (isNonThenable(entry)) { + resolveAll(index, fulfilled(entry)); + } else { + Promise.cast(entry).then(fulfilledResolver(index), rejectedResolver(index)); + } + } + }, label); + }; + + function fulfilled(value) { + return { state: 'fulfilled', value: value }; + } + + function rejected(reason) { + return { state: 'rejected', reason: reason }; + } + }); +define("rsvp/config", + ["./events","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var EventTarget = __dependency1__["default"]; + + var config = { + instrument: false + }; + + EventTarget.mixin(config); + + function configure(name, value) { + if (name === 'onerror') { + // handle for legacy users that expect the actual + // error to be passed to their function added via + // `RSVP.configure('onerror', someFunctionHere);` + config.on('error', value); + return; + } + + if (arguments.length === 2) { + config[name] = value; + } else { + return config[name]; + } + } + + __exports__.config = config; + __exports__.configure = configure; + }); +define("rsvp/defer", + ["./promise","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Promise = __dependency1__["default"]; + + /** + `RSVP.defer` returns an object similar to jQuery's `$.Deferred`. + `RSVP.defer` should be used when porting over code reliant on `$.Deferred`'s + interface. New code should use the `RSVP.Promise` constructor instead. + + The object returned from `RSVP.defer` is a plain object with three properties: + + * promise - an `RSVP.Promise`. + * reject - a function that causes the `promise` property on this object to + become rejected + * resolve - a function that causes the `promise` property on this object to + become fulfilled. + + Example: + + ```javascript + var deferred = RSVP.defer(); + + deferred.resolve("Success!"); + + deferred.promise.then(function(value){ + // value here is "Success!" + }); + ``` + + @method defer + @for RSVP + @param {String} label optional string for labeling the promise. + Useful for tooling. + @return {Object} + */ + + __exports__["default"] = function defer(label) { + var deferred = { }; + + deferred.promise = new Promise(function(resolve, reject) { + deferred.resolve = resolve; + deferred.reject = reject; + }, label); + + return deferred; + }; + }); +define("rsvp/events", + ["exports"], + function(__exports__) { + "use strict"; + var indexOf = function(callbacks, callback) { + for (var i=0, l=callbacks.length; i 1; + }; + + RSVP.filter(promises, filterFn).then(function(result){ + // result is [ 2, 3 ] + }); + ``` + + If any of the `promises` given to `RSVP.filter` are rejected, the first promise + that is rejected will be given as an argument to the returned promise's + rejection handler. For example: + + ```javascript + var promise1 = RSVP.resolve(1); + var promise2 = RSVP.reject(new Error("2")); + var promise3 = RSVP.reject(new Error("3")); + var promises = [ promise1, promise2, promise3 ]; + + var filterFn = function(item){ + return item > 1; + }; + + RSVP.filter(promises, filterFn).then(function(array){ + // Code here never runs because there are rejected promises! + }, function(reason) { + // reason.message === "2" + }); + ``` + + `RSVP.filter` will also wait for any promises returned from `filterFn`. + For instance, you may want to fetch a list of users then return a subset + of those users based on some asynchronous operation: + + ```javascript + + var alice = { name: 'alice' }; + var bob = { name: 'bob' }; + var users = [ alice, bob ]; + + var promises = users.map(function(user){ + return RSVP.resolve(user); + }); + + var filterFn = function(user){ + // Here, Alice has permissions to create a blog post, but Bob does not. + return getPrivilegesForUser(user).then(function(privs){ + return privs.can_create_blog_post === true; + }); + }; + RSVP.filter(promises, filterFn).then(function(users){ + // true, because the server told us only Alice can create a blog post. + users.length === 1; + // false, because Alice is the only user present in `users` + users[0] === bob; + }); + ``` + + @method filter + @for RSVP + @param {Array} promises + @param {Function} filterFn - function to be called on each resolved value to + filter the final results. + @param {String} label optional string describing the promise. Useful for + tooling. + @return {Promise} + */ + function filter(promises, filterFn, label) { + return all(promises, label).then(function(values){ + if (!isArray(promises)) { + throw new TypeError('You must pass an array to filter.'); + } + + if (!isFunction(filterFn)){ + throw new TypeError("You must pass a function to filter's second argument."); + } + + return map(promises, filterFn, label).then(function(filterResults){ + var i, + valuesLen = values.length, + filtered = []; + + for (i = 0; i < valuesLen; i++){ + if(filterResults[i]) filtered.push(values[i]); + } + return filtered; + }); + }); + } + + __exports__["default"] = filter; + }); +define("rsvp/hash", + ["./promise","./utils","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + var Promise = __dependency1__["default"]; + var isNonThenable = __dependency2__.isNonThenable; + var keysOf = __dependency2__.keysOf; + + /** + `RSVP.hash` is similar to `RSVP.all`, but takes an object instead of an array + for its `promises` argument. + + Returns a promise that is fulfilled when all the given promises have been + fulfilled, or rejected if any of them become rejected. The returned promise + is fulfilled with a hash that has the same key names as the `promises` object + argument. If any of the values in the object are not promises, they will + simply be copied over to the fulfilled object. + + Example: + + ```javascript + var promises = { + myPromise: RSVP.resolve(1), + yourPromise: RSVP.resolve(2), + theirPromise: RSVP.resolve(3), + notAPromise: 4 + }; + + RSVP.hash(promises).then(function(hash){ + // hash here is an object that looks like: + // { + // myPromise: 1, + // yourPromise: 2, + // theirPromise: 3, + // notAPromise: 4 + // } + }); + ```` + + If any of the `promises` given to `RSVP.hash` are rejected, the first promise + that is rejected will be given as the reason to the rejection handler. + + Example: + + ```javascript + var promises = { + myPromise: RSVP.resolve(1), + rejectedPromise: RSVP.reject(new Error("rejectedPromise")), + anotherRejectedPromise: RSVP.reject(new Error("anotherRejectedPromise")), + }; + + RSVP.hash(promises).then(function(hash){ + // Code here never runs because there are rejected promises! + }, function(reason) { + // reason.message === "rejectedPromise" + }); + ``` + + An important note: `RSVP.hash` is intended for plain JavaScript objects that + are just a set of keys and values. `RSVP.hash` will NOT preserve prototype + chains. + + Example: + + ```javascript + function MyConstructor(){ + this.example = RSVP.resolve("Example"); + } + + MyConstructor.prototype = { + protoProperty: RSVP.resolve("Proto Property") + }; + + var myObject = new MyConstructor(); + + RSVP.hash(myObject).then(function(hash){ + // protoProperty will not be present, instead you will just have an + // object that looks like: + // { + // example: "Example" + // } + // + // hash.hasOwnProperty('protoProperty'); // false + // 'undefined' === typeof hash.protoProperty + }); + ``` + + @method hash + @for RSVP + @param {Object} promises + @param {String} label optional string that describes the promise. + Useful for tooling. + @return {Promise} promise that is fulfilled when all properties of `promises` + have been fulfilled, or rejected if any of them become rejected. + @static + */ + __exports__["default"] = function hash(object, label) { + return new Promise(function(resolve, reject){ + var results = {}; + var keys = keysOf(object); + var remaining = keys.length; + var entry, property; + + if (remaining === 0) { + resolve(results); + return; + } + + function fulfilledTo(property) { + return function(value) { + results[property] = value; + if (--remaining === 0) { + resolve(results); + } + }; + } + + function onRejection(reason) { + remaining = 0; + reject(reason); + } + + for (var i = 0; i < keys.length; i++) { + property = keys[i]; + entry = object[property]; + + if (isNonThenable(entry)) { + results[property] = entry; + if (--remaining === 0) { + resolve(results); + } + } else { + Promise.cast(entry).then(fulfilledTo(property), onRejection); + } + } + }); + }; + }); +define("rsvp/instrument", + ["./config","./utils","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + var config = __dependency1__.config; + var now = __dependency2__.now; + + __exports__["default"] = function instrument(eventName, promise, child) { + // instrumentation should not disrupt normal usage. + try { + config.trigger(eventName, { + guid: promise._guidKey + promise._id, + eventName: eventName, + detail: promise._detail, + childGuid: child && promise._guidKey + child._id, + label: promise._label, + timeStamp: now(), + stack: new Error(promise._label).stack + }); + } catch(error) { + setTimeout(function(){ + throw error; + }, 0); + } + }; + }); +define("rsvp/map", + ["./promise","./all","./utils","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __exports__) { + "use strict"; + var Promise = __dependency1__["default"]; + var all = __dependency2__["default"]; + var isArray = __dependency3__.isArray; + var isFunction = __dependency3__.isFunction; + + /** + `RSVP.map` is similar to JavaScript's native `map` method, except that it + waits for all promises to become fulfilled before running the `mapFn` on + each item in given to `promises`. `RSVP.map` returns a promise that will + become fulfilled with the result of running `mapFn` on the values the promises + become fulfilled with. + + For example: + + ```javascript + + var promise1 = RSVP.resolve(1); + var promise2 = RSVP.resolve(2); + var promise3 = RSVP.resolve(3); + var promises = [ promise1, promise2, promise3 ]; + + var mapFn = function(item){ + return item + 1; + }; + + RSVP.map(promises, mapFn).then(function(result){ + // result is [ 2, 3, 4 ] + }); + ``` + + If any of the `promises` given to `RSVP.map` are rejected, the first promise + that is rejected will be given as an argument to the returned promise's + rejection handler. For example: + + ```javascript + var promise1 = RSVP.resolve(1); + var promise2 = RSVP.reject(new Error("2")); + var promise3 = RSVP.reject(new Error("3")); + var promises = [ promise1, promise2, promise3 ]; + + var mapFn = function(item){ + return item + 1; + }; + + RSVP.map(promises, mapFn).then(function(array){ + // Code here never runs because there are rejected promises! + }, function(reason) { + // reason.message === "2" + }); + ``` + + `RSVP.map` will also wait if a promise is returned from `mapFn`. For example, + say you want to get all comments from a set of blog posts, but you need + the blog posts first becuase they contain a url to those comments. + + ```javscript + + var mapFn = function(blogPost){ + // getComments does some ajax and returns an RSVP.Promise that is fulfilled + // with some comments data + return getComments(blogPost.comments_url); + }; + + // getBlogPosts does some ajax and returns an RSVP.Promise that is fulfilled + // with some blog post data + RSVP.map(getBlogPosts(), mapFn).then(function(comments){ + // comments is the result of asking the server for the comments + // of all blog posts returned from getBlogPosts() + }); + ``` + + @method map + @for RSVP + @param {Array} promises + @param {Function} mapFn function to be called on each fulfilled promise. + @param {String} label optional string for labeling the promise. + Useful for tooling. + @return {Promise} promise that is fulfilled with the result of calling + `mapFn` on each fulfilled promise or value when they become fulfilled. + The promise will be rejected if any of the given `promises` become rejected. + @static + */ + __exports__["default"] = function map(promises, mapFn, label) { + return all(promises, label).then(function(results){ + if (!isArray(promises)) { + throw new TypeError('You must pass an array to map.'); + } + + if (!isFunction(mapFn)){ + throw new TypeError("You must pass a function to map's second argument."); + } + + + var resultLen = results.length, + mappedResults = [], + i; + + for (i = 0; i < resultLen; i++){ + mappedResults.push(mapFn(results[i])); + } + + return all(mappedResults, label); + }); + }; + }); +define("rsvp/node", + ["./promise","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Promise = __dependency1__["default"]; + + var slice = Array.prototype.slice; + + function makeNodeCallbackFor(resolve, reject) { + return function (error, value) { + if (error) { + reject(error); + } else if (arguments.length > 2) { + resolve(slice.call(arguments, 1)); + } else { + resolve(value); + } + }; + } + + /** + `RSVP.denodeify` takes a "node-style" function and returns a function that + will return an `RSVP.Promise`. You can use `denodeify` in Node.js or the + browser when you'd prefer to use promises over using callbacks. For example, + `denodeify` transforms the following: + + ```javascript + var fs = require('fs'); + + fs.readFile('myfile.txt', function(err, data){ + if (err) return handleError(err); + handleData(data); + }); + ``` + + into: + + ```javascript + var fs = require('fs'); + + var readFile = RSVP.denodeify(fs.readFile); + + readFile('myfile.txt').then(handleData, handleError); + ``` + + Using `denodeify` makes it easier to compose asynchronous operations instead + of using callbacks. For example, instead of: + + ```javascript + var fs = require('fs'); + var log = require('some-async-logger'); + + fs.readFile('myfile.txt', function(err, data){ + if (err) return handleError(err); + fs.writeFile('myfile2.txt', data, function(err){ + if (err) throw err; + log('success', function(err) { + if (err) throw err; + }); + }); + }); + ``` + + You can chain the operations together using `then` from the returned promise: + + ```javascript + var fs = require('fs'); + var denodeify = RSVP.denodeify; + var readFile = denodeify(fs.readFile); + var writeFile = denodeify(fs.writeFile); + var log = denodeify(require('some-async-logger')); + + readFile('myfile.txt').then(function(data){ + return writeFile('myfile2.txt', data); + }).then(function(){ + return log('SUCCESS'); + }).then(function(){ + // success handler + }, function(reason){ + // rejection handler + }); + ``` + + @method denodeify + @for RSVP + @param {Function} nodeFunc a "node-style" function that takes a callback as + its last argument. The callback expects an error to be passed as its first + argument (if an error occurred, otherwise null), and the value from the + operation as its second argument ("function(err, value){ }"). + @param {Any} binding optional argument for binding the "this" value when + calling the `nodeFunc` function. + @return {Function} a function that wraps `nodeFunc` to return an + `RSVP.Promise` + @static + */ + __exports__["default"] = function denodeify(nodeFunc, binding) { + return function() { + var nodeArgs = slice.call(arguments), resolve, reject; + var thisArg = this || binding; + + return new Promise(function(resolve, reject) { + Promise.all(nodeArgs).then(function(nodeArgs) { + try { + nodeArgs.push(makeNodeCallbackFor(resolve, reject)); + nodeFunc.apply(thisArg, nodeArgs); + } catch(e) { + reject(e); + } + }); + }); + }; + }; + }); +define("rsvp/promise", + ["./config","./events","./instrument","./utils","./promise/cast","./promise/all","./promise/race","./promise/resolve","./promise/reject","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __exports__) { + "use strict"; + var config = __dependency1__.config; + var EventTarget = __dependency2__["default"]; + var instrument = __dependency3__["default"]; + var objectOrFunction = __dependency4__.objectOrFunction; + var isFunction = __dependency4__.isFunction; + var now = __dependency4__.now; + var cast = __dependency5__["default"]; + var all = __dependency6__["default"]; + var race = __dependency7__["default"]; + var Resolve = __dependency8__["default"]; + var Reject = __dependency9__["default"]; + + var guidKey = 'rsvp_' + now() + '-'; + var counter = 0; + + function noop() {} + + __exports__["default"] = Promise; + + + /** + Promise objects represent the eventual result of an asynchronous operation. The + primary way of interacting with a promise is through its `then` method, which + registers callbacks to receive either a promise’s eventual value or the reason + why the promise cannot be fulfilled. + + Terminology + ----------- + + - `promise` is an object or function with a `then` method whose behavior conforms to this specification. + - `thenable` is an object or function that defines a `then` method. + - `value` is any legal JavaScript value (including undefined, a thenable, or a promise). + - `exception` is a value that is thrown using the throw statement. + - `reason` is a value that indicates why a promise was rejected. + - `settled` the final resting state of a promise, fulfilled or rejected. + + A promise can be in one of three states: pending, fulfilled, or rejected. + + Promises that are fulfilled have a fulfillment value and are in the fulfilled + state. Promises that are rejected have a rejection reason and are in the + rejected state. A fulfillment value is never a thenable. Similarly, a + rejection reason is never a thenable. + + Promises can also be said to *resolve* a value. If this value is also a + promise, then the original promise's settled state will match the value's + settled state. So a promise that *resolves* a promise that rejects will + itself reject, and a promise that *resolves* a promise that fulfills will + itself fulfill. + + + Basic Usage: + ------------ + + ```js + var promise = new Promise(function(resolve, reject) { + // on success + resolve(value); + + // on failure + reject(reason); + }); + + promise.then(function(value) { + // on fulfillment + }, function(reason) { + // on rejection + }); + ``` + + Advanced Usage: + --------------- + + Promises shine when abstracting away asynchronous interactions such as + `XMLHttpRequest`s. + + ```js + function getJSON(url) { + return new Promise(function(resolve, reject){ + var xhr = new XMLHttpRequest(); + + xhr.open('GET', url); + xhr.onreadystatechange = handler; + xhr.responseType = 'json'; + xhr.setRequestHeader('Accept', 'application/json'); + xhr.send(); + + function handler() { + if (this.readyState === this.DONE) { + if (this.status === 200) { + resolve(this.response); + } else { + reject(new Error("getJSON: `" + url + "` failed with status: [" + this.status + "]"); + } + } + }; + }); + } + + getJSON('/posts.json').then(function(json) { + // on fulfillment + }, function(reason) { + // on rejection + }); + ``` + + Unlike callbacks, promises are great composable primitives. + + ```js + Promise.all([ + getJSON('/posts'), + getJSON('/comments') + ]).then(function(values){ + values[0] // => postsJSON + values[1] // => commentsJSON + + return values; + }); + ``` + + @class RSVP.Promise + @param {function} + @param {String} label optional string for labeling the promise. + Useful for tooling. + @constructor + */ + function Promise(resolver, label) { + if (!isFunction(resolver)) { + throw new TypeError('You must pass a resolver function as the first argument to the promise constructor'); + } + + if (!(this instanceof Promise)) { + throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function."); + } + + this._id = counter++; + this._label = label; + this._subscribers = []; + + if (config.instrument) { + instrument('created', this); + } + + if (noop !== resolver) { + invokeResolver(resolver, this); + } + } + + function invokeResolver(resolver, promise) { + function resolvePromise(value) { + resolve(promise, value); + } + + function rejectPromise(reason) { + reject(promise, reason); + } + + try { + resolver(resolvePromise, rejectPromise); + } catch(e) { + rejectPromise(e); + } + } + + Promise.cast = cast; + Promise.all = all; + Promise.race = race; + Promise.resolve = Resolve; + Promise.reject = Reject; + + var PENDING = void 0; + var SEALED = 0; + var FULFILLED = 1; + var REJECTED = 2; + + function subscribe(parent, child, onFulfillment, onRejection) { + var subscribers = parent._subscribers; + var length = subscribers.length; + + subscribers[length] = child; + subscribers[length + FULFILLED] = onFulfillment; + subscribers[length + REJECTED] = onRejection; + } + + function publish(promise, settled) { + var child, callback, subscribers = promise._subscribers, detail = promise._detail; + + if (config.instrument) { + instrument(settled === FULFILLED ? 'fulfilled' : 'rejected', promise); + } + + for (var i = 0; i < subscribers.length; i += 3) { + child = subscribers[i]; + callback = subscribers[i + settled]; + + invokeCallback(settled, child, callback, detail); + } + + promise._subscribers = null; + } + + Promise.prototype = { + constructor: Promise, + + _id: undefined, + _guidKey: guidKey, + _label: undefined, + + _state: undefined, + _detail: undefined, + _subscribers: undefined, + + _onerror: function (reason) { + config.trigger('error', reason); + }, + + /** + The primary way of interacting with a promise is through its `then` method, + which registers callbacks to receive either a promise's eventual value or the + reason why the promise cannot be fulfilled. + + ```js + findUser().then(function(user){ + // user is available + }, function(reason){ + // user is unavailable, and you are given the reason why + }); + ``` + + Chaining + -------- + + The return value of `then` is itself a promise. This second, "downstream" + promise is resolved with the return value of the first promise's fulfillment + or rejection handler, or rejected if the handler throws an exception. + + ```js + findUser().then(function (user) { + return user.name; + }, function (reason) { + return "default name"; + }).then(function (userName) { + // If `findUser` fulfilled, `userName` will be the user's name, otherwise it + // will be `"default name"` + }); + + findUser().then(function (user) { + throw new Error("Found user, but still unhappy"); + }, function (reason) { + throw new Error("`findUser` rejected and we're unhappy"); + }).then(function (value) { + // never reached + }, function (reason) { + // if `findUser` fulfilled, `reason` will be "Found user, but still unhappy". + // If `findUser` rejected, `reason` will be "`findUser` rejected and we're unhappy". + }); + ``` + If the downstream promise does not specify a rejection handler, rejection reasons will be propagated further downstream. + + ```js + findUser().then(function (user) { + throw new PedagogicalException("Upstream error"); + }).then(function (value) { + // never reached + }).then(function (value) { + // never reached + }, function (reason) { + // The `PedgagocialException` is propagated all the way down to here + }); + ``` + + Assimilation + ------------ + + Sometimes the value you want to propagate to a downstream promise can only be + retrieved asynchronously. This can be achieved by returning a promise in the + fulfillment or rejection handler. The downstream promise will then be pending + until the returned promise is settled. This is called *assimilation*. + + ```js + findUser().then(function (user) { + return findCommentsByAuthor(user); + }).then(function (comments) { + // The user's comments are now available + }); + ``` + + If the assimliated promise rejects, then the downstream promise will also reject. + + ```js + findUser().then(function (user) { + return findCommentsByAuthor(user); + }).then(function (comments) { + // If `findCommentsByAuthor` fulfills, we'll have the value here + }, function (reason) { + // If `findCommentsByAuthor` rejects, we'll have the reason here + }); + ``` + + Simple Example + -------------- + + Synchronous Example + + ```javascript + var result; + + try { + result = findResult(); + // success + } catch(reason) { + // failure + } + ``` + + Errback Example + + ```js + findResult(function(result, err){ + if (err) { + // failure + } else { + // success + } + }); + ``` + + Promise Example; + + ```javascript + findResult().then(function(result){ + // success + }, function(reason){ + // failure + }); + ``` + + Advanced Example + -------------- + + Synchronous Example + + ```javascript + var author, books; + + try { + author = findAuthor(); + books = findBooksByAuthor(author); + // success + } catch(reason) { + // failure + } + ``` + + Errback Example + + ```js + + function foundBooks(books) { + + } + + function failure(reason) { + + } + + findAuthor(function(author, err){ + if (err) { + failure(err); + // failure + } else { + try { + findBoooksByAuthor(author, function(books, err) { + if (err) { + failure(err); + } else { + try { + foundBooks(books); + } catch(reason) { + failure(reason); + } + } + }); + } catch(error) { + failure(err); + } + // success + } + }); + ``` + + Promise Example; + + ```javascript + findAuthor(). + then(findBooksByAuthor). + then(function(books){ + // found books + }).catch(function(reason){ + // something went wrong + }); + ``` + + @method then + @param {Function} onFulfilled + @param {Function} onRejected + @param {String} label optional string for labeling the promise. + Useful for tooling. + @return {Promise} + */ + then: function(onFulfillment, onRejection, label) { + var promise = this; + this._onerror = null; + + var thenPromise = new this.constructor(noop, label); + + if (this._state) { + var callbacks = arguments; + config.async(function invokePromiseCallback() { + invokeCallback(promise._state, thenPromise, callbacks[promise._state - 1], promise._detail); + }); + } else { + subscribe(this, thenPromise, onFulfillment, onRejection); + } + + if (config.instrument) { + instrument('chained', promise, thenPromise); + } + + return thenPromise; + }, + + /** + `catch` is simply sugar for `then(undefined, onRejection)` which makes it the same + as the catch block of a try/catch statement. + + ```js + function findAuthor(){ + throw new Error("couldn't find that author"); + } + + // synchronous + try { + findAuthor(); + } catch(reason) { + // something went wrong + } + + // async with promises + findAuthor().catch(function(reason){ + // something went wrong + }); + ``` + + @method catch + @param {Function} onRejection + @param {String} label optional string for labeling the promise. + Useful for tooling. + @return {Promise} + */ + 'catch': function(onRejection, label) { + return this.then(null, onRejection, label); + }, + + /** + `finally` will be invoked regardless of the promise's fate just as native + try/catch/finally behaves + + Synchronous example: + + ```js + findAuthor() { + if (Math.random() > 0.5) { + throw new Error(); + } + return new Author(); + } + + try { + return findAuthor(); // succeed or fail + } catch(error) { + return findOtherAuther(); + } finally { + // always runs + // doesn't affect the return value + } + ``` + + Asynchronous example: + + ```js + findAuthor().catch(function(reason){ + return findOtherAuther(); + }).finally(function(){ + // author was either found, or not + }); + ``` + + @method finally + @param {Function} callback + @param {String} label optional string for labeling the promise. + Useful for tooling. + @return {Promise} + */ + 'finally': function(callback, label) { + var constructor = this.constructor; + + return this.then(function(value) { + return constructor.cast(callback()).then(function(){ + return value; + }); + }, function(reason) { + return constructor.cast(callback()).then(function(){ + throw reason; + }); + }, label); + } + }; + + function invokeCallback(settled, promise, callback, detail) { + var hasCallback = isFunction(callback), + value, error, succeeded, failed; + + if (hasCallback) { + try { + value = callback(detail); + succeeded = true; + } catch(e) { + failed = true; + error = e; + } + } else { + value = detail; + succeeded = true; + } + + if (handleThenable(promise, value)) { + return; + } else if (hasCallback && succeeded) { + resolve(promise, value); + } else if (failed) { + reject(promise, error); + } else if (settled === FULFILLED) { + resolve(promise, value); + } else if (settled === REJECTED) { + reject(promise, value); + } + } + + function handleThenable(promise, value) { + var then = null, + resolved; + + try { + if (promise === value) { + throw new TypeError("A promises callback cannot return that same promise."); + } + + if (objectOrFunction(value)) { + then = value.then; + + if (isFunction(then)) { + then.call(value, function(val) { + if (resolved) { return true; } + resolved = true; + + if (value !== val) { + resolve(promise, val); + } else { + fulfill(promise, val); + } + }, function(val) { + if (resolved) { return true; } + resolved = true; + + reject(promise, val); + }, 'derived from: ' + (promise._label || ' unknown promise')); + + return true; + } + } + } catch (error) { + if (resolved) { return true; } + reject(promise, error); + return true; + } + + return false; + } + + function resolve(promise, value) { + if (promise === value) { + fulfill(promise, value); + } else if (!handleThenable(promise, value)) { + fulfill(promise, value); + } + } + + function fulfill(promise, value) { + if (promise._state !== PENDING) { return; } + promise._state = SEALED; + promise._detail = value; + + config.async(publishFulfillment, promise); + } + + function reject(promise, reason) { + if (promise._state !== PENDING) { return; } + promise._state = SEALED; + promise._detail = reason; + + config.async(publishRejection, promise); + } + + function publishFulfillment(promise) { + publish(promise, promise._state = FULFILLED); + } + + function publishRejection(promise) { + if (promise._onerror) { + promise._onerror(promise._detail); + } + + publish(promise, promise._state = REJECTED); + } + }); +define("rsvp/promise/all", + ["../utils","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var isArray = __dependency1__.isArray; + var isNonThenable = __dependency1__.isNonThenable; + + /** + `RSVP.Promise.all` accepts an array of promises, and returns a new promise which + is fulfilled with an array of fulfillment values for the passed promises, or + rejected with the reason of the first passed promise to be rejected. It casts all + elements of the passed iterable to promises as it runs this algorithm. + + Example: + + ```javascript + var promise1 = RSVP.resolve(1); + var promise2 = RSVP.resolve(2); + var promise3 = RSVP.resolve(3); + var promises = [ promise1, promise2, promise3 ]; + + RSVP.Promise.all(promises).then(function(array){ + // The array here would be [ 1, 2, 3 ]; + }); + ``` + + If any of the `promises` given to `RSVP.all` are rejected, the first promise + that is rejected will be given as an argument to the returned promises's + rejection handler. For example: + + Example: + + ```javascript + var promise1 = RSVP.resolve(1); + var promise2 = RSVP.reject(new Error("2")); + var promise3 = RSVP.reject(new Error("3")); + var promises = [ promise1, promise2, promise3 ]; + + RSVP.Promise.all(promises).then(function(array){ + // Code here never runs because there are rejected promises! + }, function(error) { + // error.message === "2" + }); + ``` + + @method all + @for Ember.RSVP.Promise + @param {Array} entries array of promises + @param {String} label optional string for labeling the promise. + Useful for tooling. + @return {Promise} promise that is fulfilled when all `promises` have been + fulfilled, or rejected if any of them become rejected. + @static + */ + __exports__["default"] = function all(entries, label) { + + /*jshint validthis:true */ + var Constructor = this; + + return new Constructor(function(resolve, reject) { + if (!isArray(entries)) { + throw new TypeError('You must pass an array to all.'); + } + + var remaining = entries.length; + var results = new Array(remaining); + var entry, pending = true; + + if (remaining === 0) { + resolve(results); + return; + } + + function fulfillmentAt(index) { + return function(value) { + results[index] = value; + if (--remaining === 0) { + resolve(results); + } + }; + } + + function onRejection(reason) { + remaining = 0; + reject(reason); + } + + for (var index = 0; index < entries.length; index++) { + entry = entries[index]; + if (isNonThenable(entry)) { + results[index] = entry; + if (--remaining === 0) { + resolve(results); + } + } else { + Constructor.cast(entry).then(fulfillmentAt(index), onRejection); + } + } + }, label); + }; + }); +define("rsvp/promise/cast", + ["exports"], + function(__exports__) { + "use strict"; + /** + `RSVP.Promise.cast` coerces its argument to a promise, or returns the + argument if it is already a promise which shares a constructor with the caster. + + Example: + + ```javascript + var promise = RSVP.Promise.resolve(1); + var casted = RSVP.Promise.cast(promise); + + console.log(promise === casted); // true + ``` + + In the case of a promise whose constructor does not match, it is assimilated. + The resulting promise will fulfill or reject based on the outcome of the + promise being casted. + + Example: + + ```javascript + var thennable = $.getJSON('/api/foo'); + var casted = RSVP.Promise.cast(thennable); + + console.log(thennable === casted); // false + console.log(casted instanceof RSVP.Promise) // true + + casted.then(function(data) { + // data is the value getJSON fulfills with + }); + ``` + + In the case of a non-promise, a promise which will fulfill with that value is + returned. + + Example: + + ```javascript + var value = 1; // could be a number, boolean, string, undefined... + var casted = RSVP.Promise.cast(value); + + console.log(value === casted); // false + console.log(casted instanceof RSVP.Promise) // true + + casted.then(function(val) { + val === value // => true + }); + ``` + + `RSVP.Promise.cast` is similar to `RSVP.Promise.resolve`, but `RSVP.Promise.cast` differs in the + following ways: + + * `RSVP.Promise.cast` serves as a memory-efficient way of getting a promise, when you + have something that could either be a promise or a value. RSVP.resolve + will have the same effect but will create a new promise wrapper if the + argument is a promise. + * `RSVP.Promise.cast` is a way of casting incoming thenables or promise subclasses to + promises of the exact class specified, so that the resulting object's `then` is + ensured to have the behavior of the constructor you are calling cast on (i.e., RSVP.Promise). + + @method cast + @param {Object} object to be casted + @param {String} label optional string for labeling the promise. + Useful for tooling. + @return {Promise} promise + @static + */ + + __exports__["default"] = function cast(object, label) { + /*jshint validthis:true */ + var Constructor = this; + + if (object && typeof object === 'object' && object.constructor === Constructor) { + return object; + } + + return new Constructor(function(resolve) { + resolve(object); + }, label); + }; + }); +define("rsvp/promise/race", + ["../utils","exports"], + function(__dependency1__, __exports__) { + "use strict"; + /* global toString */ + + var isArray = __dependency1__.isArray; + var isFunction = __dependency1__.isFunction; + var isNonThenable = __dependency1__.isNonThenable; + + /** + `RSVP.Promise.race` returns a new promise which is settled in the same way as the + first passed promise to settle. + + Example: + + ```javascript + var promise1 = new RSVP.Promise(function(resolve, reject){ + setTimeout(function(){ + resolve("promise 1"); + }, 200); + }); + + var promise2 = new RSVP.Promise(function(resolve, reject){ + setTimeout(function(){ + resolve("promise 2"); + }, 100); + }); + + RSVP.Promise.race([promise1, promise2]).then(function(result){ + // result === "promise 2" because it was resolved before promise1 + // was resolved. + }); + ``` + + `RSVP.Promise.race` is deterministic in that only the state of the first + settled promise matters. For example, even if other promises given to the + `promises` array argument are resolved, but the first settled promise has + become rejected before the other promises became fulfilled, the returned + promise will become rejected: + + ```javascript + var promise1 = new RSVP.Promise(function(resolve, reject){ + setTimeout(function(){ + resolve("promise 1"); + }, 200); + }); + + var promise2 = new RSVP.Promise(function(resolve, reject){ + setTimeout(function(){ + reject(new Error("promise 2")); + }, 100); + }); + + RSVP.Promise.race([promise1, promise2]).then(function(result){ + // Code here never runs + }, function(reason){ + // reason.message === "promise2" because promise 2 became rejected before + // promise 1 became fulfilled + }); + ``` + + An example real-world use case is implementing timeouts: + + ```javascript + RSVP.Promise.race([ajax('foo.json'), timeout(5000)]) + ``` + + @method race + @param {Array} promises array of promises to observe + @param {String} label optional string for describing the promise returned. + Useful for tooling. + @return {Promise} a promise which settles in the same way as the first passed + promise to settle. + @static + */ + __exports__["default"] = function race(entries, label) { + /*jshint validthis:true */ + var Constructor = this, entry; + + return new Constructor(function(resolve, reject) { + if (!isArray(entries)) { + throw new TypeError('You must pass an array to race.'); + } + + var pending = true; + + function onFulfillment(value) { if (pending) { pending = false; resolve(value); } } + function onRejection(reason) { if (pending) { pending = false; reject(reason); } } + + for (var i = 0; i < entries.length; i++) { + entry = entries[i]; + if (isNonThenable(entry)) { + pending = false; + resolve(entry); + return; + } else { + Constructor.cast(entry).then(onFulfillment, onRejection); + } + } + }, label); + }; + }); +define("rsvp/promise/reject", + ["exports"], + function(__exports__) { + "use strict"; + /** + `RSVP.Promise.reject` returns a promise rejected with the passed `reason`. + It is shorthand for the following: + + ```javascript + var promise = new RSVP.Promise(function(resolve, reject){ + reject(new Error('WHOOPS')); + }); + + promise.then(function(value){ + // Code here doesn't run because the promise is rejected! + }, function(reason){ + // reason.message === 'WHOOPS' + }); + ``` + + Instead of writing the above, your code now simply becomes the following: + + ```javascript + var promise = RSVP.Promise.reject(new Error('WHOOPS')); + + promise.then(function(value){ + // Code here doesn't run because the promise is rejected! + }, function(reason){ + // reason.message === 'WHOOPS' + }); + ``` + + @method reject + @param {Any} reason value that the returned promise will be rejected with. + @param {String} label optional string for identifying the returned promise. + Useful for tooling. + @return {Promise} a promise rejected with the given `reason`. + @static + */ + __exports__["default"] = function reject(reason, label) { + /*jshint validthis:true */ + var Constructor = this; + + return new Constructor(function (resolve, reject) { + reject(reason); + }, label); + }; + }); +define("rsvp/promise/resolve", + ["exports"], + function(__exports__) { + "use strict"; + /** + `RSVP.Promise.resolve` returns a promise that will become resolved with the + passed `value`. It is shorthand for the following: + + ```javascript + var promise = new RSVP.Promise(function(resolve, reject){ + resolve(1); + }); + + promise.then(function(value){ + // value === 1 + }); + ``` + + Instead of writing the above, your code now simply becomes the following: + + ```javascript + var promise = RSVP.Promise.resolve(1); + + promise.then(function(value){ + // value === 1 + }); + ``` + + @method resolve + @param {Any} value value that the returned promise will be resolved with + @param {String} label optional string for identifying the returned promise. + Useful for tooling. + @return {Promise} a promise that will become fulfilled with the given + `value` + @static + */ + __exports__["default"] = function resolve(value, label) { + /*jshint validthis:true */ + var Constructor = this; + + return new Constructor(function(resolve, reject) { + resolve(value); + }, label); + }; + }); +define("rsvp/race", + ["./promise","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Promise = __dependency1__["default"]; + + /** + This is a convenient alias for `RSVP.Promise.race`. + + @method race + @param {Array} array Array of promises. + @param {String} label An optional label. This is useful + for tooling. + @static + */ + __exports__["default"] = function race(array, label) { + return Promise.race(array, label); + }; + }); +define("rsvp/reject", + ["./promise","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Promise = __dependency1__["default"]; + + /** + This is a convenient alias for `RSVP.Promise.reject`. + + @method reject + @for RSVP + @param {Any} reason value that the returned promise will be rejected with. + @param {String} label optional string for identifying the returned promise. + Useful for tooling. + @return {Promise} a promise rejected with the given `reason`. + @static + */ + __exports__["default"] = function reject(reason, label) { + return Promise.reject(reason, label); + }; + }); +define("rsvp/resolve", + ["./promise","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Promise = __dependency1__["default"]; + + /** + This is a convenient alias for `RSVP.Promise.resolve`. + + @method resolve + @for RSVP + @param {Any} value value that the returned promise will be resolved with + @param {String} label optional string for identifying the returned promise. + Useful for tooling. + @return {Promise} a promise that will become fulfilled with the given + `value` + @static + */ + __exports__["default"] = function resolve(value, label) { + return Promise.resolve(value, label); + }; + }); +define("rsvp/rethrow", + ["exports"], + function(__exports__) { + "use strict"; + /** + `RSVP.rethrow` will rethrow an error on the next turn of the JavaScript event + loop in order to aid debugging. + + Promises A+ specifies that any exceptions that occur with a promise must be + caught by the promises implementation and bubbled to the last handler. For + this reason, it is recommended that you always specify a second rejection + handler function to `then`. However, `RSVP.rethrow` will throw the exception + outside of the promise, so it bubbles up to your console if in the browser, + or domain/cause uncaught exception in Node. `rethrow` will also throw the + error again so the error can be handled by the promise per the spec. + + ```javascript + function throws(){ + throw new Error('Whoops!'); + } + + var promise = new RSVP.Promise(function(resolve, reject){ + throws(); + }); + + promise.catch(RSVP.rethrow).then(function(){ + // Code here doesn't run because the promise became rejected due to an + // error! + }, function (err){ + // handle the error here + }); + ``` + + The 'Whoops' error will be thrown on the next turn of the event loop + and you can watch for it in your console. You can also handle it using a + rejection handler given to `.then` or `.catch` on the returned promise. + + @method rethrow + @for RSVP + @param {Error} reason reason the promise became rejected. + @throws Error + @static + */ + __exports__["default"] = function rethrow(reason) { + setTimeout(function() { + throw reason; + }); + throw reason; + }; + }); +define("rsvp/utils", + ["exports"], + function(__exports__) { + "use strict"; + function objectOrFunction(x) { + return typeof x === "function" || (typeof x === "object" && x !== null); + } + + __exports__.objectOrFunction = objectOrFunction;function isFunction(x) { + return typeof x === "function"; + } + + __exports__.isFunction = isFunction;function isNonThenable(x) { + return !objectOrFunction(x); + } + + __exports__.isNonThenable = isNonThenable;function isArray(x) { + return Object.prototype.toString.call(x) === "[object Array]"; + } + + __exports__.isArray = isArray;// Date.now is not available in browsers < IE9 + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now#Compatibility + var now = Date.now || function() { return new Date().getTime(); }; + __exports__.now = now; + var keysOf = Object.keys || function(object) { + var result = []; + + for (var prop in object) { + result.push(prop); + } + + return result; + }; + __exports__.keysOf = keysOf; + }); +define("rsvp", + ["./rsvp/promise","./rsvp/events","./rsvp/node","./rsvp/all","./rsvp/all_settled","./rsvp/race","./rsvp/hash","./rsvp/rethrow","./rsvp/defer","./rsvp/config","./rsvp/map","./rsvp/resolve","./rsvp/reject","./rsvp/filter","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __dependency11__, __dependency12__, __dependency13__, __dependency14__, __exports__) { + "use strict"; + var Promise = __dependency1__["default"]; + var EventTarget = __dependency2__["default"]; + var denodeify = __dependency3__["default"]; + var all = __dependency4__["default"]; + var allSettled = __dependency5__["default"]; + var race = __dependency6__["default"]; + var hash = __dependency7__["default"]; + var rethrow = __dependency8__["default"]; + var defer = __dependency9__["default"]; + var config = __dependency10__.config; + var configure = __dependency10__.configure; + var map = __dependency11__["default"]; + var resolve = __dependency12__["default"]; + var reject = __dependency13__["default"]; + var filter = __dependency14__["default"]; + + function async(callback, arg) { + config.async(callback, arg); + } + + function on() { + config.on.apply(config, arguments); + } + + function off() { + config.off.apply(config, arguments); + } + + // Set up instrumentation through `window.__PROMISE_INTRUMENTATION__` + if (typeof window !== 'undefined' && typeof window.__PROMISE_INSTRUMENTATION__ === 'object') { + var callbacks = window.__PROMISE_INSTRUMENTATION__; + configure('instrument', true); + for (var eventName in callbacks) { + if (callbacks.hasOwnProperty(eventName)) { + on(eventName, callbacks[eventName]); + } + } + } + + __exports__.Promise = Promise; + __exports__.EventTarget = EventTarget; + __exports__.all = all; + __exports__.allSettled = allSettled; + __exports__.race = race; + __exports__.hash = hash; + __exports__.rethrow = rethrow; + __exports__.defer = defer; + __exports__.denodeify = denodeify; + __exports__.configure = configure; + __exports__.on = on; + __exports__.off = off; + __exports__.resolve = resolve; + __exports__.reject = reject; + __exports__.async = async; + __exports__.map = map; + __exports__.filter = filter; + }); + +})(); + +(function() { +define("container/container", + ["container/inheriting_dict","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var InheritingDict = __dependency1__["default"]; + + // A lightweight container that helps to assemble and decouple components. + // Public api for the container is still in flux. + // The public api, specified on the application namespace should be considered the stable api. + function Container(parent) { + this.parent = parent; + this.children = []; + + this.resolver = parent && parent.resolver || function() {}; + + this.registry = new InheritingDict(parent && parent.registry); + this.cache = new InheritingDict(parent && parent.cache); + this.factoryCache = new InheritingDict(parent && parent.factoryCache); + this.resolveCache = new InheritingDict(parent && parent.resolveCache); + this.typeInjections = new InheritingDict(parent && parent.typeInjections); + this.injections = {}; + + this.factoryTypeInjections = new InheritingDict(parent && parent.factoryTypeInjections); + this.factoryInjections = {}; + + this._options = new InheritingDict(parent && parent._options); + this._typeOptions = new InheritingDict(parent && parent._typeOptions); + } + + Container.prototype = { + + /** + @property parent + @type Container + @default null + */ + parent: null, + + /** + @property children + @type Array + @default [] + */ + children: null, + + /** + @property resolver + @type function + */ + resolver: null, + + /** + @property registry + @type InheritingDict + */ + registry: null, + + /** + @property cache + @type InheritingDict + */ + cache: null, + + /** + @property typeInjections + @type InheritingDict + */ + typeInjections: null, + + /** + @property injections + @type Object + @default {} + */ + injections: null, + + /** + @private + + @property _options + @type InheritingDict + @default null + */ + _options: null, + + /** + @private + + @property _typeOptions + @type InheritingDict + */ + _typeOptions: null, + + /** + Returns a new child of the current container. These children are configured + to correctly inherit from the current container. + + @method child + @return {Container} + */ + child: function() { + var container = new Container(this); + this.children.push(container); + return container; + }, + + /** + Sets a key-value pair on the current container. If a parent container, + has the same key, once set on a child, the parent and child will diverge + as expected. + + @method set + @param {Object} object + @param {String} key + @param {any} value + */ + set: function(object, key, value) { + object[key] = value; + }, + + /** + Registers a factory for later injection. + + Example: + + ```javascript + var container = new Container(); + + container.register('model:user', Person, {singleton: false }); + container.register('fruit:favorite', Orange); + container.register('communication:main', Email, {singleton: false}); + ``` + + @method register + @param {String} fullName + @param {Function} factory + @param {Object} options + */ + register: function(fullName, factory, options) { + validateFullName(fullName); + + if (factory === undefined) { + throw new TypeError('Attempting to register an unknown factory: `' + fullName + '`'); + } + + var normalizedName = this.normalize(fullName); + + if (this.cache.has(normalizedName)) { + throw new Error('Cannot re-register: `' + fullName +'`, as it has already been looked up.'); + } + + this.registry.set(normalizedName, factory); + this._options.set(normalizedName, options || {}); + }, + + /** + Unregister a fullName + + ```javascript + var container = new Container(); + container.register('model:user', User); + + container.lookup('model:user') instanceof User //=> true + + container.unregister('model:user') + container.lookup('model:user') === undefined //=> true + ``` + + @method unregister + @param {String} fullName + */ + unregister: function(fullName) { + validateFullName(fullName); + + var normalizedName = this.normalize(fullName); + + this.registry.remove(normalizedName); + this.cache.remove(normalizedName); + this.factoryCache.remove(normalizedName); + this.resolveCache.remove(normalizedName); + this._options.remove(normalizedName); + }, + + /** + Given a fullName return the corresponding factory. + + By default `resolve` will retrieve the factory from + its container's registry. + + ```javascript + var container = new Container(); + container.register('api:twitter', Twitter); + + container.resolve('api:twitter') // => Twitter + ``` + + Optionally the container can be provided with a custom resolver. + If provided, `resolve` will first provide the custom resolver + the opportunity to resolve the fullName, otherwise it will fallback + to the registry. + + ```javascript + var container = new Container(); + container.resolver = function(fullName) { + // lookup via the module system of choice + }; + + // the twitter factory is added to the module system + container.resolve('api:twitter') // => Twitter + ``` + + @method resolve + @param {String} fullName + @return {Function} fullName's factory + */ + resolve: function(fullName) { + validateFullName(fullName); + + var normalizedName = this.normalize(fullName); + var cached = this.resolveCache.get(normalizedName); + + if (cached) { return cached; } + + var resolved = this.resolver(normalizedName) || this.registry.get(normalizedName); + + this.resolveCache.set(normalizedName, resolved); + + return resolved; + }, + + /** + A hook that can be used to describe how the resolver will + attempt to find the factory. + + For example, the default Ember `.describe` returns the full + class name (including namespace) where Ember's resolver expects + to find the `fullName`. + + @method describe + @param {String} fullName + @return {string} described fullName + */ + describe: function(fullName) { + return fullName; + }, + + /** + A hook to enable custom fullName normalization behaviour + + @method normalize + @param {String} fullName + @return {string} normalized fullName + */ + normalize: function(fullName) { + return fullName; + }, + + /** + @method makeToString + + @param {any} factory + @param {string} fullName + @return {function} toString function + */ + makeToString: function(factory, fullName) { + return factory.toString(); + }, + + /** + Given a fullName return a corresponding instance. + + The default behaviour is for lookup to return a singleton instance. + The singleton is scoped to the container, allowing multiple containers + to all have their own locally scoped singletons. + + ```javascript + var container = new Container(); + container.register('api:twitter', Twitter); + + var twitter = container.lookup('api:twitter'); + + twitter instanceof Twitter; // => true + + // by default the container will return singletons + var twitter2 = container.lookup('api:twitter'); + twitter2 instanceof Twitter; // => true + + twitter === twitter2; //=> true + ``` + + If singletons are not wanted an optional flag can be provided at lookup. + + ```javascript + var container = new Container(); + container.register('api:twitter', Twitter); + + var twitter = container.lookup('api:twitter', { singleton: false }); + var twitter2 = container.lookup('api:twitter', { singleton: false }); + + twitter === twitter2; //=> false + ``` + + @method lookup + @param {String} fullName + @param {Object} options + @return {any} + */ + lookup: function(fullName, options) { + validateFullName(fullName); + return lookup(this, this.normalize(fullName), options); + }, + + /** + Given a fullName return the corresponding factory. + + @method lookupFactory + @param {String} fullName + @return {any} + */ + lookupFactory: function(fullName) { + validateFullName(fullName); + return factoryFor(this, this.normalize(fullName)); + }, + + /** + Given a fullName check if the container is aware of its factory + or singleton instance. + + @method has + @param {String} fullName + @return {Boolean} + */ + has: function(fullName) { + validateFullName(fullName); + return has(this, this.normalize(fullName)); + }, + + /** + Allow registering options for all factories of a type. + + ```javascript + var container = new Container(); + + // if all of type `connection` must not be singletons + container.optionsForType('connection', { singleton: false }); + + container.register('connection:twitter', TwitterConnection); + container.register('connection:facebook', FacebookConnection); + + var twitter = container.lookup('connection:twitter'); + var twitter2 = container.lookup('connection:twitter'); + + twitter === twitter2; // => false + + var facebook = container.lookup('connection:facebook'); + var facebook2 = container.lookup('connection:facebook'); + + facebook === facebook2; // => false + ``` + + @method optionsForType + @param {String} type + @param {Object} options + */ + optionsForType: function(type, options) { + if (this.parent) { illegalChildOperation('optionsForType'); } + + this._typeOptions.set(type, options); + }, + + /** + @method options + @param {String} type + @param {Object} options + */ + options: function(type, options) { + this.optionsForType(type, options); + }, + + /** + Used only via `injection`. + + Provides a specialized form of injection, specifically enabling + all objects of one type to be injected with a reference to another + object. + + For example, provided each object of type `controller` needed a `router`. + one would do the following: + + ```javascript + var container = new Container(); + + container.register('router:main', Router); + container.register('controller:user', UserController); + container.register('controller:post', PostController); + + container.typeInjection('controller', 'router', 'router:main'); + + var user = container.lookup('controller:user'); + var post = container.lookup('controller:post'); + + user.router instanceof Router; //=> true + post.router instanceof Router; //=> true + + // both controllers share the same router + user.router === post.router; //=> true + ``` + + @private + @method typeInjection + @param {String} type + @param {String} property + @param {String} fullName + */ + typeInjection: function(type, property, fullName) { + validateFullName(fullName); + if (this.parent) { illegalChildOperation('typeInjection'); } + + var fullNameType = fullName.split(':')[0]; + if(fullNameType === type) { + throw new Error('Cannot inject a `' + fullName + '` on other ' + type + '(s). Register the `' + fullName + '` as a different type and perform the typeInjection.'); + } + addTypeInjection(this.typeInjections, type, property, fullName); + }, + + /** + Defines injection rules. + + These rules are used to inject dependencies onto objects when they + are instantiated. + + Two forms of injections are possible: + + * Injecting one fullName on another fullName + * Injecting one fullName on a type + + Example: + + ```javascript + var container = new Container(); + + container.register('source:main', Source); + container.register('model:user', User); + container.register('model:post', Post); + + // injecting one fullName on another fullName + // eg. each user model gets a post model + container.injection('model:user', 'post', 'model:post'); + + // injecting one fullName on another type + container.injection('model', 'source', 'source:main'); + + var user = container.lookup('model:user'); + var post = container.lookup('model:post'); + + user.source instanceof Source; //=> true + post.source instanceof Source; //=> true + + user.post instanceof Post; //=> true + + // and both models share the same source + user.source === post.source; //=> true + ``` + + @method injection + @param {String} factoryName + @param {String} property + @param {String} injectionName + */ + injection: function(fullName, property, injectionName) { + if (this.parent) { illegalChildOperation('injection'); } + + validateFullName(injectionName); + var normalizedInjectionName = this.normalize(injectionName); + + if (fullName.indexOf(':') === -1) { + return this.typeInjection(fullName, property, normalizedInjectionName); + } + + validateFullName(fullName); + var normalizedName = this.normalize(fullName); + + addInjection(this.injections, normalizedName, property, normalizedInjectionName); + }, + + + /** + Used only via `factoryInjection`. + + Provides a specialized form of injection, specifically enabling + all factory of one type to be injected with a reference to another + object. + + For example, provided each factory of type `model` needed a `store`. + one would do the following: + + ```javascript + var container = new Container(); + + container.register('store:main', SomeStore); + + container.factoryTypeInjection('model', 'store', 'store:main'); + + var store = container.lookup('store:main'); + var UserFactory = container.lookupFactory('model:user'); + + UserFactory.store instanceof SomeStore; //=> true + ``` + + @private + @method factoryTypeInjection + @param {String} type + @param {String} property + @param {String} fullName + */ + factoryTypeInjection: function(type, property, fullName) { + if (this.parent) { illegalChildOperation('factoryTypeInjection'); } + + addTypeInjection(this.factoryTypeInjections, type, property, this.normalize(fullName)); + }, + + /** + Defines factory injection rules. + + Similar to regular injection rules, but are run against factories, via + `Container#lookupFactory`. + + These rules are used to inject objects onto factories when they + are looked up. + + Two forms of injections are possible: + + * Injecting one fullName on another fullName + * Injecting one fullName on a type + + Example: + + ```javascript + var container = new Container(); + + container.register('store:main', Store); + container.register('store:secondary', OtherStore); + container.register('model:user', User); + container.register('model:post', Post); + + // injecting one fullName on another type + container.factoryInjection('model', 'store', 'store:main'); + + // injecting one fullName on another fullName + container.factoryInjection('model:post', 'secondaryStore', 'store:secondary'); + + var UserFactory = container.lookupFactory('model:user'); + var PostFactory = container.lookupFactory('model:post'); + var store = container.lookup('store:main'); + + UserFactory.store instanceof Store; //=> true + UserFactory.secondaryStore instanceof OtherStore; //=> false + + PostFactory.store instanceof Store; //=> true + PostFactory.secondaryStore instanceof OtherStore; //=> true + + // and both models share the same source instance + UserFactory.store === PostFactory.store; //=> true + ``` + + @method factoryInjection + @param {String} factoryName + @param {String} property + @param {String} injectionName + */ + factoryInjection: function(fullName, property, injectionName) { + if (this.parent) { illegalChildOperation('injection'); } + + var normalizedName = this.normalize(fullName); + var normalizedInjectionName = this.normalize(injectionName); + + validateFullName(injectionName); + + if (fullName.indexOf(':') === -1) { + return this.factoryTypeInjection(normalizedName, property, normalizedInjectionName); + } + + validateFullName(fullName); + + addInjection(this.factoryInjections, normalizedName, property, normalizedInjectionName); + }, + + /** + A depth first traversal, destroying the container, its descendant containers and all + their managed objects. + + @method destroy + */ + destroy: function() { + for (var i=0, l=this.children.length; i w. + */ + function compare(v, w) { + if (v === w) { return 0; } + + var type1 = typeOf(v); + var type2 = typeOf(w); + + if (Comparable) { + if (type1==='instance' && Comparable.detect(v.constructor)) { + return v.constructor.compare(v, w); + } + + if (type2 === 'instance' && Comparable.detect(w.constructor)) { + return 1-w.constructor.compare(w, v); + } + } + + // If we haven't yet generated a reverse-mapping of Ember.ORDER_DEFINITION, + // do so now. + var mapping = Ember.ORDER_DEFINITION_MAPPING; + if (!mapping) { + var order = Ember.ORDER_DEFINITION; + mapping = Ember.ORDER_DEFINITION_MAPPING = {}; + var idx, len; + for (idx = 0, len = order.length; idx < len; ++idx) { + mapping[order[idx]] = idx; + } + + // We no longer need Ember.ORDER_DEFINITION. + delete Ember.ORDER_DEFINITION; + } + + var type1Index = mapping[type1]; + var type2Index = mapping[type2]; + + if (type1Index < type2Index) { return -1; } + if (type1Index > type2Index) { return 1; } + + // types are equal - so we have to check values now + switch (type1) { + case 'boolean': + case 'number': + if (v < w) { return -1; } + if (v > w) { return 1; } + return 0; + + case 'string': + var comp = v.localeCompare(w); + if (comp < 0) { return -1; } + if (comp > 0) { return 1; } + return 0; + + case 'array': + var vLen = v.length; + var wLen = w.length; + var l = Math.min(vLen, wLen); + var r = 0; + var i = 0; + while (r === 0 && i < l) { + r = compare(v[i],w[i]); + i++; + } + if (r !== 0) { return r; } + + // all elements are equal now + // shorter array should be ordered first + if (vLen < wLen) { return -1; } + if (vLen > wLen) { return 1; } + // arrays are equal now + return 0; + + case 'instance': + if (Comparable && Comparable.detect(v)) { + return v.compare(v, w); + } + return 0; + + case 'date': + var vNum = v.getTime(); + var wNum = w.getTime(); + if (vNum < wNum) { return -1; } + if (vNum > wNum) { return 1; } + return 0; + + default: + return 0; + } + }; + + __exports__["default"] = compare; + }); +define("ember-runtime/computed/array_computed", + ["ember-metal/core","ember-runtime/computed/reduce_computed","ember-metal/enumerable_utils","ember-metal/platform","ember-metal/observer","ember-metal/error","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __exports__) { + "use strict"; + var Ember = __dependency1__["default"]; + var reduceComputed = __dependency2__.reduceComputed; + var ReduceComputedProperty = __dependency2__.ReduceComputedProperty; + var EnumerableUtils = __dependency3__["default"]; + var create = __dependency4__.create; + var addObserver = __dependency5__.addObserver; + var EmberError = __dependency6__["default"]; + + var a_slice = [].slice, + o_create = create, + forEach = EnumerableUtils.forEach; + + function ArrayComputedProperty() { + var cp = this; + + ReduceComputedProperty.apply(this, arguments); + + this.func = (function(reduceFunc) { + return function (propertyName) { + if (!cp._hasInstanceMeta(this, propertyName)) { + // When we recompute an array computed property, we need already + // retrieved arrays to be updated; we can't simply empty the cache and + // hope the array is re-retrieved. + forEach(cp._dependentKeys, function(dependentKey) { + addObserver(this, dependentKey, function() { + cp.recomputeOnce.call(this, propertyName); + }); + }, this); + } + + return reduceFunc.apply(this, arguments); + }; + })(this.func); + + return this; + } + + ArrayComputedProperty.prototype = o_create(ReduceComputedProperty.prototype); + ArrayComputedProperty.prototype.initialValue = function () { + return Ember.A(); + }; + ArrayComputedProperty.prototype.resetValue = function (array) { + array.clear(); + return array; + }; + + // This is a stopgap to keep the reference counts correct with lazy CPs. + ArrayComputedProperty.prototype.didChange = function (obj, keyName) { + return; + }; + + /** + Creates a computed property which operates on dependent arrays and + is updated with "one at a time" semantics. When items are added or + removed from the dependent array(s) an array computed only operates + on the change instead of re-evaluating the entire array. This should + return an array, if you'd like to use "one at a time" semantics and + compute some value other then an array look at + `Ember.reduceComputed`. + + If there are more than one arguments the first arguments are + considered to be dependent property keys. The last argument is + required to be an options object. The options object can have the + following three properties. + + `initialize` - An optional initialize function. Typically this will be used + to set up state on the instanceMeta object. + + `removedItem` - A function that is called each time an element is + removed from the array. + + `addedItem` - A function that is called each time an element is + added to the array. + + + The `initialize` function has the following signature: + + ```javascript + function(array, changeMeta, instanceMeta) + ``` + + `array` - The initial value of the arrayComputed, an empty array. + + `changeMeta` - An object which contains meta information about the + computed. It contains the following properties: + + - `property` the computed property + - `propertyName` the name of the property on the object + + `instanceMeta` - An object that can be used to store meta + information needed for calculating your computed. For example a + unique computed might use this to store the number of times a given + element is found in the dependent array. + + + The `removedItem` and `addedItem` functions both have the following signature: + + ```javascript + function(accumulatedValue, item, changeMeta, instanceMeta) + ``` + + `accumulatedValue` - The value returned from the last time + `removedItem` or `addedItem` was called or an empty array. + + `item` - the element added or removed from the array + + `changeMeta` - An object which contains meta information about the + change. It contains the following properties: + + - `property` the computed property + - `propertyName` the name of the property on the object + - `index` the index of the added or removed item + - `item` the added or removed item: this is exactly the same as + the second arg + - `arrayChanged` the array that triggered the change. Can be + useful when depending on multiple arrays. + + For property changes triggered on an item property change (when + depKey is something like `someArray.@each.someProperty`), + `changeMeta` will also contain the following property: + + - `previousValues` an object whose keys are the properties that changed on + the item, and whose values are the item's previous values. + + `previousValues` is important Ember coalesces item property changes via + Ember.run.once. This means that by the time removedItem gets called, item has + the new values, but you may need the previous value (eg for sorting & + filtering). + + `instanceMeta` - An object that can be used to store meta + information needed for calculating your computed. For example a + unique computed might use this to store the number of times a given + element is found in the dependent array. + + The `removedItem` and `addedItem` functions should return the accumulated + value. It is acceptable to not return anything (ie return undefined) + to invalidate the computation. This is generally not a good idea for + arrayComputed but it's used in eg max and min. + + Example + + ```javascript + Ember.computed.map = function(dependentKey, callback) { + var options = { + addedItem: function(array, item, changeMeta, instanceMeta) { + var mapped = callback(item); + array.insertAt(changeMeta.index, mapped); + return array; + }, + removedItem: function(array, item, changeMeta, instanceMeta) { + array.removeAt(changeMeta.index, 1); + return array; + } + }; + + return Ember.arrayComputed(dependentKey, options); + }; + ``` + + @method arrayComputed + @for Ember + @param {String} [dependentKeys*] + @param {Object} options + @return {Ember.ComputedProperty} + */ + function arrayComputed (options) { + var args; + + if (arguments.length > 1) { + args = a_slice.call(arguments, 0, -1); + options = a_slice.call(arguments, -1)[0]; + } + + if (typeof options !== "object") { + throw new EmberError("Array Computed Property declared without an options hash"); + } + + var cp = new ArrayComputedProperty(options); + + if (args) { + cp.property.apply(cp, args); + } + + return cp; + }; + + __exports__.arrayComputed = arrayComputed; + __exports__.ArrayComputedProperty = ArrayComputedProperty; + }); +define("ember-runtime/computed/reduce_computed", + ["ember-metal/core","ember-metal/property_get","ember-metal/property_set","ember-metal/utils","ember-metal/error","ember-metal/property_events","ember-metal/expand_properties","ember-metal/observer","ember-metal/computed","ember-metal/platform","ember-metal/enumerable_utils","ember-runtime/system/tracked_array","ember-runtime/mixins/array","ember-metal/run_loop","ember-runtime/system/set","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __dependency11__, __dependency12__, __dependency13__, __dependency14__, __dependency15__, __exports__) { + "use strict"; + var Ember = __dependency1__["default"]; + // Ember.assert + var e_get = __dependency2__.get; + var set = __dependency3__.set; + var guidFor = __dependency4__.guidFor; + var metaFor = __dependency4__.meta; + var EmberError = __dependency5__["default"]; + var propertyWillChange = __dependency6__.propertyWillChange; + var propertyDidChange = __dependency6__.propertyDidChange; + var expandProperties = __dependency7__["default"]; + var addObserver = __dependency8__.addObserver; + var observersFor = __dependency8__.observersFor; + var removeObserver = __dependency8__.removeObserver; + var addBeforeObserver = __dependency8__.addBeforeObserver; + var removeBeforeObserver = __dependency8__.removeBeforeObserver; + var ComputedProperty = __dependency9__.ComputedProperty; + var cacheFor = __dependency9__.cacheFor; + var create = __dependency10__.create; + var EnumerableUtils = __dependency11__["default"]; + var TrackedArray = __dependency12__["default"]; + var EmberArray = __dependency13__["default"]; + var run = __dependency14__["default"]; + var Set = __dependency15__["default"]; + var isArray = __dependency4__.isArray; + + var cacheSet = cacheFor.set, + cacheGet = cacheFor.get, + cacheRemove = cacheFor.remove, + a_slice = [].slice, + o_create = create, + forEach = EnumerableUtils.forEach, + // Here we explicitly don't allow `@each.foo`; it would require some special + // testing, but there's no particular reason why it should be disallowed. + eachPropertyPattern = /^(.*)\.@each\.(.*)/, + doubleEachPropertyPattern = /(.*\.@each){2,}/, + arrayBracketPattern = /\.\[\]$/; + + function get(obj, key) { + if (key === '@this') { + return obj; + } + + return e_get(obj, key); + } + + /* + Tracks changes to dependent arrays, as well as to properties of items in + dependent arrays. + + @class DependentArraysObserver + */ + function DependentArraysObserver(callbacks, cp, instanceMeta, context, propertyName, sugarMeta) { + // user specified callbacks for `addedItem` and `removedItem` + this.callbacks = callbacks; + + // the computed property: remember these are shared across instances + this.cp = cp; + + // the ReduceComputedPropertyInstanceMeta this DependentArraysObserver is + // associated with + this.instanceMeta = instanceMeta; + + // A map of array guids to dependentKeys, for the given context. We track + // this because we want to set up the computed property potentially before the + // dependent array even exists, but when the array observer fires, we lack + // enough context to know what to update: we can recover that context by + // getting the dependentKey. + this.dependentKeysByGuid = {}; + + // a map of dependent array guids -> TrackedArray instances. We use + // this to lazily recompute indexes for item property observers. + this.trackedArraysByGuid = {}; + + // We suspend observers to ignore replacements from `reset` when totally + // recomputing. Unfortunately we cannot properly suspend the observers + // because we only have the key; instead we make the observers no-ops + this.suspended = false; + + // This is used to coalesce item changes from property observers within a + // single item. + this.changedItems = {}; + // This is used to coalesce item changes for multiple items that depend on + // some shared state. + this.changedItemCount = 0; + } + + function ItemPropertyObserverContext (dependentArray, index, trackedArray) { + Ember.assert("Internal error: trackedArray is null or undefined", trackedArray); + + this.dependentArray = dependentArray; + this.index = index; + this.item = dependentArray.objectAt(index); + this.trackedArray = trackedArray; + this.beforeObserver = null; + this.observer = null; + + this.destroyed = false; + } + + DependentArraysObserver.prototype = { + setValue: function (newValue) { + this.instanceMeta.setValue(newValue, true); + }, + getValue: function () { + return this.instanceMeta.getValue(); + }, + + setupObservers: function (dependentArray, dependentKey) { + this.dependentKeysByGuid[guidFor(dependentArray)] = dependentKey; + + dependentArray.addArrayObserver(this, { + willChange: 'dependentArrayWillChange', + didChange: 'dependentArrayDidChange' + }); + + if (this.cp._itemPropertyKeys[dependentKey]) { + this.setupPropertyObservers(dependentKey, this.cp._itemPropertyKeys[dependentKey]); + } + }, + + teardownObservers: function (dependentArray, dependentKey) { + var itemPropertyKeys = this.cp._itemPropertyKeys[dependentKey] || []; + + delete this.dependentKeysByGuid[guidFor(dependentArray)]; + + this.teardownPropertyObservers(dependentKey, itemPropertyKeys); + + dependentArray.removeArrayObserver(this, { + willChange: 'dependentArrayWillChange', + didChange: 'dependentArrayDidChange' + }); + }, + + suspendArrayObservers: function (callback, binding) { + var oldSuspended = this.suspended; + this.suspended = true; + callback.call(binding); + this.suspended = oldSuspended; + }, + + setupPropertyObservers: function (dependentKey, itemPropertyKeys) { + var dependentArray = get(this.instanceMeta.context, dependentKey), + length = get(dependentArray, 'length'), + observerContexts = new Array(length); + + this.resetTransformations(dependentKey, observerContexts); + + forEach(dependentArray, function (item, index) { + var observerContext = this.createPropertyObserverContext(dependentArray, index, this.trackedArraysByGuid[dependentKey]); + observerContexts[index] = observerContext; + + forEach(itemPropertyKeys, function (propertyKey) { + addBeforeObserver(item, propertyKey, this, observerContext.beforeObserver); + addObserver(item, propertyKey, this, observerContext.observer); + }, this); + }, this); + }, + + teardownPropertyObservers: function (dependentKey, itemPropertyKeys) { + var dependentArrayObserver = this, + trackedArray = this.trackedArraysByGuid[dependentKey], + beforeObserver, + observer, + item; + + if (!trackedArray) { return; } + + trackedArray.apply(function (observerContexts, offset, operation) { + if (operation === TrackedArray.DELETE) { return; } + + forEach(observerContexts, function (observerContext) { + observerContext.destroyed = true; + beforeObserver = observerContext.beforeObserver; + observer = observerContext.observer; + item = observerContext.item; + + forEach(itemPropertyKeys, function (propertyKey) { + removeBeforeObserver(item, propertyKey, dependentArrayObserver, beforeObserver); + removeObserver(item, propertyKey, dependentArrayObserver, observer); + }); + }); + }); + }, + + createPropertyObserverContext: function (dependentArray, index, trackedArray) { + var observerContext = new ItemPropertyObserverContext(dependentArray, index, trackedArray); + + this.createPropertyObserver(observerContext); + + return observerContext; + }, + + createPropertyObserver: function (observerContext) { + var dependentArrayObserver = this; + + observerContext.beforeObserver = function (obj, keyName) { + return dependentArrayObserver.itemPropertyWillChange(obj, keyName, observerContext.dependentArray, observerContext); + }; + observerContext.observer = function (obj, keyName) { + return dependentArrayObserver.itemPropertyDidChange(obj, keyName, observerContext.dependentArray, observerContext); + }; + }, + + resetTransformations: function (dependentKey, observerContexts) { + this.trackedArraysByGuid[dependentKey] = new TrackedArray(observerContexts); + }, + + trackAdd: function (dependentKey, index, newItems) { + var trackedArray = this.trackedArraysByGuid[dependentKey]; + if (trackedArray) { + trackedArray.addItems(index, newItems); + } + }, + + trackRemove: function (dependentKey, index, removedCount) { + var trackedArray = this.trackedArraysByGuid[dependentKey]; + + if (trackedArray) { + return trackedArray.removeItems(index, removedCount); + } + + return []; + }, + + updateIndexes: function (trackedArray, array) { + var length = get(array, 'length'); + // OPTIMIZE: we could stop updating once we hit the object whose observer + // fired; ie partially apply the transformations + trackedArray.apply(function (observerContexts, offset, operation, operationIndex) { + // we don't even have observer contexts for removed items, even if we did, + // they no longer have any index in the array + if (operation === TrackedArray.DELETE) { return; } + if (operationIndex === 0 && operation === TrackedArray.RETAIN && observerContexts.length === length && offset === 0) { + // If we update many items we don't want to walk the array each time: we + // only need to update the indexes at most once per run loop. + return; + } + + forEach(observerContexts, function (context, index) { + context.index = index + offset; + }); + }); + }, + + dependentArrayWillChange: function (dependentArray, index, removedCount, addedCount) { + if (this.suspended) { return; } + + var removedItem = this.callbacks.removedItem, + changeMeta, + guid = guidFor(dependentArray), + dependentKey = this.dependentKeysByGuid[guid], + itemPropertyKeys = this.cp._itemPropertyKeys[dependentKey] || [], + length = get(dependentArray, 'length'), + normalizedIndex = normalizeIndex(index, length, 0), + normalizedRemoveCount = normalizeRemoveCount(normalizedIndex, length, removedCount), + item, + itemIndex, + sliceIndex, + observerContexts; + + observerContexts = this.trackRemove(dependentKey, normalizedIndex, normalizedRemoveCount); + + function removeObservers(propertyKey) { + observerContexts[sliceIndex].destroyed = true; + removeBeforeObserver(item, propertyKey, this, observerContexts[sliceIndex].beforeObserver); + removeObserver(item, propertyKey, this, observerContexts[sliceIndex].observer); + } + + for (sliceIndex = normalizedRemoveCount - 1; sliceIndex >= 0; --sliceIndex) { + itemIndex = normalizedIndex + sliceIndex; + if (itemIndex >= length) { break; } + + item = dependentArray.objectAt(itemIndex); + + forEach(itemPropertyKeys, removeObservers, this); + + changeMeta = createChangeMeta(dependentArray, item, itemIndex, this.instanceMeta.propertyName, this.cp); + this.setValue( removedItem.call( + this.instanceMeta.context, this.getValue(), item, changeMeta, this.instanceMeta.sugarMeta)); + } + }, + + dependentArrayDidChange: function (dependentArray, index, removedCount, addedCount) { + if (this.suspended) { return; } + + var addedItem = this.callbacks.addedItem, + guid = guidFor(dependentArray), + dependentKey = this.dependentKeysByGuid[guid], + observerContexts = new Array(addedCount), + itemPropertyKeys = this.cp._itemPropertyKeys[dependentKey], + length = get(dependentArray, 'length'), + normalizedIndex = normalizeIndex(index, length, addedCount), + changeMeta, + observerContext; + + forEach(dependentArray.slice(normalizedIndex, normalizedIndex + addedCount), function (item, sliceIndex) { + if (itemPropertyKeys) { + observerContext = + observerContexts[sliceIndex] = + this.createPropertyObserverContext(dependentArray, normalizedIndex + sliceIndex, this.trackedArraysByGuid[dependentKey]); + forEach(itemPropertyKeys, function (propertyKey) { + addBeforeObserver(item, propertyKey, this, observerContext.beforeObserver); + addObserver(item, propertyKey, this, observerContext.observer); + }, this); + } + + changeMeta = createChangeMeta(dependentArray, item, normalizedIndex + sliceIndex, this.instanceMeta.propertyName, this.cp); + this.setValue( addedItem.call( + this.instanceMeta.context, this.getValue(), item, changeMeta, this.instanceMeta.sugarMeta)); + }, this); + + this.trackAdd(dependentKey, normalizedIndex, observerContexts); + }, + + itemPropertyWillChange: function (obj, keyName, array, observerContext) { + var guid = guidFor(obj); + + if (!this.changedItems[guid]) { + this.changedItems[guid] = { + array: array, + observerContext: observerContext, + obj: obj, + previousValues: {} + }; + } + ++this.changedItemCount; + + this.changedItems[guid].previousValues[keyName] = get(obj, keyName); + }, + + itemPropertyDidChange: function(obj, keyName, array, observerContext) { + if (--this.changedItemCount === 0) { + this.flushChanges(); + } + }, + + flushChanges: function() { + var changedItems = this.changedItems, key, c, changeMeta; + + for (key in changedItems) { + c = changedItems[key]; + if (c.observerContext.destroyed) { continue; } + + this.updateIndexes(c.observerContext.trackedArray, c.observerContext.dependentArray); + + changeMeta = createChangeMeta(c.array, c.obj, c.observerContext.index, this.instanceMeta.propertyName, this.cp, c.previousValues); + this.setValue( + this.callbacks.removedItem.call(this.instanceMeta.context, this.getValue(), c.obj, changeMeta, this.instanceMeta.sugarMeta)); + this.setValue( + this.callbacks.addedItem.call(this.instanceMeta.context, this.getValue(), c.obj, changeMeta, this.instanceMeta.sugarMeta)); + } + this.changedItems = {}; + } + }; + + function normalizeIndex(index, length, newItemsOffset) { + if (index < 0) { + return Math.max(0, length + index); + } else if (index < length) { + return index; + } else /* index > length */ { + return Math.min(length - newItemsOffset, index); + } + } + + function normalizeRemoveCount(index, length, removedCount) { + return Math.min(removedCount, length - index); + } + + function createChangeMeta(dependentArray, item, index, propertyName, property, previousValues) { + var meta = { + arrayChanged: dependentArray, + index: index, + item: item, + propertyName: propertyName, + property: property + }; + + if (previousValues) { + // previous values only available for item property changes + meta.previousValues = previousValues; + } + + return meta; + } + + function addItems (dependentArray, callbacks, cp, propertyName, meta) { + forEach(dependentArray, function (item, index) { + meta.setValue( callbacks.addedItem.call( + this, meta.getValue(), item, createChangeMeta(dependentArray, item, index, propertyName, cp), meta.sugarMeta)); + }, this); + } + + function reset(cp, propertyName) { + var callbacks = cp._callbacks(), + meta; + + if (cp._hasInstanceMeta(this, propertyName)) { + meta = cp._instanceMeta(this, propertyName); + meta.setValue(cp.resetValue(meta.getValue())); + } else { + meta = cp._instanceMeta(this, propertyName); + } + + if (cp.options.initialize) { + cp.options.initialize.call(this, meta.getValue(), { property: cp, propertyName: propertyName }, meta.sugarMeta); + } + } + + function partiallyRecomputeFor(obj, dependentKey) { + if (arrayBracketPattern.test(dependentKey)) { + return false; + } + + var value = get(obj, dependentKey); + return EmberArray.detect(value); + } + + function ReduceComputedPropertyInstanceMeta(context, propertyName, initialValue) { + this.context = context; + this.propertyName = propertyName; + this.cache = metaFor(context).cache; + + this.dependentArrays = {}; + this.sugarMeta = {}; + + this.initialValue = initialValue; + } + + ReduceComputedPropertyInstanceMeta.prototype = { + getValue: function () { + var value = cacheGet(this.cache, this.propertyName); + if (value !== undefined) { + return value; + } else { + return this.initialValue; + } + }, + + setValue: function(newValue, triggerObservers) { + // This lets sugars force a recomputation, handy for very simple + // implementations of eg max. + if (newValue === cacheGet(this.cache, this.propertyName)) { + return; + } + + if (triggerObservers) { + propertyWillChange(this.context, this.propertyName); + } + + if (newValue === undefined) { + cacheRemove(this.cache, this.propertyName); + } else { + cacheSet(this.cache, this.propertyName, newValue); + } + + if (triggerObservers) { + propertyDidChange(this.context, this.propertyName); + } + } + }; + + /** + A computed property whose dependent keys are arrays and which is updated with + "one at a time" semantics. + + @class ReduceComputedProperty + @namespace Ember + @extends Ember.ComputedProperty + @constructor + */ + function ReduceComputedProperty(options) { + var cp = this; + + this.options = options; + + this._dependentKeys = null; + // A map of dependentKey -> [itemProperty, ...] that tracks what properties of + // items in the array we must track to update this property. + this._itemPropertyKeys = {}; + this._previousItemPropertyKeys = {}; + + this.readOnly(); + this.cacheable(); + + this.recomputeOnce = function(propertyName) { + // What we really want to do is coalesce by . + // We need a form of `scheduleOnce` that accepts an arbitrary token to + // coalesce by, in addition to the target and method. + run.once(this, recompute, propertyName); + }; + var recompute = function(propertyName) { + var dependentKeys = cp._dependentKeys, + meta = cp._instanceMeta(this, propertyName), + callbacks = cp._callbacks(); + + reset.call(this, cp, propertyName); + + meta.dependentArraysObserver.suspendArrayObservers(function () { + forEach(cp._dependentKeys, function (dependentKey) { + Ember.assert( + "dependent array " + dependentKey + " must be an `Ember.Array`. " + + "If you are not extending arrays, you will need to wrap native arrays with `Ember.A`", + !(isArray(get(this, dependentKey)) && !EmberArray.detect(get(this, dependentKey)))); + + if (!partiallyRecomputeFor(this, dependentKey)) { return; } + + var dependentArray = get(this, dependentKey), + previousDependentArray = meta.dependentArrays[dependentKey]; + + if (dependentArray === previousDependentArray) { + // The array may be the same, but our item property keys may have + // changed, so we set them up again. We can't easily tell if they've + // changed: the array may be the same object, but with different + // contents. + if (cp._previousItemPropertyKeys[dependentKey]) { + delete cp._previousItemPropertyKeys[dependentKey]; + meta.dependentArraysObserver.setupPropertyObservers(dependentKey, cp._itemPropertyKeys[dependentKey]); + } + } else { + meta.dependentArrays[dependentKey] = dependentArray; + + if (previousDependentArray) { + meta.dependentArraysObserver.teardownObservers(previousDependentArray, dependentKey); + } + + if (dependentArray) { + meta.dependentArraysObserver.setupObservers(dependentArray, dependentKey); + } + } + }, this); + }, this); + + forEach(cp._dependentKeys, function(dependentKey) { + if (!partiallyRecomputeFor(this, dependentKey)) { return; } + + var dependentArray = get(this, dependentKey); + if (dependentArray) { + addItems.call(this, dependentArray, callbacks, cp, propertyName, meta); + } + }, this); + }; + + + this.func = function (propertyName) { + Ember.assert("Computed reduce values require at least one dependent key", cp._dependentKeys); + + recompute.call(this, propertyName); + + return cp._instanceMeta(this, propertyName).getValue(); + }; + } + + ReduceComputedProperty.prototype = o_create(ComputedProperty.prototype); + + function defaultCallback(computedValue) { + return computedValue; + } + + ReduceComputedProperty.prototype._callbacks = function () { + if (!this.callbacks) { + var options = this.options; + this.callbacks = { + removedItem: options.removedItem || defaultCallback, + addedItem: options.addedItem || defaultCallback + }; + } + return this.callbacks; + }; + + ReduceComputedProperty.prototype._hasInstanceMeta = function (context, propertyName) { + return !!metaFor(context).cacheMeta[propertyName]; + }; + + ReduceComputedProperty.prototype._instanceMeta = function (context, propertyName) { + var cacheMeta = metaFor(context).cacheMeta, + meta = cacheMeta[propertyName]; + + if (!meta) { + meta = cacheMeta[propertyName] = new ReduceComputedPropertyInstanceMeta(context, propertyName, this.initialValue()); + meta.dependentArraysObserver = new DependentArraysObserver(this._callbacks(), this, meta, context, propertyName, meta.sugarMeta); + } + + return meta; + }; + + ReduceComputedProperty.prototype.initialValue = function () { + if (typeof this.options.initialValue === 'function') { + return this.options.initialValue(); + } + else { + return this.options.initialValue; + } + }; + + ReduceComputedProperty.prototype.resetValue = function (value) { + return this.initialValue(); + }; + + ReduceComputedProperty.prototype.itemPropertyKey = function (dependentArrayKey, itemPropertyKey) { + this._itemPropertyKeys[dependentArrayKey] = this._itemPropertyKeys[dependentArrayKey] || []; + this._itemPropertyKeys[dependentArrayKey].push(itemPropertyKey); + }; + + ReduceComputedProperty.prototype.clearItemPropertyKeys = function (dependentArrayKey) { + if (this._itemPropertyKeys[dependentArrayKey]) { + this._previousItemPropertyKeys[dependentArrayKey] = this._itemPropertyKeys[dependentArrayKey]; + this._itemPropertyKeys[dependentArrayKey] = []; + } + }; + + ReduceComputedProperty.prototype.property = function () { + var cp = this, + args = a_slice.call(arguments), + propertyArgs = new Set(), + match, + dependentArrayKey, + itemPropertyKey; + + forEach(args, function (dependentKey) { + if (doubleEachPropertyPattern.test(dependentKey)) { + throw new EmberError("Nested @each properties not supported: " + dependentKey); + } else if (match = eachPropertyPattern.exec(dependentKey)) { + dependentArrayKey = match[1]; + + var itemPropertyKeyPattern = match[2], + addItemPropertyKey = function (itemPropertyKey) { + cp.itemPropertyKey(dependentArrayKey, itemPropertyKey); + }; + + expandProperties(itemPropertyKeyPattern, addItemPropertyKey); + propertyArgs.add(dependentArrayKey); + } else { + propertyArgs.add(dependentKey); + } + }); + + return ComputedProperty.prototype.property.apply(this, propertyArgs.toArray()); + + }; + + /** + Creates a computed property which operates on dependent arrays and + is updated with "one at a time" semantics. When items are added or + removed from the dependent array(s) a reduce computed only operates + on the change instead of re-evaluating the entire array. + + If there are more than one arguments the first arguments are + considered to be dependent property keys. The last argument is + required to be an options object. The options object can have the + following four properties: + + `initialValue` - A value or function that will be used as the initial + value for the computed. If this property is a function the result of calling + the function will be used as the initial value. This property is required. + + `initialize` - An optional initialize function. Typically this will be used + to set up state on the instanceMeta object. + + `removedItem` - A function that is called each time an element is removed + from the array. + + `addedItem` - A function that is called each time an element is added to + the array. + + + The `initialize` function has the following signature: + + ```javascript + function(initialValue, changeMeta, instanceMeta) + ``` + + `initialValue` - The value of the `initialValue` property from the + options object. + + `changeMeta` - An object which contains meta information about the + computed. It contains the following properties: + + - `property` the computed property + - `propertyName` the name of the property on the object + + `instanceMeta` - An object that can be used to store meta + information needed for calculating your computed. For example a + unique computed might use this to store the number of times a given + element is found in the dependent array. + + + The `removedItem` and `addedItem` functions both have the following signature: + + ```javascript + function(accumulatedValue, item, changeMeta, instanceMeta) + ``` + + `accumulatedValue` - The value returned from the last time + `removedItem` or `addedItem` was called or `initialValue`. + + `item` - the element added or removed from the array + + `changeMeta` - An object which contains meta information about the + change. It contains the following properties: + + - `property` the computed property + - `propertyName` the name of the property on the object + - `index` the index of the added or removed item + - `item` the added or removed item: this is exactly the same as + the second arg + - `arrayChanged` the array that triggered the change. Can be + useful when depending on multiple arrays. + + For property changes triggered on an item property change (when + depKey is something like `someArray.@each.someProperty`), + `changeMeta` will also contain the following property: + + - `previousValues` an object whose keys are the properties that changed on + the item, and whose values are the item's previous values. + + `previousValues` is important Ember coalesces item property changes via + Ember.run.once. This means that by the time removedItem gets called, item has + the new values, but you may need the previous value (eg for sorting & + filtering). + + `instanceMeta` - An object that can be used to store meta + information needed for calculating your computed. For example a + unique computed might use this to store the number of times a given + element is found in the dependent array. + + The `removedItem` and `addedItem` functions should return the accumulated + value. It is acceptable to not return anything (ie return undefined) + to invalidate the computation. This is generally not a good idea for + arrayComputed but it's used in eg max and min. + + Note that observers will be fired if either of these functions return a value + that differs from the accumulated value. When returning an object that + mutates in response to array changes, for example an array that maps + everything from some other array (see `Ember.computed.map`), it is usually + important that the *same* array be returned to avoid accidentally triggering observers. + + Example + + ```javascript + Ember.computed.max = function(dependentKey) { + return Ember.reduceComputed(dependentKey, { + initialValue: -Infinity, + + addedItem: function(accumulatedValue, item, changeMeta, instanceMeta) { + return Math.max(accumulatedValue, item); + }, + + removedItem: function(accumulatedValue, item, changeMeta, instanceMeta) { + if (item < accumulatedValue) { + return accumulatedValue; + } + } + }); + }; + ``` + + Dependent keys may refer to `@this` to observe changes to the object itself, + which must be array-like, rather than a property of the object. This is + mostly useful for array proxies, to ensure objects are retrieved via + `objectAtContent`. This is how you could sort items by properties defined on an item controller. + + Example + + ```javascript + App.PeopleController = Ember.ArrayController.extend({ + itemController: 'person', + + sortedPeople: Ember.computed.sort('@this.@each.reversedName', function(personA, personB) { + // `reversedName` isn't defined on Person, but we have access to it via + // the item controller App.PersonController. If we'd used + // `content.@each.reversedName` above, we would be getting the objects + // directly and not have access to `reversedName`. + // + var reversedNameA = get(personA, 'reversedName'), + reversedNameB = get(personB, 'reversedName'); + + return Ember.compare(reversedNameA, reversedNameB); + }) + }); + + App.PersonController = Ember.ObjectController.extend({ + reversedName: function() { + return reverse(get(this, 'name')); + }.property('name') + }); + ``` + + Dependent keys whose values are not arrays are treated as regular + dependencies: when they change, the computed property is completely + recalculated. It is sometimes useful to have dependent arrays with similar + semantics. Dependent keys which end in `.[]` do not use "one at a time" + semantics. When an item is added or removed from such a dependency, the + computed property is completely recomputed. + + When the computed property is completely recomputed, the `accumulatedValue` + is discarded, it starts with `initialValue` again, and each item is passed + to `addedItem` in turn. + + Example + + ```javascript + Ember.Object.extend({ + // When `string` is changed, `computed` is completely recomputed. + string: 'a string', + + // When an item is added to `array`, `addedItem` is called. + array: [], + + // When an item is added to `anotherArray`, `computed` is completely + // recomputed. + anotherArray: [], + + computed: Ember.reduceComputed('string', 'array', 'anotherArray.[]', { + addedItem: addedItemCallback, + removedItem: removedItemCallback + }) + }); + ``` + + @method reduceComputed + @for Ember + @param {String} [dependentKeys*] + @param {Object} options + @return {Ember.ComputedProperty} + */ + function reduceComputed(options) { + var args; + + if (arguments.length > 1) { + args = a_slice.call(arguments, 0, -1); + options = a_slice.call(arguments, -1)[0]; + } + + if (typeof options !== "object") { + throw new EmberError("Reduce Computed Property declared without an options hash"); + } + + if (!('initialValue' in options)) { + throw new EmberError("Reduce Computed Property declared without an initial value"); + } + + var cp = new ReduceComputedProperty(options); + + if (args) { + cp.property.apply(cp, args); + } + + return cp; + } + + __exports__.reduceComputed = reduceComputed; + __exports__.ReduceComputedProperty = ReduceComputedProperty; + }); +define("ember-runtime/computed/reduce_computed_macros", + ["ember-metal/core","ember-metal/merge","ember-metal/property_get","ember-metal/property_set","ember-metal/utils","ember-metal/error","ember-metal/enumerable_utils","ember-metal/run_loop","ember-metal/observer","ember-runtime/computed/array_computed","ember-runtime/computed/reduce_computed","ember-runtime/system/object_proxy","ember-runtime/system/subarray","ember-runtime/keys","ember-runtime/compare","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __dependency11__, __dependency12__, __dependency13__, __dependency14__, __dependency15__, __exports__) { + "use strict"; + /** + @module ember + @submodule ember-runtime + */ + + var Ember = __dependency1__["default"]; + // Ember.assert + var merge = __dependency2__["default"]; + var get = __dependency3__.get; + var set = __dependency4__.set; + var isArray = __dependency5__.isArray; + var guidFor = __dependency5__.guidFor; + var EmberError = __dependency6__["default"]; + var EnumerableUtils = __dependency7__["default"]; + var run = __dependency8__["default"]; + var addObserver = __dependency9__.addObserver; + var arrayComputed = __dependency10__.arrayComputed; + var reduceComputed = __dependency11__.reduceComputed; + var ObjectProxy = __dependency12__["default"]; + var SubArray = __dependency13__["default"]; + var keys = __dependency14__["default"]; + var compare = __dependency15__["default"]; + + var a_slice = [].slice, + forEach = EnumerableUtils.forEach, + SearchProxy; + + /** + A computed property that returns the sum of the value + in the dependent array. + + @method computed.sum + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computes the sum of all values in the dependentKey's array + @since 1.4.0 + */ + + function sum(dependentKey){ + return reduceComputed(dependentKey, { + initialValue: 0, + + addedItem: function(accumulatedValue, item, changeMeta, instanceMeta){ + return accumulatedValue + item; + }, + + removedItem: function(accumulatedValue, item, changeMeta, instanceMeta){ + return accumulatedValue - item; + } + }); + }; + + /** + A computed property that calculates the maximum value in the + dependent array. This will return `-Infinity` when the dependent + array is empty. + + ```javascript + var Person = Ember.Object.extend({ + childAges: Ember.computed.mapBy('children', 'age'), + maxChildAge: Ember.computed.max('childAges') + }); + + var lordByron = Person.create({ children: [] }); + + lordByron.get('maxChildAge'); // -Infinity + lordByron.get('children').pushObject({ + name: 'Augusta Ada Byron', age: 7 + }); + lordByron.get('maxChildAge'); // 7 + lordByron.get('children').pushObjects([{ + name: 'Allegra Byron', + age: 5 + }, { + name: 'Elizabeth Medora Leigh', + age: 8 + }]); + lordByron.get('maxChildAge'); // 8 + ``` + + @method computed.max + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computes the largest value in the dependentKey's array + */ + function max (dependentKey) { + return reduceComputed(dependentKey, { + initialValue: -Infinity, + + addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + return Math.max(accumulatedValue, item); + }, + + removedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + if (item < accumulatedValue) { + return accumulatedValue; + } + } + }); + }; + + /** + A computed property that calculates the minimum value in the + dependent array. This will return `Infinity` when the dependent + array is empty. + + ```javascript + var Person = Ember.Object.extend({ + childAges: Ember.computed.mapBy('children', 'age'), + minChildAge: Ember.computed.min('childAges') + }); + + var lordByron = Person.create({ children: [] }); + + lordByron.get('minChildAge'); // Infinity + lordByron.get('children').pushObject({ + name: 'Augusta Ada Byron', age: 7 + }); + lordByron.get('minChildAge'); // 7 + lordByron.get('children').pushObjects([{ + name: 'Allegra Byron', + age: 5 + }, { + name: 'Elizabeth Medora Leigh', + age: 8 + }]); + lordByron.get('minChildAge'); // 5 + ``` + + @method computed.min + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computes the smallest value in the dependentKey's array + */ + function min(dependentKey) { + return reduceComputed(dependentKey, { + initialValue: Infinity, + + addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + return Math.min(accumulatedValue, item); + }, + + removedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + if (item > accumulatedValue) { + return accumulatedValue; + } + } + }); + }; + + /** + Returns an array mapped via the callback + + The callback method you provide should have the following signature. + `item` is the current item in the iteration. + + ```javascript + function(item); + ``` + + Example + + ```javascript + var Hamster = Ember.Object.extend({ + excitingChores: Ember.computed.map('chores', function(chore) { + return chore.toUpperCase() + '!'; + }) + }); + + var hamster = Hamster.create({ + chores: ['clean', 'write more unit tests'] + }); + + hamster.get('excitingChores'); // ['CLEAN!', 'WRITE MORE UNIT TESTS!'] + ``` + + @method computed.map + @for Ember + @param {String} dependentKey + @param {Function} callback + @return {Ember.ComputedProperty} an array mapped via the callback + */ + function map(dependentKey, callback) { + var options = { + addedItem: function(array, item, changeMeta, instanceMeta) { + var mapped = callback.call(this, item); + array.insertAt(changeMeta.index, mapped); + return array; + }, + removedItem: function(array, item, changeMeta, instanceMeta) { + array.removeAt(changeMeta.index, 1); + return array; + } + }; + + return arrayComputed(dependentKey, options); + }; + + /** + Returns an array mapped to the specified key. + + ```javascript + var Person = Ember.Object.extend({ + childAges: Ember.computed.mapBy('children', 'age') + }); + + var lordByron = Person.create({ children: [] }); + + lordByron.get('childAges'); // [] + lordByron.get('children').pushObject({ name: 'Augusta Ada Byron', age: 7 }); + lordByron.get('childAges'); // [7] + lordByron.get('children').pushObjects([{ + name: 'Allegra Byron', + age: 5 + }, { + name: 'Elizabeth Medora Leigh', + age: 8 + }]); + lordByron.get('childAges'); // [7, 5, 8] + ``` + + @method computed.mapBy + @for Ember + @param {String} dependentKey + @param {String} propertyKey + @return {Ember.ComputedProperty} an array mapped to the specified key + */ + function mapBy (dependentKey, propertyKey) { + var callback = function(item) { return get(item, propertyKey); }; + return map(dependentKey + '.@each.' + propertyKey, callback); + }; + + /** + @method computed.mapProperty + @for Ember + @deprecated Use `Ember.computed.mapBy` instead + @param dependentKey + @param propertyKey + */ + var mapProperty = mapBy; + + /** + Filters the array by the callback. + + The callback method you provide should have the following signature. + `item` is the current item in the iteration. + + ```javascript + function(item); + ``` + + ```javascript + var Hamster = Ember.Object.extend({ + remainingChores: Ember.computed.filter('chores', function(chore) { + return !chore.done; + }) + }); + + var hamster = Hamster.create({ + chores: [ + { name: 'cook', done: true }, + { name: 'clean', done: true }, + { name: 'write more unit tests', done: false } + ] + }); + + hamster.get('remainingChores'); // [{name: 'write more unit tests', done: false}] + ``` + + @method computed.filter + @for Ember + @param {String} dependentKey + @param {Function} callback + @return {Ember.ComputedProperty} the filtered array + */ + function filter(dependentKey, callback) { + var options = { + initialize: function (array, changeMeta, instanceMeta) { + instanceMeta.filteredArrayIndexes = new SubArray(); + }, + + addedItem: function(array, item, changeMeta, instanceMeta) { + var match = !!callback.call(this, item), + filterIndex = instanceMeta.filteredArrayIndexes.addItem(changeMeta.index, match); + + if (match) { + array.insertAt(filterIndex, item); + } + + return array; + }, + + removedItem: function(array, item, changeMeta, instanceMeta) { + var filterIndex = instanceMeta.filteredArrayIndexes.removeItem(changeMeta.index); + + if (filterIndex > -1) { + array.removeAt(filterIndex); + } + + return array; + } + }; + + return arrayComputed(dependentKey, options); + }; + + /** + Filters the array by the property and value + + ```javascript + var Hamster = Ember.Object.extend({ + remainingChores: Ember.computed.filterBy('chores', 'done', false) + }); + + var hamster = Hamster.create({ + chores: [ + { name: 'cook', done: true }, + { name: 'clean', done: true }, + { name: 'write more unit tests', done: false } + ] + }); + + hamster.get('remainingChores'); // [{ name: 'write more unit tests', done: false }] + ``` + + @method computed.filterBy + @for Ember + @param {String} dependentKey + @param {String} propertyKey + @param {*} value + @return {Ember.ComputedProperty} the filtered array + */ + function filterBy (dependentKey, propertyKey, value) { + var callback; + + if (arguments.length === 2) { + callback = function(item) { + return get(item, propertyKey); + }; + } else { + callback = function(item) { + return get(item, propertyKey) === value; + }; + } + + return filter(dependentKey + '.@each.' + propertyKey, callback); + }; + + /** + @method computed.filterProperty + @for Ember + @param dependentKey + @param propertyKey + @param value + @deprecated Use `Ember.computed.filterBy` instead + */ + var filterProperty = filterBy; + + /** + A computed property which returns a new array with all the unique + elements from one or more dependent arrays. + + Example + + ```javascript + var Hamster = Ember.Object.extend({ + uniqueFruits: Ember.computed.uniq('fruits') + }); + + var hamster = Hamster.create({ + fruits: [ + 'banana', + 'grape', + 'kale', + 'banana' + ] + }); + + hamster.get('uniqueFruits'); // ['banana', 'grape', 'kale'] + ``` + + @method computed.uniq + @for Ember + @param {String} propertyKey* + @return {Ember.ComputedProperty} computes a new array with all the + unique elements from the dependent array + */ + function uniq() { + var args = a_slice.call(arguments); + args.push({ + initialize: function(array, changeMeta, instanceMeta) { + instanceMeta.itemCounts = {}; + }, + + addedItem: function(array, item, changeMeta, instanceMeta) { + var guid = guidFor(item); + + if (!instanceMeta.itemCounts[guid]) { + instanceMeta.itemCounts[guid] = 1; + } else { + ++instanceMeta.itemCounts[guid]; + } + array.addObject(item); + return array; + }, + removedItem: function(array, item, _, instanceMeta) { + var guid = guidFor(item), + itemCounts = instanceMeta.itemCounts; + + if (--itemCounts[guid] === 0) { + array.removeObject(item); + } + return array; + } + }); + return arrayComputed.apply(null, args); + }; + + /** + Alias for [Ember.computed.uniq](/api/#method_computed_uniq). + + @method computed.union + @for Ember + @param {String} propertyKey* + @return {Ember.ComputedProperty} computes a new array with all the + unique elements from the dependent array + */ + var union = uniq; + + /** + A computed property which returns a new array with all the duplicated + elements from two or more dependent arrays. + + Example + + ```javascript + var obj = Ember.Object.createWithMixins({ + adaFriends: ['Charles Babbage', 'John Hobhouse', 'William King', 'Mary Somerville'], + charlesFriends: ['William King', 'Mary Somerville', 'Ada Lovelace', 'George Peacock'], + friendsInCommon: Ember.computed.intersect('adaFriends', 'charlesFriends') + }); + + obj.get('friendsInCommon'); // ['William King', 'Mary Somerville'] + ``` + + @method computed.intersect + @for Ember + @param {String} propertyKey* + @return {Ember.ComputedProperty} computes a new array with all the + duplicated elements from the dependent arrays + */ + function intersect() { + var getDependentKeyGuids = function (changeMeta) { + return EnumerableUtils.map(changeMeta.property._dependentKeys, function (dependentKey) { + return guidFor(dependentKey); + }); + }; + + var args = a_slice.call(arguments); + args.push({ + initialize: function (array, changeMeta, instanceMeta) { + instanceMeta.itemCounts = {}; + }, + + addedItem: function(array, item, changeMeta, instanceMeta) { + var itemGuid = guidFor(item), + dependentGuids = getDependentKeyGuids(changeMeta), + dependentGuid = guidFor(changeMeta.arrayChanged), + numberOfDependentArrays = changeMeta.property._dependentKeys.length, + itemCounts = instanceMeta.itemCounts; + + if (!itemCounts[itemGuid]) { itemCounts[itemGuid] = {}; } + if (itemCounts[itemGuid][dependentGuid] === undefined) { itemCounts[itemGuid][dependentGuid] = 0; } + + if (++itemCounts[itemGuid][dependentGuid] === 1 && + numberOfDependentArrays === keys(itemCounts[itemGuid]).length) { + + array.addObject(item); + } + return array; + }, + removedItem: function(array, item, changeMeta, instanceMeta) { + var itemGuid = guidFor(item), + dependentGuids = getDependentKeyGuids(changeMeta), + dependentGuid = guidFor(changeMeta.arrayChanged), + numberOfDependentArrays = changeMeta.property._dependentKeys.length, + numberOfArraysItemAppearsIn, + itemCounts = instanceMeta.itemCounts; + + if (itemCounts[itemGuid][dependentGuid] === undefined) { itemCounts[itemGuid][dependentGuid] = 0; } + if (--itemCounts[itemGuid][dependentGuid] === 0) { + delete itemCounts[itemGuid][dependentGuid]; + numberOfArraysItemAppearsIn = keys(itemCounts[itemGuid]).length; + + if (numberOfArraysItemAppearsIn === 0) { + delete itemCounts[itemGuid]; + } + array.removeObject(item); + } + return array; + } + }); + return arrayComputed.apply(null, args); + }; + + /** + A computed property which returns a new array with all the + properties from the first dependent array that are not in the second + dependent array. + + Example + + ```javascript + var Hamster = Ember.Object.extend({ + likes: ['banana', 'grape', 'kale'], + wants: Ember.computed.setDiff('likes', 'fruits') + }); + + var hamster = Hamster.create({ + fruits: [ + 'grape', + 'kale', + ] + }); + + hamster.get('wants'); // ['banana'] + ``` + + @method computed.setDiff + @for Ember + @param {String} setAProperty + @param {String} setBProperty + @return {Ember.ComputedProperty} computes a new array with all the + items from the first dependent array that are not in the second + dependent array + */ + function setDiff(setAProperty, setBProperty) { + if (arguments.length !== 2) { + throw new EmberError("setDiff requires exactly two dependent arrays."); + } + return arrayComputed(setAProperty, setBProperty, { + addedItem: function (array, item, changeMeta, instanceMeta) { + var setA = get(this, setAProperty), + setB = get(this, setBProperty); + + if (changeMeta.arrayChanged === setA) { + if (!setB.contains(item)) { + array.addObject(item); + } + } else { + array.removeObject(item); + } + return array; + }, + + removedItem: function (array, item, changeMeta, instanceMeta) { + var setA = get(this, setAProperty), + setB = get(this, setBProperty); + + if (changeMeta.arrayChanged === setB) { + if (setA.contains(item)) { + array.addObject(item); + } + } else { + array.removeObject(item); + } + return array; + } + }); + }; + + function binarySearch(array, item, low, high) { + var mid, midItem, res, guidMid, guidItem; + + if (arguments.length < 4) { high = get(array, 'length'); } + if (arguments.length < 3) { low = 0; } + + if (low === high) { + return low; + } + + mid = low + Math.floor((high - low) / 2); + midItem = array.objectAt(mid); + + guidMid = _guidFor(midItem); + guidItem = _guidFor(item); + + if (guidMid === guidItem) { + return mid; + } + + res = this.order(midItem, item); + if (res === 0) { + res = guidMid < guidItem ? -1 : 1; + } + + + if (res < 0) { + return this.binarySearch(array, item, mid+1, high); + } else if (res > 0) { + return this.binarySearch(array, item, low, mid); + } + + return mid; + + function _guidFor(item) { + if (SearchProxy.detectInstance(item)) { + return guidFor(get(item, 'content')); + } + return guidFor(item); + } + } + + + var SearchProxy = ObjectProxy.extend(); + + /** + A computed property which returns a new array with all the + properties from the first dependent array sorted based on a property + or sort function. + + The callback method you provide should have the following signature: + + ```javascript + function(itemA, itemB); + ``` + + - `itemA` the first item to compare. + - `itemB` the second item to compare. + + This function should return negative number (e.g. `-1`) when `itemA` should come before + `itemB`. It should return positive number (e.g. `1`) when `itemA` should come after + `itemB`. If the `itemA` and `itemB` are equal this function should return `0`. + + Therefore, if this function is comparing some numeric values, simple `itemA - itemB` or + `itemA.get( 'foo' ) - itemB.get( 'foo' )` can be used instead of series of `if`. + + Example + + ```javascript + var ToDoList = Ember.Object.extend({ + // using standard ascending sort + todosSorting: ['name'], + sortedTodos: Ember.computed.sort('todos', 'todosSorting'), + + // using descending sort + todosSortingDesc: ['name:desc'], + sortedTodosDesc: Ember.computed.sort('todos', 'todosSortingDesc'), + + // using a custom sort function + priorityTodos: Ember.computed.sort('todos', function(a, b){ + if (a.priority > b.priority) { + return 1; + } else if (a.priority < b.priority) { + return -1; + } + + return 0; + }), + }); + + var todoList = ToDoList.create({todos: [ + { name: 'Unit Test', priority: 2 }, + { name: 'Documentation', priority: 3 }, + { name: 'Release', priority: 1 } + ]}); + + todoList.get('sortedTodos'); // [{ name:'Documentation', priority:3 }, { name:'Release', priority:1 }, { name:'Unit Test', priority:2 }] + todoList.get('sortedTodosDesc'); // [{ name:'Unit Test', priority:2 }, { name:'Release', priority:1 }, { name:'Documentation', priority:3 }] + todoList.get('priorityTodos'); // [{ name:'Release', priority:1 }, { name:'Unit Test', priority:2 }, { name:'Documentation', priority:3 }] + ``` + + @method computed.sort + @for Ember + @param {String} dependentKey + @param {String or Function} sortDefinition a dependent key to an + array of sort properties (add `:desc` to the arrays sort properties to sort descending) or a function to use when sorting + @return {Ember.ComputedProperty} computes a new sorted array based + on the sort property array or callback function + */ + function sort(itemsKey, sortDefinition) { + Ember.assert("Ember.computed.sort requires two arguments: an array key to sort and either a sort properties key or sort function", arguments.length === 2); + + var initFn, sortPropertiesKey; + + if (typeof sortDefinition === 'function') { + initFn = function (array, changeMeta, instanceMeta) { + instanceMeta.order = sortDefinition; + instanceMeta.binarySearch = binarySearch; + }; + } else { + sortPropertiesKey = sortDefinition; + initFn = function (array, changeMeta, instanceMeta) { + function setupSortProperties() { + var sortPropertyDefinitions = get(this, sortPropertiesKey), + sortProperty, + sortProperties = instanceMeta.sortProperties = [], + sortPropertyAscending = instanceMeta.sortPropertyAscending = {}, + idx, + asc; + + Ember.assert("Cannot sort: '" + sortPropertiesKey + "' is not an array.", isArray(sortPropertyDefinitions)); + + changeMeta.property.clearItemPropertyKeys(itemsKey); + + forEach(sortPropertyDefinitions, function (sortPropertyDefinition) { + if ((idx = sortPropertyDefinition.indexOf(':')) !== -1) { + sortProperty = sortPropertyDefinition.substring(0, idx); + asc = sortPropertyDefinition.substring(idx+1).toLowerCase() !== 'desc'; + } else { + sortProperty = sortPropertyDefinition; + asc = true; + } + + sortProperties.push(sortProperty); + sortPropertyAscending[sortProperty] = asc; + changeMeta.property.itemPropertyKey(itemsKey, sortProperty); + }); + + sortPropertyDefinitions.addObserver('@each', this, updateSortPropertiesOnce); + } + + function updateSortPropertiesOnce() { + run.once(this, updateSortProperties, changeMeta.propertyName); + } + + function updateSortProperties(propertyName) { + setupSortProperties.call(this); + changeMeta.property.recomputeOnce.call(this, propertyName); + } + + addObserver(this, sortPropertiesKey, updateSortPropertiesOnce); + + setupSortProperties.call(this); + + + instanceMeta.order = function (itemA, itemB) { + var isProxy = itemB instanceof SearchProxy, + sortProperty, result, asc; + + for (var i = 0; i < this.sortProperties.length; ++i) { + sortProperty = this.sortProperties[i]; + result = compare(get(itemA, sortProperty), isProxy ? itemB[sortProperty] : get(itemB, sortProperty)); + + if (result !== 0) { + asc = this.sortPropertyAscending[sortProperty]; + return asc ? result : (-1 * result); + } + } + + return 0; + }; + + instanceMeta.binarySearch = binarySearch; + }; + } + + return arrayComputed(itemsKey, { + initialize: initFn, + + addedItem: function (array, item, changeMeta, instanceMeta) { + var index = instanceMeta.binarySearch(array, item); + array.insertAt(index, item); + return array; + }, + + removedItem: function (array, item, changeMeta, instanceMeta) { + var proxyProperties, index, searchItem; + + if (changeMeta.previousValues) { + proxyProperties = merge({ content: item }, changeMeta.previousValues); + + searchItem = SearchProxy.create(proxyProperties); + } else { + searchItem = item; + } + + index = instanceMeta.binarySearch(array, searchItem); + array.removeAt(index); + return array; + } + }); + }; + + + __exports__.sum = sum; + __exports__.min = min; + __exports__.max = max; + __exports__.map = map; + __exports__.sort = sort; + __exports__.setDiff = setDiff; + __exports__.mapBy = mapBy; + __exports__.mapProperty = mapProperty; + __exports__.filter = filter; + __exports__.filterBy = filterBy; + __exports__.filterProperty = filterProperty; + __exports__.uniq = uniq; + __exports__.union = union; + __exports__.intersect = intersect; + }); +define("ember-runtime/controllers/array_controller", + ["ember-metal/core","ember-metal/property_get","ember-metal/property_set","ember-metal/enumerable_utils","ember-runtime/system/array_proxy","ember-runtime/mixins/sortable","ember-runtime/controllers/controller","ember-metal/computed","ember-metal/error","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __exports__) { + "use strict"; + /** + @module ember + @submodule ember-runtime + */ + + var Ember = __dependency1__["default"]; + var get = __dependency2__.get; + var set = __dependency3__.set; + var EnumerableUtils = __dependency4__["default"]; + var ArrayProxy = __dependency5__["default"]; + var SortableMixin = __dependency6__["default"]; + var ControllerMixin = __dependency7__.ControllerMixin; + var computed = __dependency8__.computed; + var EmberError = __dependency9__["default"]; + + var forEach = EnumerableUtils.forEach, + replace = EnumerableUtils.replace; + + /** + `Ember.ArrayController` provides a way for you to publish a collection of + objects so that you can easily bind to the collection from a Handlebars + `#each` helper, an `Ember.CollectionView`, or other controllers. + + The advantage of using an `ArrayController` is that you only have to set up + your view bindings once; to change what's displayed, simply swap out the + `content` property on the controller. + + For example, imagine you wanted to display a list of items fetched via an XHR + request. Create an `Ember.ArrayController` and set its `content` property: + + ```javascript + MyApp.listController = Ember.ArrayController.create(); + + $.get('people.json', function(data) { + MyApp.listController.set('content', data); + }); + ``` + + Then, create a view that binds to your new controller: + + ```handlebars + {{#each MyApp.listController}} + {{firstName}} {{lastName}} + {{/each}} + ``` + + Although you are binding to the controller, the behavior of this controller + is to pass through any methods or properties to the underlying array. This + capability comes from `Ember.ArrayProxy`, which this class inherits from. + + Sometimes you want to display computed properties within the body of an + `#each` helper that depend on the underlying items in `content`, but are not + present on those items. To do this, set `itemController` to the name of a + controller (probably an `ObjectController`) that will wrap each individual item. + + For example: + + ```handlebars + {{#each post in controller}} +
  • {{post.title}} ({{post.titleLength}} characters)
  • + {{/each}} + ``` + + ```javascript + App.PostsController = Ember.ArrayController.extend({ + itemController: 'post' + }); + + App.PostController = Ember.ObjectController.extend({ + // the `title` property will be proxied to the underlying post. + + titleLength: function() { + return this.get('title').length; + }.property('title') + }); + ``` + + In some cases it is helpful to return a different `itemController` depending + on the particular item. Subclasses can do this by overriding + `lookupItemController`. + + For example: + + ```javascript + App.MyArrayController = Ember.ArrayController.extend({ + lookupItemController: function( object ) { + if (object.get('isSpecial')) { + return "special"; // use App.SpecialController + } else { + return "regular"; // use App.RegularController + } + } + }); + ``` + + The itemController instances will have a `parentController` property set to + the `ArrayController` instance. + + @class ArrayController + @namespace Ember + @extends Ember.ArrayProxy + @uses Ember.SortableMixin + @uses Ember.ControllerMixin + */ + + var ArrayController = ArrayProxy.extend(ControllerMixin, SortableMixin, { + + /** + The controller used to wrap items, if any. + + @property itemController + @type String + @default null + */ + itemController: null, + + /** + Return the name of the controller to wrap items, or `null` if items should + be returned directly. The default implementation simply returns the + `itemController` property, but subclasses can override this method to return + different controllers for different objects. + + For example: + + ```javascript + App.MyArrayController = Ember.ArrayController.extend({ + lookupItemController: function( object ) { + if (object.get('isSpecial')) { + return "special"; // use App.SpecialController + } else { + return "regular"; // use App.RegularController + } + } + }); + ``` + + @method lookupItemController + @param {Object} object + @return {String} + */ + lookupItemController: function(object) { + return get(this, 'itemController'); + }, + + objectAtContent: function(idx) { + var length = get(this, 'length'), + arrangedContent = get(this,'arrangedContent'), + object = arrangedContent && arrangedContent.objectAt(idx); + + if (idx >= 0 && idx < length) { + var controllerClass = this.lookupItemController(object); + if (controllerClass) { + return this.controllerAt(idx, object, controllerClass); + } + } + + // When `controllerClass` is falsy, we have not opted in to using item + // controllers, so return the object directly. + + // When the index is out of range, we want to return the "out of range" + // value, whatever that might be. Rather than make assumptions + // (e.g. guessing `null` or `undefined`) we defer this to `arrangedContent`. + return object; + }, + + arrangedContentDidChange: function() { + this._super(); + this._resetSubControllers(); + }, + + arrayContentDidChange: function(idx, removedCnt, addedCnt) { + var subControllers = get(this, '_subControllers'), + subControllersToRemove = subControllers.slice(idx, idx+removedCnt); + + forEach(subControllersToRemove, function(subController) { + if (subController) { subController.destroy(); } + }); + + replace(subControllers, idx, removedCnt, new Array(addedCnt)); + + // The shadow array of subcontrollers must be updated before we trigger + // observers, otherwise observers will get the wrong subcontainer when + // calling `objectAt` + this._super(idx, removedCnt, addedCnt); + }, + + init: function() { + this._super(); + + this.set('_subControllers', [ ]); + }, + + content: computed(function () { + return Ember.A(); + }), + + /** + * Flag to mark as being "virtual". Used to keep this instance + * from participating in the parentController hierarchy. + * + * @private + * @property _isVirtual + * @type Boolean + */ + _isVirtual: false, + + controllerAt: function(idx, object, controllerClass) { + var container = get(this, 'container'), + subControllers = get(this, '_subControllers'), + subController = subControllers[idx], + fullName; + + if (subController) { return subController; } + + fullName = "controller:" + controllerClass; + + if (!container.has(fullName)) { + throw new EmberError('Could not resolve itemController: "' + controllerClass + '"'); + } + var parentController; + if (this._isVirtual) { + parentController = get(this, 'parentController'); + } + parentController = parentController || this; + subController = container.lookupFactory(fullName).create({ + target: this, + parentController: parentController, + content: object + }); + + subControllers[idx] = subController; + + return subController; + }, + + _subControllers: null, + + _resetSubControllers: function() { + var subControllers = get(this, '_subControllers'); + var controller; + + if (subControllers.length) { + for (var i = 0, length = subControllers.length; length > i; i++) { + controller = subControllers[i]; + if (controller) { + controller.destroy(); + } + } + + subControllers.length = 0; + } + }, + + willDestroy: function() { + this._resetSubControllers(); + this._super(); + } + }); + + __exports__["default"] = ArrayController; + }); +define("ember-runtime/controllers/controller", + ["ember-metal/core","ember-metal/property_get","ember-runtime/system/object","ember-metal/mixin","ember-metal/computed","ember-runtime/mixins/action_handler","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __exports__) { + "use strict"; + var Ember = __dependency1__["default"]; + // Ember.assert, Ember.deprecate + var get = __dependency2__.get; + var EmberObject = __dependency3__["default"]; + var Mixin = __dependency4__.Mixin; + var computed = __dependency5__.computed; + var ActionHandler = __dependency6__["default"]; + + /** + @module ember + @submodule ember-runtime + */ + + /** + `Ember.ControllerMixin` provides a standard interface for all classes that + compose Ember's controller layer: `Ember.Controller`, + `Ember.ArrayController`, and `Ember.ObjectController`. + + @class ControllerMixin + @namespace Ember + @uses Ember.ActionHandler + */ + var ControllerMixin = Mixin.create(ActionHandler, { + /* ducktype as a controller */ + isController: true, + + /** + The object to which actions from the view should be sent. + + For example, when a Handlebars template uses the `{{action}}` helper, + it will attempt to send the action to the view's controller's `target`. + + By default, the value of the target property is set to the router, and + is injected when a controller is instantiated. This injection is defined + in Ember.Application#buildContainer, and is applied as part of the + applications initialization process. It can also be set after a controller + has been instantiated, for instance when using the render helper in a + template, or when a controller is used as an `itemController`. In most + cases the `target` property will automatically be set to the logical + consumer of actions for the controller. + + @property target + @default null + */ + target: null, + + container: null, + + parentController: null, + + store: null, + + model: computed.alias('content'), + + deprecatedSendHandles: function(actionName) { + return !!this[actionName]; + }, + + deprecatedSend: function(actionName) { + var args = [].slice.call(arguments, 1); + Ember.assert('' + this + " has the action " + actionName + " but it is not a function", typeof this[actionName] === 'function'); + Ember.deprecate('Action handlers implemented directly on controllers are deprecated in favor of action handlers on an `actions` object ( action: `' + actionName + '` on ' + this + ')', false); + this[actionName].apply(this, args); + return; + } + }); + + /** + @class Controller + @namespace Ember + @extends Ember.Object + @uses Ember.ControllerMixin + */ + var Controller = EmberObject.extend(ControllerMixin); + + __exports__.Controller = Controller; + __exports__.ControllerMixin = ControllerMixin; + }); +define("ember-runtime/controllers/object_controller", + ["ember-runtime/controllers/controller","ember-runtime/system/object_proxy","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + var ControllerMixin = __dependency1__.ControllerMixin; + var ObjectProxy = __dependency2__["default"]; + + /** + @module ember + @submodule ember-runtime + */ + + /** + `Ember.ObjectController` is part of Ember's Controller layer. It is intended + to wrap a single object, proxying unhandled attempts to `get` and `set` to the underlying + content object, and to forward unhandled action attempts to its `target`. + + `Ember.ObjectController` derives this functionality from its superclass + `Ember.ObjectProxy` and the `Ember.ControllerMixin` mixin. + + @class ObjectController + @namespace Ember + @extends Ember.ObjectProxy + @uses Ember.ControllerMixin + **/ + var ObjectController = ObjectProxy.extend(ControllerMixin); + __exports__["default"] = ObjectController; + }); +define("ember-runtime/copy", + ["ember-metal/enumerable_utils","ember-metal/utils","ember-runtime/system/object","ember-runtime/mixins/copyable","ember-metal/platform","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __exports__) { + "use strict"; + var EnumerableUtils = __dependency1__["default"]; + var typeOf = __dependency2__.typeOf; + var EmberObject = __dependency3__["default"]; + var Copyable = __dependency4__["default"]; + var create = __dependency5__.create; + + var indexOf = EnumerableUtils.indexOf; + + function _copy(obj, deep, seen, copies) { + var ret, loc, key; + + // primitive data types are immutable, just return them. + if ('object' !== typeof obj || obj===null) return obj; + + // avoid cyclical loops + if (deep && (loc=indexOf(seen, obj))>=0) return copies[loc]; + + Ember.assert('Cannot clone an Ember.Object that does not implement Ember.Copyable', !(obj instanceof EmberObject) || (Copyable && Copyable.detect(obj))); + + // IMPORTANT: this specific test will detect a native array only. Any other + // object will need to implement Copyable. + if (typeOf(obj) === 'array') { + ret = obj.slice(); + if (deep) { + loc = ret.length; + while(--loc>=0) ret[loc] = _copy(ret[loc], deep, seen, copies); + } + } else if (Copyable && Copyable.detect(obj)) { + ret = obj.copy(deep, seen, copies); + } else if (obj instanceof Date) { + ret = new Date(obj.getTime()); + } else { + ret = {}; + for(key in obj) { + if (!obj.hasOwnProperty(key)) continue; + + // Prevents browsers that don't respect non-enumerability from + // copying internal Ember properties + if (key.substring(0,2) === '__') continue; + + ret[key] = deep ? _copy(obj[key], deep, seen, copies) : obj[key]; + } + } + + if (deep) { + seen.push(obj); + copies.push(ret); + } + + return ret; + } + + /** + Creates a clone of the passed object. This function can take just about + any type of object and create a clone of it, including primitive values + (which are not actually cloned because they are immutable). + + If the passed object implements the `clone()` method, then this function + will simply call that method and return the result. + + @method copy + @for Ember + @param {Object} obj The object to clone + @param {Boolean} deep If true, a deep copy of the object is made + @return {Object} The cloned object + */ + function copy(obj, deep) { + // fast paths + if ('object' !== typeof obj || obj===null) return obj; // can't copy primitives + if (Copyable && Copyable.detect(obj)) return obj.copy(deep); + return _copy(obj, deep, deep ? [] : null, deep ? [] : null); + }; + + __exports__["default"] = copy; + }); +define("ember-runtime/core", + ["exports"], + function(__exports__) { + "use strict"; + /** + @module ember + @submodule ember-runtime + */ + + /** + Compares two objects, returning true if they are logically equal. This is + a deeper comparison than a simple triple equal. For sets it will compare the + internal objects. For any other object that implements `isEqual()` it will + respect that method. + + ```javascript + Ember.isEqual('hello', 'hello'); // true + Ember.isEqual(1, 2); // false + Ember.isEqual([4, 2], [4, 2]); // false + ``` + + @method isEqual + @for Ember + @param {Object} a first object to compare + @param {Object} b second object to compare + @return {Boolean} + */ + function isEqual(a, b) { + if (a && 'function'===typeof a.isEqual) return a.isEqual(b); + if (a instanceof Date && b instanceof Date) { + return a.getTime() === b.getTime(); + } + return a === b; + }; + + __exports__.isEqual = isEqual; + }); +define("ember-runtime/ext/function", + ["ember-metal/core","ember-metal/expand_properties","ember-metal/computed"], + function(__dependency1__, __dependency2__, __dependency3__) { + "use strict"; + /** + @module ember + @submodule ember-runtime + */ + + var Ember = __dependency1__["default"]; + // Ember.EXTEND_PROTOTYPES, Ember.assert + var expandProperties = __dependency2__["default"]; + var computed = __dependency3__.computed; + + var a_slice = Array.prototype.slice; + var FunctionPrototype = Function.prototype; + + if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Function) { + + /** + The `property` extension of Javascript's Function prototype is available + when `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Function` is + `true`, which is the default. + + Computed properties allow you to treat a function like a property: + + ```javascript + MyApp.President = Ember.Object.extend({ + firstName: '', + lastName: '', + + fullName: function() { + return this.get('firstName') + ' ' + this.get('lastName'); + + // Call this flag to mark the function as a property + }.property() + }); + + var president = MyApp.President.create({ + firstName: "Barack", + lastName: "Obama" + }); + + president.get('fullName'); // "Barack Obama" + ``` + + Treating a function like a property is useful because they can work with + bindings, just like any other property. + + Many computed properties have dependencies on other properties. For + example, in the above example, the `fullName` property depends on + `firstName` and `lastName` to determine its value. You can tell Ember + about these dependencies like this: + + ```javascript + MyApp.President = Ember.Object.extend({ + firstName: '', + lastName: '', + + fullName: function() { + return this.get('firstName') + ' ' + this.get('lastName'); + + // Tell Ember.js that this computed property depends on firstName + // and lastName + }.property('firstName', 'lastName') + }); + ``` + + Make sure you list these dependencies so Ember knows when to update + bindings that connect to a computed property. Changing a dependency + will not immediately trigger an update of the computed property, but + will instead clear the cache so that it is updated when the next `get` + is called on the property. + + See [Ember.ComputedProperty](/api/classes/Ember.ComputedProperty.html), [Ember.computed](/api/#method_computed). + + @method property + @for Function + */ + FunctionPrototype.property = function() { + var ret = computed(this); + // ComputedProperty.prototype.property expands properties; no need for us to + // do so here. + return ret.property.apply(ret, arguments); + }; + + /** + The `observes` extension of Javascript's Function prototype is available + when `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Function` is + true, which is the default. + + You can observe property changes simply by adding the `observes` + call to the end of your method declarations in classes that you write. + For example: + + ```javascript + Ember.Object.extend({ + valueObserver: function() { + // Executes whenever the "value" property changes + }.observes('value') + }); + ``` + + In the future this method may become asynchronous. If you want to ensure + synchronous behavior, use `observesImmediately`. + + See `Ember.observer`. + + @method observes + @for Function + */ + FunctionPrototype.observes = function() { + var addWatchedProperty = function (obs) { watched.push(obs); }; + var watched = []; + + for (var i=0; i= 0) return; + if (typeof obj.hasOwnProperty === 'function' && !obj.hasOwnProperty(key)) return; + + array.push(key); + }; + + keys = function keys(obj) { + var ret = [], key; + for (key in obj) { + pushPropertyName(obj, ret, key); + } + + // IE8 doesn't enumerate property that named the same as prototype properties. + for (var i = 0, l = prototypeProperties.length; i < l; i++) { + key = prototypeProperties[i]; + + pushPropertyName(obj, ret, key); + } + + return ret; + }; + } + + __exports__["default"] = keys; + }); +define("ember-runtime", + ["ember-metal","ember-runtime/core","ember-runtime/keys","ember-runtime/compare","ember-runtime/copy","ember-runtime/system/namespace","ember-runtime/system/object","ember-runtime/system/tracked_array","ember-runtime/system/subarray","ember-runtime/system/container","ember-runtime/system/application","ember-runtime/system/array_proxy","ember-runtime/system/object_proxy","ember-runtime/system/core_object","ember-runtime/system/each_proxy","ember-runtime/system/native_array","ember-runtime/system/set","ember-runtime/system/string","ember-runtime/system/deferred","ember-runtime/system/lazy_load","ember-runtime/mixins/array","ember-runtime/mixins/comparable","ember-runtime/mixins/copyable","ember-runtime/mixins/enumerable","ember-runtime/mixins/freezable","ember-runtime/mixins/observable","ember-runtime/mixins/action_handler","ember-runtime/mixins/deferred","ember-runtime/mixins/mutable_enumerable","ember-runtime/mixins/mutable_array","ember-runtime/mixins/target_action_support","ember-runtime/mixins/evented","ember-runtime/mixins/promise_proxy","ember-runtime/mixins/sortable","ember-runtime/computed/array_computed","ember-runtime/computed/reduce_computed","ember-runtime/computed/reduce_computed_macros","ember-runtime/controllers/array_controller","ember-runtime/controllers/object_controller","ember-runtime/controllers/controller","ember-runtime/ext/rsvp","ember-runtime/ext/string","ember-runtime/ext/function","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __dependency11__, __dependency12__, __dependency13__, __dependency14__, __dependency15__, __dependency16__, __dependency17__, __dependency18__, __dependency19__, __dependency20__, __dependency21__, __dependency22__, __dependency23__, __dependency24__, __dependency25__, __dependency26__, __dependency27__, __dependency28__, __dependency29__, __dependency30__, __dependency31__, __dependency32__, __dependency33__, __dependency34__, __dependency35__, __dependency36__, __dependency37__, __dependency38__, __dependency39__, __dependency40__, __dependency41__, __dependency42__, __dependency43__, __exports__) { + "use strict"; + /** + Ember Runtime + + @module ember + @submodule ember-runtime + @requires ember-metal + */ + + + // BEGIN EXPORTS + Ember.compare = __dependency4__["default"]; + Ember.copy = __dependency5__["default"]; + Ember.isEqual = __dependency2__.isEqual; + Ember.keys = __dependency3__["default"]; + + Ember.Array = __dependency21__["default"]; + + Ember.Comparable = __dependency22__["default"]; + Ember.Copyable = __dependency23__["default"]; + + Ember.SortableMixin = __dependency34__["default"]; + + Ember.Freezable = __dependency25__.Freezable; + Ember.FROZEN_ERROR = __dependency25__.FROZEN_ERROR; + + Ember.DeferredMixin = __dependency28__["default"]; + + Ember.MutableEnumerable = __dependency29__["default"]; + Ember.MutableArray = __dependency30__["default"]; + + Ember.TargetActionSupport = __dependency31__["default"]; + Ember.Evented = __dependency32__["default"]; + + Ember.PromiseProxyMixin = __dependency33__["default"]; + + Ember.Observable = __dependency26__["default"]; + + Ember.arrayComputed = __dependency35__.arrayComputed; + Ember.ArrayComputedProperty = __dependency35__.ArrayComputedProperty; + Ember.reduceComputed = __dependency36__.reduceComputed; + Ember.ReduceComputedProperty = __dependency36__.ReduceComputedProperty; + + // ES6TODO: this seems a less than ideal way/place to add properties to Ember.computed + var EmComputed = Ember.computed; + + EmComputed.sum = __dependency37__.sum; + EmComputed.min = __dependency37__.min; + EmComputed.max = __dependency37__.max; + EmComputed.map = __dependency37__.map; + EmComputed.sort = __dependency37__.sort; + EmComputed.setDiff = __dependency37__.setDiff; + EmComputed.mapBy = __dependency37__.mapBy; + EmComputed.mapProperty = __dependency37__.mapProperty; + EmComputed.filter = __dependency37__.filter; + EmComputed.filterBy = __dependency37__.filterBy; + EmComputed.filterProperty = __dependency37__.filterProperty; + EmComputed.uniq = __dependency37__.uniq; + EmComputed.union = __dependency37__.union; + EmComputed.intersect = __dependency37__.intersect; + + Ember.String = __dependency18__["default"]; + Ember.Object = __dependency7__["default"]; + Ember.TrackedArray = __dependency8__["default"]; + Ember.SubArray = __dependency9__["default"]; + Ember.Container = __dependency10__["default"]; + Ember.Namespace = __dependency6__["default"]; + Ember.Application = __dependency11__["default"]; + Ember.Enumerable = __dependency24__["default"]; + Ember.ArrayProxy = __dependency12__["default"]; + Ember.ObjectProxy = __dependency13__["default"]; + Ember.ActionHandler = __dependency27__["default"]; + Ember.CoreObject = __dependency14__["default"]; + Ember.EachArray = __dependency15__.EachArray; + Ember.EachProxy = __dependency15__.EachProxy; + Ember.NativeArray = __dependency16__["default"]; + // ES6TODO: Currently we must rely on the global from ember-metal/core to avoid circular deps + // Ember.A = A; + Ember.Set = __dependency17__["default"]; + Ember.Deferred = __dependency19__["default"]; + Ember.onLoad = __dependency20__.onLoad; + Ember.runLoadHooks = __dependency20__.runLoadHooks; + + Ember.ArrayController = __dependency38__["default"]; + Ember.ObjectController = __dependency39__["default"]; + Ember.Controller = __dependency40__.Controller; + Ember.ControllerMixin = __dependency40__.ControllerMixin; + + Ember.RSVP = __dependency41__["default"]; + // END EXPORTS + + __exports__["default"] = Ember; + }); +define("ember-runtime/mixins/action_handler", + ["ember-metal/merge","ember-metal/mixin","ember-metal/property_get","ember-metal/utils","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __exports__) { + "use strict"; + /** + @module ember + @submodule ember-runtime + */ + var merge = __dependency1__["default"]; + var Mixin = __dependency2__.Mixin; + var get = __dependency3__.get; + var typeOf = __dependency4__.typeOf; + + /** + The `Ember.ActionHandler` mixin implements support for moving an `actions` + property to an `_actions` property at extend time, and adding `_actions` + to the object's mergedProperties list. + + `Ember.ActionHandler` is available on some familiar classes including + `Ember.Route`, `Ember.View`, `Ember.Component`, and controllers such as + `Ember.Controller` and `Ember.ObjectController`. + (Internally the mixin is used by `Ember.CoreView`, `Ember.ControllerMixin`, + and `Ember.Route` and available to the above classes through + inheritance.) + + @class ActionHandler + @namespace Ember + */ + var ActionHandler = Mixin.create({ + mergedProperties: ['_actions'], + + /** + The collection of functions, keyed by name, available on this + `ActionHandler` as action targets. + + These functions will be invoked when a matching `{{action}}` is triggered + from within a template and the application's current route is this route. + + Actions can also be invoked from other parts of your application + via `ActionHandler#send`. + + The `actions` hash will inherit action handlers from + the `actions` hash defined on extended parent classes + or mixins rather than just replace the entire hash, e.g.: + + ```js + App.CanDisplayBanner = Ember.Mixin.create({ + actions: { + displayBanner: function(msg) { + // ... + } + } + }); + + App.WelcomeRoute = Ember.Route.extend(App.CanDisplayBanner, { + actions: { + playMusic: function() { + // ... + } + } + }); + + // `WelcomeRoute`, when active, will be able to respond + // to both actions, since the actions hash is merged rather + // then replaced when extending mixins / parent classes. + this.send('displayBanner'); + this.send('playMusic'); + ``` + + Within a Controller, Route, View or Component's action handler, + the value of the `this` context is the Controller, Route, View or + Component object: + + ```js + App.SongRoute = Ember.Route.extend({ + actions: { + myAction: function() { + this.controllerFor("song"); + this.transitionTo("other.route"); + ... + } + } + }); + ``` + + It is also possible to call `this._super()` from within an + action handler if it overrides a handler defined on a parent + class or mixin: + + Take for example the following routes: + + ```js + App.DebugRoute = Ember.Mixin.create({ + actions: { + debugRouteInformation: function() { + console.debug("trololo"); + } + } + }); + + App.AnnoyingDebugRoute = Ember.Route.extend(App.DebugRoute, { + actions: { + debugRouteInformation: function() { + // also call the debugRouteInformation of mixed in App.DebugRoute + this._super(); + + // show additional annoyance + window.alert(...); + } + } + }); + ``` + + ## Bubbling + + By default, an action will stop bubbling once a handler defined + on the `actions` hash handles it. To continue bubbling the action, + you must return `true` from the handler: + + ```js + App.Router.map(function() { + this.resource("album", function() { + this.route("song"); + }); + }); + + App.AlbumRoute = Ember.Route.extend({ + actions: { + startPlaying: function() { + } + } + }); + + App.AlbumSongRoute = Ember.Route.extend({ + actions: { + startPlaying: function() { + // ... + + if (actionShouldAlsoBeTriggeredOnParentRoute) { + return true; + } + } + } + }); + ``` + + @property actions + @type Hash + @default null + */ + + /** + Moves `actions` to `_actions` at extend time. Note that this currently + modifies the mixin themselves, which is technically dubious but + is practically of little consequence. This may change in the future. + + @private + @method willMergeMixin + */ + willMergeMixin: function(props) { + var hashName; + + if (!props._actions) { + Ember.assert("'actions' should not be a function", typeof(props.actions) !== 'function'); + + if (typeOf(props.actions) === 'object') { + hashName = 'actions'; + } else if (typeOf(props.events) === 'object') { + Ember.deprecate('Action handlers contained in an `events` object are deprecated in favor of putting them in an `actions` object', false); + hashName = 'events'; + } + + if (hashName) { + props._actions = merge(props._actions || {}, props[hashName]); + } + + delete props[hashName]; + } + }, + + /** + Triggers a named action on the `ActionHandler`. Any parameters + supplied after the `actionName` string will be passed as arguments + to the action target function. + + If the `ActionHandler` has its `target` property set, actions may + bubble to the `target`. Bubbling happens when an `actionName` can + not be found in the `ActionHandler`'s `actions` hash or if the + action target function returns `true`. + + Example + + ```js + App.WelcomeRoute = Ember.Route.extend({ + actions: { + playTheme: function() { + this.send('playMusic', 'theme.mp3'); + }, + playMusic: function(track) { + // ... + } + } + }); + ``` + + @method send + @param {String} actionName The action to trigger + @param {*} context a context to send with the action + */ + send: function(actionName) { + var args = [].slice.call(arguments, 1), target; + + if (this._actions && this._actions[actionName]) { + if (this._actions[actionName].apply(this, args) === true) { + // handler returned true, so this action will bubble + } else { + return; + } + } else if (!Ember.FEATURES.isEnabled('ember-routing-drop-deprecated-action-style') && this.deprecatedSend && this.deprecatedSendHandles && this.deprecatedSendHandles(actionName)) { + Ember.warn("The current default is deprecated but will prefer to handle actions directly on the controller instead of a similarly named action in the actions hash. To turn off this deprecated feature set: Ember.FEATURES['ember-routing-drop-deprecated-action-style'] = true"); + if (this.deprecatedSend.apply(this, [].slice.call(arguments)) === true) { + // handler return true, so this action will bubble + } else { + return; + } + } + + if (target = get(this, 'target')) { + Ember.assert("The `target` for " + this + " (" + target + ") does not have a `send` method", typeof target.send === 'function'); + target.send.apply(target, arguments); + } + } + }); + + __exports__["default"] = ActionHandler; + }); +define("ember-runtime/mixins/array", + ["ember-metal/core","ember-metal/property_get","ember-metal/property_set","ember-metal/computed","ember-metal/is_none","ember-runtime/mixins/enumerable","ember-metal/enumerable_utils","ember-metal/mixin","ember-metal/property_events","ember-metal/events","ember-metal/watching","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __dependency11__, __exports__) { + "use strict"; + /** + @module ember + @submodule ember-runtime + */ + + // .......................................................... + // HELPERS + // + var Ember = __dependency1__["default"]; + // ES6TODO: Ember.A + + var get = __dependency2__.get; + var set = __dependency3__.set; + var computed = __dependency4__.computed; + var cacheFor = __dependency4__.cacheFor; + var isNone = __dependency5__.isNone; + var none = __dependency5__.none; + var Enumerable = __dependency6__["default"]; + var EnumerableUtils = __dependency7__["default"]; + var Mixin = __dependency8__.Mixin; + var required = __dependency8__.required; + var propertyWillChange = __dependency9__.propertyWillChange; + var propertyDidChange = __dependency9__.propertyDidChange; + var addListener = __dependency10__.addListener; + var removeListener = __dependency10__.removeListener; + var sendEvent = __dependency10__.sendEvent; + var hasListeners = __dependency10__.hasListeners; + var isWatching = __dependency11__.isWatching; + + var map = EnumerableUtils.map; + + // .......................................................... + // ARRAY + // + /** + This mixin implements Observer-friendly Array-like behavior. It is not a + concrete implementation, but it can be used up by other classes that want + to appear like arrays. + + For example, ArrayProxy and ArrayController are both concrete classes that can + be instantiated to implement array-like behavior. Both of these classes use + the Array Mixin by way of the MutableArray mixin, which allows observable + changes to be made to the underlying array. + + Unlike `Ember.Enumerable,` this mixin defines methods specifically for + collections that provide index-ordered access to their contents. When you + are designing code that needs to accept any kind of Array-like object, you + should use these methods instead of Array primitives because these will + properly notify observers of changes to the array. + + Although these methods are efficient, they do add a layer of indirection to + your application so it is a good idea to use them only when you need the + flexibility of using both true JavaScript arrays and "virtual" arrays such + as controllers and collections. + + You can use the methods defined in this module to access and modify array + contents in a KVO-friendly way. You can also be notified whenever the + membership of an array changes by using `.observes('myArray.[]')`. + + To support `Ember.Array` in your own class, you must override two + primitives to use it: `replace()` and `objectAt()`. + + Note that the Ember.Array mixin also incorporates the `Ember.Enumerable` + mixin. All `Ember.Array`-like objects are also enumerable. + + @class Array + @namespace Ember + @uses Ember.Enumerable + @since Ember 0.9.0 + */ + var EmberArray = Mixin.create(Enumerable, { + + /** + Your array must support the `length` property. Your replace methods should + set this property whenever it changes. + + @property {Number} length + */ + length: required(), + + /** + Returns the object at the given `index`. If the given `index` is negative + or is greater or equal than the array length, returns `undefined`. + + This is one of the primitives you must implement to support `Ember.Array`. + If your object supports retrieving the value of an array item using `get()` + (i.e. `myArray.get(0)`), then you do not need to implement this method + yourself. + + ```javascript + var arr = ['a', 'b', 'c', 'd']; + arr.objectAt(0); // "a" + arr.objectAt(3); // "d" + arr.objectAt(-1); // undefined + arr.objectAt(4); // undefined + arr.objectAt(5); // undefined + ``` + + @method objectAt + @param {Number} idx The index of the item to return. + @return {*} item at index or undefined + */ + objectAt: function(idx) { + if ((idx < 0) || (idx >= get(this, 'length'))) return undefined; + return get(this, idx); + }, + + /** + This returns the objects at the specified indexes, using `objectAt`. + + ```javascript + var arr = ['a', 'b', 'c', 'd']; + arr.objectsAt([0, 1, 2]); // ["a", "b", "c"] + arr.objectsAt([2, 3, 4]); // ["c", "d", undefined] + ``` + + @method objectsAt + @param {Array} indexes An array of indexes of items to return. + @return {Array} + */ + objectsAt: function(indexes) { + var self = this; + return map(indexes, function(idx) { return self.objectAt(idx); }); + }, + + // overrides Ember.Enumerable version + nextObject: function(idx) { + return this.objectAt(idx); + }, + + /** + This is the handler for the special array content property. If you get + this property, it will return this. If you set this property it a new + array, it will replace the current content. + + This property overrides the default property defined in `Ember.Enumerable`. + + @property [] + @return this + */ + '[]': computed(function(key, value) { + if (value !== undefined) this.replace(0, get(this, 'length'), value) ; + return this ; + }), + + firstObject: computed(function() { + return this.objectAt(0); + }), + + lastObject: computed(function() { + return this.objectAt(get(this, 'length')-1); + }), + + // optimized version from Enumerable + contains: function(obj) { + return this.indexOf(obj) >= 0; + }, + + // Add any extra methods to Ember.Array that are native to the built-in Array. + /** + Returns a new array that is a slice of the receiver. This implementation + uses the observable array methods to retrieve the objects for the new + slice. + + ```javascript + var arr = ['red', 'green', 'blue']; + arr.slice(0); // ['red', 'green', 'blue'] + arr.slice(0, 2); // ['red', 'green'] + arr.slice(1, 100); // ['green', 'blue'] + ``` + + @method slice + @param {Integer} beginIndex (Optional) index to begin slicing from. + @param {Integer} endIndex (Optional) index to end the slice at (but not included). + @return {Array} New array with specified slice + */ + slice: function(beginIndex, endIndex) { + var ret = Ember.A(); + var length = get(this, 'length') ; + if (isNone(beginIndex)) beginIndex = 0 ; + if (isNone(endIndex) || (endIndex > length)) endIndex = length ; + + if (beginIndex < 0) beginIndex = length + beginIndex; + if (endIndex < 0) endIndex = length + endIndex; + + while(beginIndex < endIndex) { + ret[ret.length] = this.objectAt(beginIndex++) ; + } + return ret ; + }, + + /** + Returns the index of the given object's first occurrence. + If no `startAt` argument is given, the starting location to + search is 0. If it's negative, will count backward from + the end of the array. Returns -1 if no match is found. + + ```javascript + var arr = ["a", "b", "c", "d", "a"]; + arr.indexOf("a"); // 0 + arr.indexOf("z"); // -1 + arr.indexOf("a", 2); // 4 + arr.indexOf("a", -1); // 4 + arr.indexOf("b", 3); // -1 + arr.indexOf("a", 100); // -1 + ``` + + @method indexOf + @param {Object} object the item to search for + @param {Number} startAt optional starting location to search, default 0 + @return {Number} index or -1 if not found + */ + indexOf: function(object, startAt) { + var idx, len = get(this, 'length'); + + if (startAt === undefined) startAt = 0; + if (startAt < 0) startAt += len; + + for(idx = startAt; idx < len; idx++) { + if (this.objectAt(idx) === object) return idx; + } + return -1; + }, + + /** + Returns the index of the given object's last occurrence. + If no `startAt` argument is given, the search starts from + the last position. If it's negative, will count backward + from the end of the array. Returns -1 if no match is found. + + ```javascript + var arr = ["a", "b", "c", "d", "a"]; + arr.lastIndexOf("a"); // 4 + arr.lastIndexOf("z"); // -1 + arr.lastIndexOf("a", 2); // 0 + arr.lastIndexOf("a", -1); // 4 + arr.lastIndexOf("b", 3); // 1 + arr.lastIndexOf("a", 100); // 4 + ``` + + @method lastIndexOf + @param {Object} object the item to search for + @param {Number} startAt optional starting location to search, default 0 + @return {Number} index or -1 if not found + */ + lastIndexOf: function(object, startAt) { + var idx, len = get(this, 'length'); + + if (startAt === undefined || startAt >= len) startAt = len-1; + if (startAt < 0) startAt += len; + + for(idx = startAt; idx >= 0; idx--) { + if (this.objectAt(idx) === object) return idx; + } + return -1; + }, + + // .......................................................... + // ARRAY OBSERVERS + // + + /** + Adds an array observer to the receiving array. The array observer object + normally must implement two methods: + + * `arrayWillChange(observedObj, start, removeCount, addCount)` - This method will be + called just before the array is modified. + * `arrayDidChange(observedObj, start, removeCount, addCount)` - This method will be + called just after the array is modified. + + Both callbacks will be passed the observed object, starting index of the + change as well a a count of the items to be removed and added. You can use + these callbacks to optionally inspect the array during the change, clear + caches, or do any other bookkeeping necessary. + + In addition to passing a target, you can also include an options hash + which you can use to override the method names that will be invoked on the + target. + + @method addArrayObserver + @param {Object} target The observer object. + @param {Hash} opts Optional hash of configuration options including + `willChange` and `didChange` option. + @return {Ember.Array} receiver + */ + addArrayObserver: function(target, opts) { + var willChange = (opts && opts.willChange) || 'arrayWillChange', + didChange = (opts && opts.didChange) || 'arrayDidChange'; + + var hasObservers = get(this, 'hasArrayObservers'); + if (!hasObservers) propertyWillChange(this, 'hasArrayObservers'); + addListener(this, '@array:before', target, willChange); + addListener(this, '@array:change', target, didChange); + if (!hasObservers) propertyDidChange(this, 'hasArrayObservers'); + return this; + }, + + /** + Removes an array observer from the object if the observer is current + registered. Calling this method multiple times with the same object will + have no effect. + + @method removeArrayObserver + @param {Object} target The object observing the array. + @param {Hash} opts Optional hash of configuration options including + `willChange` and `didChange` option. + @return {Ember.Array} receiver + */ + removeArrayObserver: function(target, opts) { + var willChange = (opts && opts.willChange) || 'arrayWillChange', + didChange = (opts && opts.didChange) || 'arrayDidChange'; + + var hasObservers = get(this, 'hasArrayObservers'); + if (hasObservers) propertyWillChange(this, 'hasArrayObservers'); + removeListener(this, '@array:before', target, willChange); + removeListener(this, '@array:change', target, didChange); + if (hasObservers) propertyDidChange(this, 'hasArrayObservers'); + return this; + }, + + /** + Becomes true whenever the array currently has observers watching changes + on the array. + + @property {Boolean} hasArrayObservers + */ + hasArrayObservers: computed(function() { + return hasListeners(this, '@array:change') || hasListeners(this, '@array:before'); + }), + + /** + If you are implementing an object that supports `Ember.Array`, call this + method just before the array content changes to notify any observers and + invalidate any related properties. Pass the starting index of the change + as well as a delta of the amounts to change. + + @method arrayContentWillChange + @param {Number} startIdx The starting index in the array that will change. + @param {Number} removeAmt The number of items that will be removed. If you + pass `null` assumes 0 + @param {Number} addAmt The number of items that will be added. If you + pass `null` assumes 0. + @return {Ember.Array} receiver + */ + arrayContentWillChange: function(startIdx, removeAmt, addAmt) { + + // if no args are passed assume everything changes + if (startIdx===undefined) { + startIdx = 0; + removeAmt = addAmt = -1; + } else { + if (removeAmt === undefined) removeAmt=-1; + if (addAmt === undefined) addAmt=-1; + } + + // Make sure the @each proxy is set up if anyone is observing @each + if (isWatching(this, '@each')) { get(this, '@each'); } + + sendEvent(this, '@array:before', [this, startIdx, removeAmt, addAmt]); + + var removing, lim; + if (startIdx>=0 && removeAmt>=0 && get(this, 'hasEnumerableObservers')) { + removing = []; + lim = startIdx+removeAmt; + for(var idx=startIdx;idx=0 && addAmt>=0 && get(this, 'hasEnumerableObservers')) { + adding = []; + lim = startIdx+addAmt; + for(var idx=startIdx;idx b` + + Default implementation raises an exception. + + @method compare + @param a {Object} the first object to compare + @param b {Object} the second object to compare + @return {Integer} the result of the comparison + */ + compare: required(Function) + + }); + + __exports__["default"] = Comparable; + }); +define("ember-runtime/mixins/copyable", + ["ember-metal/property_get","ember-metal/property_set","ember-metal/mixin","ember-runtime/mixins/freezable","ember-runtime/system/string","ember-metal/error","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __exports__) { + "use strict"; + /** + @module ember + @submodule ember-runtime + */ + + + var get = __dependency1__.get; + var set = __dependency2__.set; + var required = __dependency3__.required; + var Freezable = __dependency4__.Freezable; + var Mixin = __dependency3__.Mixin; + var fmt = __dependency5__.fmt; + var EmberError = __dependency6__["default"]; + + + /** + Implements some standard methods for copying an object. Add this mixin to + any object you create that can create a copy of itself. This mixin is + added automatically to the built-in array. + + You should generally implement the `copy()` method to return a copy of the + receiver. + + Note that `frozenCopy()` will only work if you also implement + `Ember.Freezable`. + + @class Copyable + @namespace Ember + @since Ember 0.9 + */ + var Copyable = Mixin.create({ + + /** + Override to return a copy of the receiver. Default implementation raises + an exception. + + @method copy + @param {Boolean} deep if `true`, a deep copy of the object should be made + @return {Object} copy of receiver + */ + copy: required(Function), + + /** + If the object implements `Ember.Freezable`, then this will return a new + copy if the object is not frozen and the receiver if the object is frozen. + + Raises an exception if you try to call this method on a object that does + not support freezing. + + You should use this method whenever you want a copy of a freezable object + since a freezable object can simply return itself without actually + consuming more memory. + + @method frozenCopy + @return {Object} copy of receiver or receiver + */ + frozenCopy: function() { + if (Freezable && Freezable.detect(this)) { + return get(this, 'isFrozen') ? this : this.copy().freeze(); + } else { + throw new EmberError(fmt("%@ does not support freezing", [this])); + } + } + }); + + __exports__["default"] = Copyable; + }); +define("ember-runtime/mixins/deferred", + ["ember-metal/core","ember-metal/property_get","ember-metal/mixin","ember-metal/computed","ember-metal/run_loop","ember-runtime/ext/rsvp","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __exports__) { + "use strict"; + var Ember = __dependency1__["default"]; + // Ember.FEATURES, Ember.Test + var get = __dependency2__.get; + var Mixin = __dependency3__.Mixin; + var computed = __dependency4__.computed; + var run = __dependency5__["default"]; + var RSVP = __dependency6__["default"]; + + var asyncStart = function() { + if (Ember.Test && Ember.Test.adapter) { + Ember.Test.adapter.asyncStart(); + } + }; + + var asyncEnd = function() { + if (Ember.Test && Ember.Test.adapter) { + Ember.Test.adapter.asyncEnd(); + } + }; + + RSVP.configure('async', function(callback, promise) { + var async = !run.currentRunLoop; + + if (Ember.testing && async) { asyncStart(); } + + run.backburner.schedule('actions', function(){ + if (Ember.testing && async) { asyncEnd(); } + callback(promise); + }); + }); + + RSVP.Promise.prototype.fail = function(callback, label){ + Ember.deprecate('RSVP.Promise.fail has been renamed as RSVP.Promise.catch'); + return this['catch'](callback, label); + }; + + /** + @module ember + @submodule ember-runtime + */ + + + /** + @class Deferred + @namespace Ember + */ + var DeferredMixin = Mixin.create({ + /** + Add handlers to be called when the Deferred object is resolved or rejected. + + @method then + @param {Function} resolve a callback function to be called when done + @param {Function} reject a callback function to be called when failed + */ + then: function(resolve, reject, label) { + var deferred, promise, entity; + + entity = this; + deferred = get(this, '_deferred'); + promise = deferred.promise; + + function fulfillmentHandler(fulfillment) { + if (fulfillment === promise) { + return resolve(entity); + } else { + return resolve(fulfillment); + } + } + + return promise.then(resolve && fulfillmentHandler, reject, label); + }, + + /** + Resolve a Deferred object and call any `doneCallbacks` with the given args. + + @method resolve + */ + resolve: function(value) { + var deferred, promise; + + deferred = get(this, '_deferred'); + promise = deferred.promise; + + if (value === this) { + deferred.resolve(promise); + } else { + deferred.resolve(value); + } + }, + + /** + Reject a Deferred object and call any `failCallbacks` with the given args. + + @method reject + */ + reject: function(value) { + get(this, '_deferred').reject(value); + }, + + _deferred: computed(function() { + return RSVP.defer('Ember: DeferredMixin - ' + this); + }) + }); + + __exports__["default"] = DeferredMixin; + }); +define("ember-runtime/mixins/enumerable", + ["ember-metal/core","ember-metal/property_get","ember-metal/property_set","ember-metal/utils","ember-metal/mixin","ember-metal/enumerable_utils","ember-metal/computed","ember-metal/property_events","ember-metal/events","ember-runtime/compare","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __exports__) { + "use strict"; + /** + @module ember + @submodule ember-runtime + */ + + // .......................................................... + // HELPERS + // + + var Ember = __dependency1__["default"]; + var get = __dependency2__.get; + var set = __dependency3__.set; + var apply = __dependency4__.apply; + var Mixin = __dependency5__.Mixin; + var required = __dependency5__.required; + var aliasMethod = __dependency5__.aliasMethod; + var EnumerableUtils = __dependency6__["default"]; + var computed = __dependency7__.computed; + var propertyWillChange = __dependency8__.propertyWillChange; + var propertyDidChange = __dependency8__.propertyDidChange; + var addListener = __dependency9__.addListener; + var removeListener = __dependency9__.removeListener; + var sendEvent = __dependency9__.sendEvent; + var hasListeners = __dependency9__.hasListeners; + var compare = __dependency10__["default"]; + + var a_slice = Array.prototype.slice; + var a_indexOf = EnumerableUtils.indexOf; + + var contexts = []; + + function popCtx() { + return contexts.length===0 ? {} : contexts.pop(); + } + + function pushCtx(ctx) { + contexts.push(ctx); + return null; + } + + function iter(key, value) { + var valueProvided = arguments.length === 2; + + function i(item) { + var cur = get(item, key); + return valueProvided ? value===cur : !!cur; + } + return i ; + } + + /** + This mixin defines the common interface implemented by enumerable objects + in Ember. Most of these methods follow the standard Array iteration + API defined up to JavaScript 1.8 (excluding language-specific features that + cannot be emulated in older versions of JavaScript). + + This mixin is applied automatically to the Array class on page load, so you + can use any of these methods on simple arrays. If Array already implements + one of these methods, the mixin will not override them. + + ## Writing Your Own Enumerable + + To make your own custom class enumerable, you need two items: + + 1. You must have a length property. This property should change whenever + the number of items in your enumerable object changes. If you use this + with an `Ember.Object` subclass, you should be sure to change the length + property using `set().` + + 2. You must implement `nextObject().` See documentation. + + Once you have these two methods implemented, apply the `Ember.Enumerable` mixin + to your class and you will be able to enumerate the contents of your object + like any other collection. + + ## Using Ember Enumeration with Other Libraries + + Many other libraries provide some kind of iterator or enumeration like + facility. This is often where the most common API conflicts occur. + Ember's API is designed to be as friendly as possible with other + libraries by implementing only methods that mostly correspond to the + JavaScript 1.8 API. + + @class Enumerable + @namespace Ember + @since Ember 0.9 + */ + var Enumerable = Mixin.create({ + + /** + Implement this method to make your class enumerable. + + This method will be call repeatedly during enumeration. The index value + will always begin with 0 and increment monotonically. You don't have to + rely on the index value to determine what object to return, but you should + always check the value and start from the beginning when you see the + requested index is 0. + + The `previousObject` is the object that was returned from the last call + to `nextObject` for the current iteration. This is a useful way to + manage iteration if you are tracing a linked list, for example. + + Finally the context parameter will always contain a hash you can use as + a "scratchpad" to maintain any other state you need in order to iterate + properly. The context object is reused and is not reset between + iterations so make sure you setup the context with a fresh state whenever + the index parameter is 0. + + Generally iterators will continue to call `nextObject` until the index + reaches the your current length-1. If you run out of data before this + time for some reason, you should simply return undefined. + + The default implementation of this method simply looks up the index. + This works great on any Array-like objects. + + @method nextObject + @param {Number} index the current index of the iteration + @param {Object} previousObject the value returned by the last call to + `nextObject`. + @param {Object} context a context object you can use to maintain state. + @return {Object} the next object in the iteration or undefined + */ + nextObject: required(Function), + + /** + Helper method returns the first object from a collection. This is usually + used by bindings and other parts of the framework to extract a single + object if the enumerable contains only one item. + + If you override this method, you should implement it so that it will + always return the same value each time it is called. If your enumerable + contains only one object, this method should always return that object. + If your enumerable is empty, this method should return `undefined`. + + ```javascript + var arr = ["a", "b", "c"]; + arr.get('firstObject'); // "a" + + var arr = []; + arr.get('firstObject'); // undefined + ``` + + @property firstObject + @return {Object} the object or undefined + */ + firstObject: computed(function() { + if (get(this, 'length')===0) return undefined ; + + // handle generic enumerables + var context = popCtx(), ret; + ret = this.nextObject(0, null, context); + pushCtx(context); + return ret ; + }).property('[]'), + + /** + Helper method returns the last object from a collection. If your enumerable + contains only one object, this method should always return that object. + If your enumerable is empty, this method should return `undefined`. + + ```javascript + var arr = ["a", "b", "c"]; + arr.get('lastObject'); // "c" + + var arr = []; + arr.get('lastObject'); // undefined + ``` + + @property lastObject + @return {Object} the last object or undefined + */ + lastObject: computed(function() { + var len = get(this, 'length'); + if (len===0) return undefined ; + var context = popCtx(), idx=0, cur, last = null; + do { + last = cur; + cur = this.nextObject(idx++, last, context); + } while (cur !== undefined); + pushCtx(context); + return last; + }).property('[]'), + + /** + Returns `true` if the passed object can be found in the receiver. The + default version will iterate through the enumerable until the object + is found. You may want to override this with a more efficient version. + + ```javascript + var arr = ["a", "b", "c"]; + arr.contains("a"); // true + arr.contains("z"); // false + ``` + + @method contains + @param {Object} obj The object to search for. + @return {Boolean} `true` if object is found in enumerable. + */ + contains: function(obj) { + return this.find(function(item) { return item===obj; }) !== undefined; + }, + + /** + Iterates through the enumerable, calling the passed function on each + item. This method corresponds to the `forEach()` method defined in + JavaScript 1.6. + + The callback method you provide should have the following signature (all + parameters are optional): + + ```javascript + function(item, index, enumerable); + ``` + + - `item` is the current item in the iteration. + - `index` is the current index in the iteration. + - `enumerable` is the enumerable object itself. + + Note that in addition to a callback, you can also pass an optional target + object that will be set as `this` on the context. This is a good way + to give your iterator function access to the current object. + + @method forEach + @param {Function} callback The callback to execute + @param {Object} [target] The target object to use + @return {Object} receiver + */ + forEach: function(callback, target) { + if (typeof callback !== "function") throw new TypeError() ; + var len = get(this, 'length'), last = null, context = popCtx(); + + if (target === undefined) target = null; + + for(var idx=0;idx1) args = a_slice.call(arguments, 1); + + this.forEach(function(x, idx) { + var method = x && x[methodName]; + if ('function' === typeof method) { + ret[idx] = args ? apply(x, method, args) : x[methodName](); + } + }, this); + + return ret; + }, + + /** + Simply converts the enumerable into a genuine array. The order is not + guaranteed. Corresponds to the method implemented by Prototype. + + @method toArray + @return {Array} the enumerable as an array. + */ + toArray: function() { + var ret = Ember.A(); + this.forEach(function(o, idx) { ret[idx] = o; }); + return ret; + }, + + /** + Returns a copy of the array with all null and undefined elements removed. + + ```javascript + var arr = ["a", null, "c", undefined]; + arr.compact(); // ["a", "c"] + ``` + + @method compact + @return {Array} the array without null and undefined elements. + */ + compact: function() { + return this.filter(function(value) { return value != null; }); + }, + + /** + Returns a new enumerable that excludes the passed value. The default + implementation returns an array regardless of the receiver type unless + the receiver does not contain the value. + + ```javascript + var arr = ["a", "b", "a", "c"]; + arr.without("a"); // ["b", "c"] + ``` + + @method without + @param {Object} value + @return {Ember.Enumerable} + */ + without: function(value) { + if (!this.contains(value)) return this; // nothing to do + var ret = Ember.A(); + this.forEach(function(k) { + if (k !== value) ret[ret.length] = k; + }) ; + return ret ; + }, + + /** + Returns a new enumerable that contains only unique values. The default + implementation returns an array regardless of the receiver type. + + ```javascript + var arr = ["a", "a", "b", "b"]; + arr.uniq(); // ["a", "b"] + ``` + + @method uniq + @return {Ember.Enumerable} + */ + uniq: function() { + var ret = Ember.A(); + this.forEach(function(k) { + if (a_indexOf(ret, k)<0) ret.push(k); + }); + return ret; + }, + + /** + This property will trigger anytime the enumerable's content changes. + You can observe this property to be notified of changes to the enumerables + content. + + For plain enumerables, this property is read only. `Array` overrides + this method. + + @property [] + @type Array + @return this + */ + '[]': computed(function(key, value) { + return this; + }), + + // .......................................................... + // ENUMERABLE OBSERVERS + // + + /** + Registers an enumerable observer. Must implement `Ember.EnumerableObserver` + mixin. + + @method addEnumerableObserver + @param {Object} target + @param {Hash} [opts] + @return this + */ + addEnumerableObserver: function(target, opts) { + var willChange = (opts && opts.willChange) || 'enumerableWillChange', + didChange = (opts && opts.didChange) || 'enumerableDidChange'; + + var hasObservers = get(this, 'hasEnumerableObservers'); + if (!hasObservers) propertyWillChange(this, 'hasEnumerableObservers'); + addListener(this, '@enumerable:before', target, willChange); + addListener(this, '@enumerable:change', target, didChange); + if (!hasObservers) propertyDidChange(this, 'hasEnumerableObservers'); + return this; + }, + + /** + Removes a registered enumerable observer. + + @method removeEnumerableObserver + @param {Object} target + @param {Hash} [opts] + @return this + */ + removeEnumerableObserver: function(target, opts) { + var willChange = (opts && opts.willChange) || 'enumerableWillChange', + didChange = (opts && opts.didChange) || 'enumerableDidChange'; + + var hasObservers = get(this, 'hasEnumerableObservers'); + if (hasObservers) propertyWillChange(this, 'hasEnumerableObservers'); + removeListener(this, '@enumerable:before', target, willChange); + removeListener(this, '@enumerable:change', target, didChange); + if (hasObservers) propertyDidChange(this, 'hasEnumerableObservers'); + return this; + }, + + /** + Becomes true whenever the array currently has observers watching changes + on the array. + + @property hasEnumerableObservers + @type Boolean + */ + hasEnumerableObservers: computed(function() { + return hasListeners(this, '@enumerable:change') || hasListeners(this, '@enumerable:before'); + }), + + + /** + Invoke this method just before the contents of your enumerable will + change. You can either omit the parameters completely or pass the objects + to be removed or added if available or just a count. + + @method enumerableContentWillChange + @param {Ember.Enumerable|Number} removing An enumerable of the objects to + be removed or the number of items to be removed. + @param {Ember.Enumerable|Number} adding An enumerable of the objects to be + added or the number of items to be added. + @chainable + */ + enumerableContentWillChange: function(removing, adding) { + + var removeCnt, addCnt, hasDelta; + + if ('number' === typeof removing) removeCnt = removing; + else if (removing) removeCnt = get(removing, 'length'); + else removeCnt = removing = -1; + + if ('number' === typeof adding) addCnt = adding; + else if (adding) addCnt = get(adding,'length'); + else addCnt = adding = -1; + + hasDelta = addCnt<0 || removeCnt<0 || addCnt-removeCnt!==0; + + if (removing === -1) removing = null; + if (adding === -1) adding = null; + + propertyWillChange(this, '[]'); + if (hasDelta) propertyWillChange(this, 'length'); + sendEvent(this, '@enumerable:before', [this, removing, adding]); + + return this; + }, + + /** + Invoke this method when the contents of your enumerable has changed. + This will notify any observers watching for content changes. If your are + implementing an ordered enumerable (such as an array), also pass the + start and end values where the content changed so that it can be used to + notify range observers. + + @method enumerableContentDidChange + @param {Ember.Enumerable|Number} removing An enumerable of the objects to + be removed or the number of items to be removed. + @param {Ember.Enumerable|Number} adding An enumerable of the objects to + be added or the number of items to be added. + @chainable + */ + enumerableContentDidChange: function(removing, adding) { + var removeCnt, addCnt, hasDelta; + + if ('number' === typeof removing) removeCnt = removing; + else if (removing) removeCnt = get(removing, 'length'); + else removeCnt = removing = -1; + + if ('number' === typeof adding) addCnt = adding; + else if (adding) addCnt = get(adding, 'length'); + else addCnt = adding = -1; + + hasDelta = addCnt<0 || removeCnt<0 || addCnt-removeCnt!==0; + + if (removing === -1) removing = null; + if (adding === -1) adding = null; + + sendEvent(this, '@enumerable:change', [this, removing, adding]); + if (hasDelta) propertyDidChange(this, 'length'); + propertyDidChange(this, '[]'); + + return this ; + }, + + /** + Converts the enumerable into an array and sorts by the keys + specified in the argument. + + You may provide multiple arguments to sort by multiple properties. + + @method sortBy + @param {String} property name(s) to sort on + @return {Array} The sorted array. + @since 1.2.0 + */ + sortBy: function() { + var sortKeys = arguments; + return this.toArray().sort(function(a, b){ + for(var i = 0; i < sortKeys.length; i++) { + var key = sortKeys[i], + propA = get(a, key), + propB = get(b, key); + // return 1 or -1 else continue to the next sortKey + var compareValue = compare(propA, propB); + if (compareValue) { return compareValue; } + } + return 0; + }); + } + }); + + __exports__["default"] = Enumerable; + }); +define("ember-runtime/mixins/evented", + ["ember-metal/mixin","ember-metal/events","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + var Mixin = __dependency1__.Mixin; + var addListener = __dependency2__.addListener; + var removeListener = __dependency2__.removeListener; + var hasListeners = __dependency2__.hasListeners; + var sendEvent = __dependency2__.sendEvent; + + /** + @module ember + @submodule ember-runtime + */ + + /** + This mixin allows for Ember objects to subscribe to and emit events. + + ```javascript + App.Person = Ember.Object.extend(Ember.Evented, { + greet: function() { + // ... + this.trigger('greet'); + } + }); + + var person = App.Person.create(); + + person.on('greet', function() { + console.log('Our person has greeted'); + }); + + person.greet(); + + // outputs: 'Our person has greeted' + ``` + + You can also chain multiple event subscriptions: + + ```javascript + person.on('greet', function() { + console.log('Our person has greeted'); + }).one('greet', function() { + console.log('Offer one-time special'); + }).off('event', this, forgetThis); + ``` + + @class Evented + @namespace Ember + */ + var Evented = Mixin.create({ + + /** + Subscribes to a named event with given function. + + ```javascript + person.on('didLoad', function() { + // fired once the person has loaded + }); + ``` + + An optional target can be passed in as the 2nd argument that will + be set as the "this" for the callback. This is a good way to give your + function access to the object triggering the event. When the target + parameter is used the callback becomes the third argument. + + @method on + @param {String} name The name of the event + @param {Object} [target] The "this" binding for the callback + @param {Function} method The callback to execute + @return this + */ + on: function(name, target, method) { + addListener(this, name, target, method); + return this; + }, + + /** + Subscribes a function to a named event and then cancels the subscription + after the first time the event is triggered. It is good to use ``one`` when + you only care about the first time an event has taken place. + + This function takes an optional 2nd argument that will become the "this" + value for the callback. If this argument is passed then the 3rd argument + becomes the function. + + @method one + @param {String} name The name of the event + @param {Object} [target] The "this" binding for the callback + @param {Function} method The callback to execute + @return this + */ + one: function(name, target, method) { + if (!method) { + method = target; + target = null; + } + + addListener(this, name, target, method, true); + return this; + }, + + /** + Triggers a named event for the object. Any additional arguments + will be passed as parameters to the functions that are subscribed to the + event. + + ```javascript + person.on('didEat', function(food) { + console.log('person ate some ' + food); + }); + + person.trigger('didEat', 'broccoli'); + + // outputs: person ate some broccoli + ``` + @method trigger + @param {String} name The name of the event + @param {Object...} args Optional arguments to pass on + */ + trigger: function(name) { + var args = [], i, l; + for (i = 1, l = arguments.length; i < l; i++) { + args.push(arguments[i]); + } + sendEvent(this, name, args); + }, + + /** + Cancels subscription for given name, target, and method. + + @method off + @param {String} name The name of the event + @param {Object} target The target of the subscription + @param {Function} method The function of the subscription + @return this + */ + off: function(name, target, method) { + removeListener(this, name, target, method); + return this; + }, + + /** + Checks to see if object has any subscriptions for named event. + + @method has + @param {String} name The name of the event + @return {Boolean} does the object have a subscription for event + */ + has: function(name) { + return hasListeners(this, name); + } + }); + + __exports__["default"] = Evented; + }); +define("ember-runtime/mixins/freezable", + ["ember-metal/mixin","ember-metal/property_get","ember-metal/property_set","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __exports__) { + "use strict"; + /** + @module ember + @submodule ember-runtime + */ + + var Mixin = __dependency1__.Mixin; + var get = __dependency2__.get; + var set = __dependency3__.set; + + /** + The `Ember.Freezable` mixin implements some basic methods for marking an + object as frozen. Once an object is frozen it should be read only. No changes + may be made the internal state of the object. + + ## Enforcement + + To fully support freezing in your subclass, you must include this mixin and + override any method that might alter any property on the object to instead + raise an exception. You can check the state of an object by checking the + `isFrozen` property. + + Although future versions of JavaScript may support language-level freezing + object objects, that is not the case today. Even if an object is freezable, + it is still technically possible to modify the object, even though it could + break other parts of your application that do not expect a frozen object to + change. It is, therefore, very important that you always respect the + `isFrozen` property on all freezable objects. + + ## Example Usage + + The example below shows a simple object that implement the `Ember.Freezable` + protocol. + + ```javascript + Contact = Ember.Object.extend(Ember.Freezable, { + firstName: null, + lastName: null, + + // swaps the names + swapNames: function() { + if (this.get('isFrozen')) throw Ember.FROZEN_ERROR; + var tmp = this.get('firstName'); + this.set('firstName', this.get('lastName')); + this.set('lastName', tmp); + return this; + } + + }); + + c = Contact.create({ firstName: "John", lastName: "Doe" }); + c.swapNames(); // returns c + c.freeze(); + c.swapNames(); // EXCEPTION + ``` + + ## Copying + + Usually the `Ember.Freezable` protocol is implemented in cooperation with the + `Ember.Copyable` protocol, which defines a `frozenCopy()` method that will + return a frozen object, if the object implements this method as well. + + @class Freezable + @namespace Ember + @since Ember 0.9 + */ + var Freezable = Mixin.create({ + + /** + Set to `true` when the object is frozen. Use this property to detect + whether your object is frozen or not. + + @property isFrozen + @type Boolean + */ + isFrozen: false, + + /** + Freezes the object. Once this method has been called the object should + no longer allow any properties to be edited. + + @method freeze + @return {Object} receiver + */ + freeze: function() { + if (get(this, 'isFrozen')) return this; + set(this, 'isFrozen', true); + return this; + } + + }); + + var FROZEN_ERROR = "Frozen object cannot be modified."; + + __exports__.Freezable = Freezable; + __exports__.FROZEN_ERROR = FROZEN_ERROR; + }); +define("ember-runtime/mixins/mutable_array", + ["ember-metal/property_get","ember-metal/property_set","ember-metal/utils","ember-metal/error","ember-metal/mixin","ember-runtime/mixins/array","ember-runtime/mixins/mutable_enumerable","ember-runtime/mixins/enumerable","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __exports__) { + "use strict"; + /** + @module ember + @submodule ember-runtime + */ + + + // require('ember-runtime/mixins/array'); + // require('ember-runtime/mixins/mutable_enumerable'); + + // .......................................................... + // CONSTANTS + // + + var OUT_OF_RANGE_EXCEPTION = "Index out of range"; + var EMPTY = []; + + // .......................................................... + // HELPERS + // + + var get = __dependency1__.get; + var set = __dependency2__.set; + var isArray = __dependency3__.isArray; + var EmberError = __dependency4__["default"]; + var Mixin = __dependency5__.Mixin; + var required = __dependency5__.required; + var EmberArray = __dependency6__["default"]; + var MutableEnumerable = __dependency7__["default"]; + var Enumerable = __dependency8__["default"]; + /** + This mixin defines the API for modifying array-like objects. These methods + can be applied only to a collection that keeps its items in an ordered set. + It builds upon the Array mixin and adds methods to modify the array. + Concrete implementations of this class include ArrayProxy and ArrayController. + + It is important to use the methods in this class to modify arrays so that + changes are observable. This allows the binding system in Ember to function + correctly. + + + Note that an Array can change even if it does not implement this mixin. + For example, one might implement a SparseArray that cannot be directly + modified, but if its underlying enumerable changes, it will change also. + + @class MutableArray + @namespace Ember + @uses Ember.Array + @uses Ember.MutableEnumerable + */ + var MutableArray = Mixin.create(EmberArray, MutableEnumerable, { + + /** + __Required.__ You must implement this method to apply this mixin. + + This is one of the primitives you must implement to support `Ember.Array`. + You should replace amt objects started at idx with the objects in the + passed array. You should also call `this.enumerableContentDidChange()` + + @method replace + @param {Number} idx Starting index in the array to replace. If + idx >= length, then append to the end of the array. + @param {Number} amt Number of elements that should be removed from + the array, starting at *idx*. + @param {Array} objects An array of zero or more objects that should be + inserted into the array at *idx* + */ + replace: required(), + + /** + Remove all elements from the array. This is useful if you + want to reuse an existing array without having to recreate it. + + ```javascript + var colors = ["red", "green", "blue"]; + color.length(); // 3 + colors.clear(); // [] + colors.length(); // 0 + ``` + + @method clear + @return {Ember.Array} An empty Array. + */ + clear: function () { + var len = get(this, 'length'); + if (len === 0) return this; + this.replace(0, len, EMPTY); + return this; + }, + + /** + This will use the primitive `replace()` method to insert an object at the + specified index. + + ```javascript + var colors = ["red", "green", "blue"]; + colors.insertAt(2, "yellow"); // ["red", "green", "yellow", "blue"] + colors.insertAt(5, "orange"); // Error: Index out of range + ``` + + @method insertAt + @param {Number} idx index of insert the object at. + @param {Object} object object to insert + @return {Ember.Array} receiver + */ + insertAt: function(idx, object) { + if (idx > get(this, 'length')) throw new EmberError(OUT_OF_RANGE_EXCEPTION); + this.replace(idx, 0, [object]); + return this; + }, + + /** + Remove an object at the specified index using the `replace()` primitive + method. You can pass either a single index, or a start and a length. + + If you pass a start and length that is beyond the + length this method will throw an `OUT_OF_RANGE_EXCEPTION`. + + ```javascript + var colors = ["red", "green", "blue", "yellow", "orange"]; + colors.removeAt(0); // ["green", "blue", "yellow", "orange"] + colors.removeAt(2, 2); // ["green", "blue"] + colors.removeAt(4, 2); // Error: Index out of range + ``` + + @method removeAt + @param {Number} start index, start of range + @param {Number} len length of passing range + @return {Ember.Array} receiver + */ + removeAt: function(start, len) { + if ('number' === typeof start) { + + if ((start < 0) || (start >= get(this, 'length'))) { + throw new EmberError(OUT_OF_RANGE_EXCEPTION); + } + + // fast case + if (len === undefined) len = 1; + this.replace(start, len, EMPTY); + } + + return this; + }, + + /** + Push the object onto the end of the array. Works just like `push()` but it + is KVO-compliant. + + ```javascript + var colors = ["red", "green"]; + colors.pushObject("black"); // ["red", "green", "black"] + colors.pushObject(["yellow"]); // ["red", "green", ["yellow"]] + ``` + + @method pushObject + @param {*} obj object to push + @return object same object passed as a param + */ + pushObject: function(obj) { + this.insertAt(get(this, 'length'), obj); + return obj; + }, + + /** + Add the objects in the passed numerable to the end of the array. Defers + notifying observers of the change until all objects are added. + + ```javascript + var colors = ["red"]; + colors.pushObjects(["yellow", "orange"]); // ["red", "yellow", "orange"] + ``` + + @method pushObjects + @param {Ember.Enumerable} objects the objects to add + @return {Ember.Array} receiver + */ + pushObjects: function(objects) { + if (!(Enumerable.detect(objects) || isArray(objects))) { + throw new TypeError("Must pass Ember.Enumerable to Ember.MutableArray#pushObjects"); + } + this.replace(get(this, 'length'), 0, objects); + return this; + }, + + /** + Pop object from array or nil if none are left. Works just like `pop()` but + it is KVO-compliant. + + ```javascript + var colors = ["red", "green", "blue"]; + colors.popObject(); // "blue" + console.log(colors); // ["red", "green"] + ``` + + @method popObject + @return object + */ + popObject: function() { + var len = get(this, 'length'); + if (len === 0) return null; + + var ret = this.objectAt(len-1); + this.removeAt(len-1, 1); + return ret; + }, + + /** + Shift an object from start of array or nil if none are left. Works just + like `shift()` but it is KVO-compliant. + + ```javascript + var colors = ["red", "green", "blue"]; + colors.shiftObject(); // "red" + console.log(colors); // ["green", "blue"] + ``` + + @method shiftObject + @return object + */ + shiftObject: function() { + if (get(this, 'length') === 0) return null; + var ret = this.objectAt(0); + this.removeAt(0); + return ret; + }, + + /** + Unshift an object to start of array. Works just like `unshift()` but it is + KVO-compliant. + + ```javascript + var colors = ["red"]; + colors.unshiftObject("yellow"); // ["yellow", "red"] + colors.unshiftObject(["black"]); // [["black"], "yellow", "red"] + ``` + + @method unshiftObject + @param {*} obj object to unshift + @return object same object passed as a param + */ + unshiftObject: function(obj) { + this.insertAt(0, obj); + return obj; + }, + + /** + Adds the named objects to the beginning of the array. Defers notifying + observers until all objects have been added. + + ```javascript + var colors = ["red"]; + colors.unshiftObjects(["black", "white"]); // ["black", "white", "red"] + colors.unshiftObjects("yellow"); // Type Error: 'undefined' is not a function + ``` + + @method unshiftObjects + @param {Ember.Enumerable} objects the objects to add + @return {Ember.Array} receiver + */ + unshiftObjects: function(objects) { + this.replace(0, 0, objects); + return this; + }, + + /** + Reverse objects in the array. Works just like `reverse()` but it is + KVO-compliant. + + @method reverseObjects + @return {Ember.Array} receiver + */ + reverseObjects: function() { + var len = get(this, 'length'); + if (len === 0) return this; + var objects = this.toArray().reverse(); + this.replace(0, len, objects); + return this; + }, + + /** + Replace all the the receiver's content with content of the argument. + If argument is an empty array receiver will be cleared. + + ```javascript + var colors = ["red", "green", "blue"]; + colors.setObjects(["black", "white"]); // ["black", "white"] + colors.setObjects([]); // [] + ``` + + @method setObjects + @param {Ember.Array} objects array whose content will be used for replacing + the content of the receiver + @return {Ember.Array} receiver with the new content + */ + setObjects: function(objects) { + if (objects.length === 0) return this.clear(); + + var len = get(this, 'length'); + this.replace(0, len, objects); + return this; + }, + + // .......................................................... + // IMPLEMENT Ember.MutableEnumerable + // + + /** + Remove all occurances of an object in the array. + + ```javascript + var cities = ["Chicago", "Berlin", "Lima", "Chicago"]; + cities.removeObject("Chicago"); // ["Berlin", "Lima"] + cities.removeObject("Lima"); // ["Berlin"] + cities.removeObject("Tokyo") // ["Berlin"] + ``` + + @method removeObject + @param {*} obj object to remove + @return {Ember.Array} receiver + */ + removeObject: function(obj) { + var loc = get(this, 'length') || 0; + while(--loc >= 0) { + var curObject = this.objectAt(loc); + if (curObject === obj) this.removeAt(loc); + } + return this; + }, + + /** + Push the object onto the end of the array if it is not already + present in the array. + + ```javascript + var cities = ["Chicago", "Berlin"]; + cities.addObject("Lima"); // ["Chicago", "Berlin", "Lima"] + cities.addObject("Berlin"); // ["Chicago", "Berlin", "Lima"] + ``` + + @method addObject + @param {*} obj object to add, if not already present + @return {Ember.Array} receiver + */ + addObject: function(obj) { + if (!this.contains(obj)) this.pushObject(obj); + return this; + } + + }); + + __exports__["default"] = MutableArray; + }); +define("ember-runtime/mixins/mutable_enumerable", + ["ember-metal/enumerable_utils","ember-runtime/mixins/enumerable","ember-metal/mixin","ember-metal/property_events","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __exports__) { + "use strict"; + var EnumerableUtils = __dependency1__["default"]; + var Enumerable = __dependency2__["default"]; + var Mixin = __dependency3__.Mixin; + var required = __dependency3__.required; + var beginPropertyChanges = __dependency4__.beginPropertyChanges; + var endPropertyChanges = __dependency4__.endPropertyChanges; + + /** + @module ember + @submodule ember-runtime + */ + + var forEach = EnumerableUtils.forEach; + + /** + This mixin defines the API for modifying generic enumerables. These methods + can be applied to an object regardless of whether it is ordered or + unordered. + + Note that an Enumerable can change even if it does not implement this mixin. + For example, a MappedEnumerable cannot be directly modified but if its + underlying enumerable changes, it will change also. + + ## Adding Objects + + To add an object to an enumerable, use the `addObject()` method. This + method will only add the object to the enumerable if the object is not + already present and is of a type supported by the enumerable. + + ```javascript + set.addObject(contact); + ``` + + ## Removing Objects + + To remove an object from an enumerable, use the `removeObject()` method. This + will only remove the object if it is present in the enumerable, otherwise + this method has no effect. + + ```javascript + set.removeObject(contact); + ``` + + ## Implementing In Your Own Code + + If you are implementing an object and want to support this API, just include + this mixin in your class and implement the required methods. In your unit + tests, be sure to apply the Ember.MutableEnumerableTests to your object. + + @class MutableEnumerable + @namespace Ember + @uses Ember.Enumerable + */ + var MutableEnumerable = Mixin.create(Enumerable, { + + /** + __Required.__ You must implement this method to apply this mixin. + + Attempts to add the passed object to the receiver if the object is not + already present in the collection. If the object is present, this method + has no effect. + + If the passed object is of a type not supported by the receiver, + then this method should raise an exception. + + @method addObject + @param {Object} object The object to add to the enumerable. + @return {Object} the passed object + */ + addObject: required(Function), + + /** + Adds each object in the passed enumerable to the receiver. + + @method addObjects + @param {Ember.Enumerable} objects the objects to add. + @return {Object} receiver + */ + addObjects: function(objects) { + beginPropertyChanges(this); + forEach(objects, function(obj) { this.addObject(obj); }, this); + endPropertyChanges(this); + return this; + }, + + /** + __Required.__ You must implement this method to apply this mixin. + + Attempts to remove the passed object from the receiver collection if the + object is present in the collection. If the object is not present, + this method has no effect. + + If the passed object is of a type not supported by the receiver, + then this method should raise an exception. + + @method removeObject + @param {Object} object The object to remove from the enumerable. + @return {Object} the passed object + */ + removeObject: required(Function), + + + /** + Removes each object in the passed enumerable from the receiver. + + @method removeObjects + @param {Ember.Enumerable} objects the objects to remove + @return {Object} receiver + */ + removeObjects: function(objects) { + beginPropertyChanges(this); + for (var i = objects.length - 1; i >= 0; i--) { + this.removeObject(objects[i]); + } + endPropertyChanges(this); + return this; + } + }); + + __exports__["default"] = MutableEnumerable; + }); +define("ember-runtime/mixins/observable", + ["ember-metal/core","ember-metal/property_get","ember-metal/property_set","ember-metal/utils","ember-metal/get_properties","ember-metal/set_properties","ember-metal/mixin","ember-metal/events","ember-metal/property_events","ember-metal/observer","ember-metal/computed","ember-metal/is_none","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __dependency11__, __dependency12__, __exports__) { + "use strict"; + /** + @module ember + @submodule ember-runtime + */ + var Ember = __dependency1__["default"]; + // Ember.assert + + var get = __dependency2__.get; + var getWithDefault = __dependency2__.getWithDefault; + var set = __dependency3__.set; + var apply = __dependency4__.apply; + var getProperties = __dependency5__["default"]; + var setProperties = __dependency6__["default"]; + var Mixin = __dependency7__.Mixin; + var hasListeners = __dependency8__.hasListeners; + var beginPropertyChanges = __dependency9__.beginPropertyChanges; + var propertyWillChange = __dependency9__.propertyWillChange; + var propertyDidChange = __dependency9__.propertyDidChange; + var endPropertyChanges = __dependency9__.endPropertyChanges; + var addObserver = __dependency10__.addObserver; + var addBeforeObserver = __dependency10__.addBeforeObserver; + var removeObserver = __dependency10__.removeObserver; + var observersFor = __dependency10__.observersFor; + var cacheFor = __dependency11__.cacheFor; + var isNone = __dependency12__.isNone; + + + var slice = Array.prototype.slice; + /** + ## Overview + + This mixin provides properties and property observing functionality, core + features of the Ember object model. + + Properties and observers allow one object to observe changes to a + property on another object. This is one of the fundamental ways that + models, controllers and views communicate with each other in an Ember + application. + + Any object that has this mixin applied can be used in observer + operations. That includes `Ember.Object` and most objects you will + interact with as you write your Ember application. + + Note that you will not generally apply this mixin to classes yourself, + but you will use the features provided by this module frequently, so it + is important to understand how to use it. + + ## Using `get()` and `set()` + + Because of Ember's support for bindings and observers, you will always + access properties using the get method, and set properties using the + set method. This allows the observing objects to be notified and + computed properties to be handled properly. + + More documentation about `get` and `set` are below. + + ## Observing Property Changes + + You typically observe property changes simply by adding the `observes` + call to the end of your method declarations in classes that you write. + For example: + + ```javascript + Ember.Object.extend({ + valueObserver: function() { + // Executes whenever the "value" property changes + }.observes('value') + }); + ``` + + Although this is the most common way to add an observer, this capability + is actually built into the `Ember.Object` class on top of two methods + defined in this mixin: `addObserver` and `removeObserver`. You can use + these two methods to add and remove observers yourself if you need to + do so at runtime. + + To add an observer for a property, call: + + ```javascript + object.addObserver('propertyKey', targetObject, targetAction) + ``` + + This will call the `targetAction` method on the `targetObject` whenever + the value of the `propertyKey` changes. + + Note that if `propertyKey` is a computed property, the observer will be + called when any of the property dependencies are changed, even if the + resulting value of the computed property is unchanged. This is necessary + because computed properties are not computed until `get` is called. + + @class Observable + @namespace Ember + */ + var Observable = Mixin.create({ + + /** + Retrieves the value of a property from the object. + + This method is usually similar to using `object[keyName]` or `object.keyName`, + however it supports both computed properties and the unknownProperty + handler. + + Because `get` unifies the syntax for accessing all these kinds + of properties, it can make many refactorings easier, such as replacing a + simple property with a computed property, or vice versa. + + ### Computed Properties + + Computed properties are methods defined with the `property` modifier + declared at the end, such as: + + ```javascript + fullName: function() { + return this.get('firstName') + ' ' + this.get('lastName'); + }.property('firstName', 'lastName') + ``` + + When you call `get` on a computed property, the function will be + called and the return value will be returned instead of the function + itself. + + ### Unknown Properties + + Likewise, if you try to call `get` on a property whose value is + `undefined`, the `unknownProperty()` method will be called on the object. + If this method returns any value other than `undefined`, it will be returned + instead. This allows you to implement "virtual" properties that are + not defined upfront. + + @method get + @param {String} keyName The property to retrieve + @return {Object} The property value or undefined. + */ + get: function(keyName) { + return get(this, keyName); + }, + + /** + To get multiple properties at once, call `getProperties` + with a list of strings or an array: + + ```javascript + record.getProperties('firstName', 'lastName', 'zipCode'); // { firstName: 'John', lastName: 'Doe', zipCode: '10011' } + ``` + + is equivalent to: + + ```javascript + record.getProperties(['firstName', 'lastName', 'zipCode']); // { firstName: 'John', lastName: 'Doe', zipCode: '10011' } + ``` + + @method getProperties + @param {String...|Array} list of keys to get + @return {Hash} + */ + getProperties: function() { + return apply(null, getProperties, [this].concat(slice.call(arguments))); + }, + + /** + Sets the provided key or path to the value. + + This method is generally very similar to calling `object[key] = value` or + `object.key = value`, except that it provides support for computed + properties, the `setUnknownProperty()` method and property observers. + + ### Computed Properties + + If you try to set a value on a key that has a computed property handler + defined (see the `get()` method for an example), then `set()` will call + that method, passing both the value and key instead of simply changing + the value itself. This is useful for those times when you need to + implement a property that is composed of one or more member + properties. + + ### Unknown Properties + + If you try to set a value on a key that is undefined in the target + object, then the `setUnknownProperty()` handler will be called instead. This + gives you an opportunity to implement complex "virtual" properties that + are not predefined on the object. If `setUnknownProperty()` returns + undefined, then `set()` will simply set the value on the object. + + ### Property Observers + + In addition to changing the property, `set()` will also register a property + change with the object. Unless you have placed this call inside of a + `beginPropertyChanges()` and `endPropertyChanges(),` any "local" observers + (i.e. observer methods declared on the same object), will be called + immediately. Any "remote" observers (i.e. observer methods declared on + another object) will be placed in a queue and called at a later time in a + coalesced manner. + + ### Chaining + + In addition to property changes, `set()` returns the value of the object + itself so you can do chaining like this: + + ```javascript + record.set('firstName', 'Charles').set('lastName', 'Jolley'); + ``` + + @method set + @param {String} keyName The property to set + @param {Object} value The value to set or `null`. + @return {Ember.Observable} + */ + set: function(keyName, value) { + set(this, keyName, value); + return this; + }, + + + /** + Sets a list of properties at once. These properties are set inside + a single `beginPropertyChanges` and `endPropertyChanges` batch, so + observers will be buffered. + + ```javascript + record.setProperties({ firstName: 'Charles', lastName: 'Jolley' }); + ``` + + @method setProperties + @param {Hash} hash the hash of keys and values to set + @return {Ember.Observable} + */ + setProperties: function(hash) { + return setProperties(this, hash); + }, + + /** + Begins a grouping of property changes. + + You can use this method to group property changes so that notifications + will not be sent until the changes are finished. If you plan to make a + large number of changes to an object at one time, you should call this + method at the beginning of the changes to begin deferring change + notifications. When you are done making changes, call + `endPropertyChanges()` to deliver the deferred change notifications and end + deferring. + + @method beginPropertyChanges + @return {Ember.Observable} + */ + beginPropertyChanges: function() { + beginPropertyChanges(); + return this; + }, + + /** + Ends a grouping of property changes. + + You can use this method to group property changes so that notifications + will not be sent until the changes are finished. If you plan to make a + large number of changes to an object at one time, you should call + `beginPropertyChanges()` at the beginning of the changes to defer change + notifications. When you are done making changes, call this method to + deliver the deferred change notifications and end deferring. + + @method endPropertyChanges + @return {Ember.Observable} + */ + endPropertyChanges: function() { + endPropertyChanges(); + return this; + }, + + /** + Notify the observer system that a property is about to change. + + Sometimes you need to change a value directly or indirectly without + actually calling `get()` or `set()` on it. In this case, you can use this + method and `propertyDidChange()` instead. Calling these two methods + together will notify all observers that the property has potentially + changed value. + + Note that you must always call `propertyWillChange` and `propertyDidChange` + as a pair. If you do not, it may get the property change groups out of + order and cause notifications to be delivered more often than you would + like. + + @method propertyWillChange + @param {String} keyName The property key that is about to change. + @return {Ember.Observable} + */ + propertyWillChange: function(keyName) { + propertyWillChange(this, keyName); + return this; + }, + + /** + Notify the observer system that a property has just changed. + + Sometimes you need to change a value directly or indirectly without + actually calling `get()` or `set()` on it. In this case, you can use this + method and `propertyWillChange()` instead. Calling these two methods + together will notify all observers that the property has potentially + changed value. + + Note that you must always call `propertyWillChange` and `propertyDidChange` + as a pair. If you do not, it may get the property change groups out of + order and cause notifications to be delivered more often than you would + like. + + @method propertyDidChange + @param {String} keyName The property key that has just changed. + @return {Ember.Observable} + */ + propertyDidChange: function(keyName) { + propertyDidChange(this, keyName); + return this; + }, + + /** + Convenience method to call `propertyWillChange` and `propertyDidChange` in + succession. + + @method notifyPropertyChange + @param {String} keyName The property key to be notified about. + @return {Ember.Observable} + */ + notifyPropertyChange: function(keyName) { + this.propertyWillChange(keyName); + this.propertyDidChange(keyName); + return this; + }, + + addBeforeObserver: function(key, target, method) { + addBeforeObserver(this, key, target, method); + }, + + /** + Adds an observer on a property. + + This is the core method used to register an observer for a property. + + Once you call this method, any time the key's value is set, your observer + will be notified. Note that the observers are triggered any time the + value is set, regardless of whether it has actually changed. Your + observer should be prepared to handle that. + + You can also pass an optional context parameter to this method. The + context will be passed to your observer method whenever it is triggered. + Note that if you add the same target/method pair on a key multiple times + with different context parameters, your observer will only be called once + with the last context you passed. + + ### Observer Methods + + Observer methods you pass should generally have the following signature if + you do not pass a `context` parameter: + + ```javascript + fooDidChange: function(sender, key, value, rev) { }; + ``` + + The sender is the object that changed. The key is the property that + changes. The value property is currently reserved and unused. The rev + is the last property revision of the object when it changed, which you can + use to detect if the key value has really changed or not. + + If you pass a `context` parameter, the context will be passed before the + revision like so: + + ```javascript + fooDidChange: function(sender, key, value, context, rev) { }; + ``` + + Usually you will not need the value, context or revision parameters at + the end. In this case, it is common to write observer methods that take + only a sender and key value as parameters or, if you aren't interested in + any of these values, to write an observer that has no parameters at all. + + @method addObserver + @param {String} key The key to observer + @param {Object} target The target object to invoke + @param {String|Function} method The method to invoke. + */ + addObserver: function(key, target, method) { + addObserver(this, key, target, method); + }, + + /** + Remove an observer you have previously registered on this object. Pass + the same key, target, and method you passed to `addObserver()` and your + target will no longer receive notifications. + + @method removeObserver + @param {String} key The key to observer + @param {Object} target The target object to invoke + @param {String|Function} method The method to invoke. + */ + removeObserver: function(key, target, method) { + removeObserver(this, key, target, method); + }, + + /** + Returns `true` if the object currently has observers registered for a + particular key. You can use this method to potentially defer performing + an expensive action until someone begins observing a particular property + on the object. + + @method hasObserverFor + @param {String} key Key to check + @return {Boolean} + */ + hasObserverFor: function(key) { + return hasListeners(this, key+':change'); + }, + + /** + Retrieves the value of a property, or a default value in the case that the + property returns `undefined`. + + ```javascript + person.getWithDefault('lastName', 'Doe'); + ``` + + @method getWithDefault + @param {String} keyName The name of the property to retrieve + @param {Object} defaultValue The value to return if the property value is undefined + @return {Object} The property value or the defaultValue. + */ + getWithDefault: function(keyName, defaultValue) { + return getWithDefault(this, keyName, defaultValue); + }, + + /** + Set the value of a property to the current value plus some amount. + + ```javascript + person.incrementProperty('age'); + team.incrementProperty('score', 2); + ``` + + @method incrementProperty + @param {String} keyName The name of the property to increment + @param {Number} increment The amount to increment by. Defaults to 1 + @return {Number} The new property value + */ + incrementProperty: function(keyName, increment) { + if (isNone(increment)) { increment = 1; } + Ember.assert("Must pass a numeric value to incrementProperty", (!isNaN(parseFloat(increment)) && isFinite(increment))); + set(this, keyName, (parseFloat(get(this, keyName)) || 0) + increment); + return get(this, keyName); + }, + + /** + Set the value of a property to the current value minus some amount. + + ```javascript + player.decrementProperty('lives'); + orc.decrementProperty('health', 5); + ``` + + @method decrementProperty + @param {String} keyName The name of the property to decrement + @param {Number} decrement The amount to decrement by. Defaults to 1 + @return {Number} The new property value + */ + decrementProperty: function(keyName, decrement) { + if (isNone(decrement)) { decrement = 1; } + Ember.assert("Must pass a numeric value to decrementProperty", (!isNaN(parseFloat(decrement)) && isFinite(decrement))); + set(this, keyName, (get(this, keyName) || 0) - decrement); + return get(this, keyName); + }, + + /** + Set the value of a boolean property to the opposite of it's + current value. + + ```javascript + starship.toggleProperty('warpDriveEngaged'); + ``` + + @method toggleProperty + @param {String} keyName The name of the property to toggle + @return {Object} The new property value + */ + toggleProperty: function(keyName) { + set(this, keyName, !get(this, keyName)); + return get(this, keyName); + }, + + /** + Returns the cached value of a computed property, if it exists. + This allows you to inspect the value of a computed property + without accidentally invoking it if it is intended to be + generated lazily. + + @method cacheFor + @param {String} keyName + @return {Object} The cached value of the computed property, if any + */ + cacheFor: function(keyName) { + return cacheFor(this, keyName); + }, + + // intended for debugging purposes + observersForKey: function(keyName) { + return observersFor(this, keyName); + } + }); + + __exports__["default"] = Observable; + }); +define("ember-runtime/mixins/promise_proxy", + ["ember-metal/property_get","ember-metal/property_set","ember-metal/computed","ember-metal/mixin","ember-metal/error","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __exports__) { + "use strict"; + var get = __dependency1__.get; + var set = __dependency2__.set; + var computed = __dependency3__.computed; + var Mixin = __dependency4__.Mixin; + var EmberError = __dependency5__["default"]; + + var not = computed.not, or = computed.or; + + /** + @module ember + @submodule ember-runtime + */ + + function tap(proxy, promise) { + set(proxy, 'isFulfilled', false); + set(proxy, 'isRejected', false); + + return promise.then(function(value) { + set(proxy, 'isFulfilled', true); + set(proxy, 'content', value); + return value; + }, function(reason) { + set(proxy, 'isRejected', true); + set(proxy, 'reason', reason); + throw reason; + }, "Ember: PromiseProxy"); + } + + /** + A low level mixin making ObjectProxy, ObjectController or ArrayController's promise aware. + + ```javascript + var ObjectPromiseController = Ember.ObjectController.extend(Ember.PromiseProxyMixin); + + var controller = ObjectPromiseController.create({ + promise: $.getJSON('/some/remote/data.json') + }); + + controller.then(function(json){ + // the json + }, function(reason) { + // the reason why you have no json + }); + ``` + + the controller has bindable attributes which + track the promises life cycle + + ```javascript + controller.get('isPending') //=> true + controller.get('isSettled') //=> false + controller.get('isRejected') //=> false + controller.get('isFulfilled') //=> false + ``` + + When the the $.getJSON completes, and the promise is fulfilled + with json, the life cycle attributes will update accordingly. + + ```javascript + controller.get('isPending') //=> false + controller.get('isSettled') //=> true + controller.get('isRejected') //=> false + controller.get('isFulfilled') //=> true + ``` + + As the controller is an ObjectController, and the json now its content, + all the json properties will be available directly from the controller. + + ```javascript + // Assuming the following json: + { + firstName: 'Stefan', + lastName: 'Penner' + } + + // both properties will accessible on the controller + controller.get('firstName') //=> 'Stefan' + controller.get('lastName') //=> 'Penner' + ``` + + If the controller is backing a template, the attributes are + bindable from within that template + + ```handlebars + {{#if isPending}} + loading... + {{else}} + firstName: {{firstName}} + lastName: {{lastName}} + {{/if}} + ``` + @class Ember.PromiseProxyMixin + */ + var PromiseProxyMixin = Mixin.create({ + /** + If the proxied promise is rejected this will contain the reason + provided. + + @property reason + @default null + */ + reason: null, + + /** + Once the proxied promise has settled this will become `false`. + + @property isPending + @default true + */ + isPending: not('isSettled').readOnly(), + + /** + Once the proxied promise has settled this will become `true`. + + @property isSettled + @default false + */ + isSettled: or('isRejected', 'isFulfilled').readOnly(), + + /** + Will become `true` if the proxied promise is rejected. + + @property isRejected + @default false + */ + isRejected: false, + + /** + Will become `true` if the proxied promise is fulfilled. + + @property isFullfilled + @default false + */ + isFulfilled: false, + + /** + The promise whose fulfillment value is being proxied by this object. + + This property must be specified upon creation, and should not be + changed once created. + + Example: + + ```javascript + Ember.ObjectController.extend(Ember.PromiseProxyMixin).create({ + promise: + }); + ``` + + @property promise + */ + promise: computed(function(key, promise) { + if (arguments.length === 2) { + return tap(this, promise); + } else { + throw new EmberError("PromiseProxy's promise must be set"); + } + }), + + /** + An alias to the proxied promise's `then`. + + See RSVP.Promise.then. + + @method then + @param {Function} callback + @return {RSVP.Promise} + */ + then: promiseAlias('then'), + + /** + An alias to the proxied promise's `catch`. + + See RSVP.Promise.catch. + + @method catch + @param {Function} callback + @return {RSVP.Promise} + @since 1.3.0 + */ + 'catch': promiseAlias('catch'), + + /** + An alias to the proxied promise's `finally`. + + See RSVP.Promise.finally. + + @method finally + @param {Function} callback + @return {RSVP.Promise} + @since 1.3.0 + */ + 'finally': promiseAlias('finally') + + }); + + function promiseAlias(name) { + return function () { + var promise = get(this, 'promise'); + return promise[name].apply(promise, arguments); + }; + } + + __exports__["default"] = PromiseProxyMixin; + }); +define("ember-runtime/mixins/sortable", + ["ember-metal/core","ember-metal/property_get","ember-metal/property_set","ember-metal/enumerable_utils","ember-metal/mixin","ember-runtime/mixins/mutable_enumerable","ember-runtime/compare","ember-metal/observer","ember-metal/computed","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __exports__) { + "use strict"; + /** + @module ember + @submodule ember-runtime + */ + + var Ember = __dependency1__["default"]; + // Ember.assert, Ember.A + + var get = __dependency2__.get; + var set = __dependency3__.set; + var EnumerableUtils = __dependency4__["default"]; + var Mixin = __dependency5__.Mixin; + var MutableEnumerable = __dependency6__["default"]; + var compare = __dependency7__["default"]; + var addObserver = __dependency8__.addObserver; + var removeObserver = __dependency8__.removeObserver; + var computed = __dependency9__.computed; + var beforeObserver = __dependency5__.beforeObserver; + var observer = __dependency5__.observer; + //ES6TODO: should we access these directly from their package or from how thier exposed in ember-metal? + + var forEach = EnumerableUtils.forEach; + + /** + `Ember.SortableMixin` provides a standard interface for array proxies + to specify a sort order and maintain this sorting when objects are added, + removed, or updated without changing the implicit order of their underlying + content array: + + ```javascript + songs = [ + {trackNumber: 4, title: 'Ob-La-Di, Ob-La-Da'}, + {trackNumber: 2, title: 'Back in the U.S.S.R.'}, + {trackNumber: 3, title: 'Glass Onion'}, + ]; + + songsController = Ember.ArrayController.create({ + content: songs, + sortProperties: ['trackNumber'], + sortAscending: true + }); + + songsController.get('firstObject'); // {trackNumber: 2, title: 'Back in the U.S.S.R.'} + + songsController.addObject({trackNumber: 1, title: 'Dear Prudence'}); + songsController.get('firstObject'); // {trackNumber: 1, title: 'Dear Prudence'} + ``` + + If you add or remove the properties to sort by or change the sort direction the content + sort order will be automatically updated. + + ```javascript + songsController.set('sortProperties', ['title']); + songsController.get('firstObject'); // {trackNumber: 2, title: 'Back in the U.S.S.R.'} + + songsController.toggleProperty('sortAscending'); + songsController.get('firstObject'); // {trackNumber: 4, title: 'Ob-La-Di, Ob-La-Da'} + ``` + + SortableMixin works by sorting the arrangedContent array, which is the array that + arrayProxy displays. Due to the fact that the underlying 'content' array is not changed, that + array will not display the sorted list: + + ```javascript + songsController.get('content').get('firstObject'); // Returns the unsorted original content + songsController.get('firstObject'); // Returns the sorted content. + ``` + + Although the sorted content can also be accessed through the arrangedContent property, + it is preferable to use the proxied class and not the arrangedContent array directly. + + @class SortableMixin + @namespace Ember + @uses Ember.MutableEnumerable + */ + var SortableMixin = Mixin.create(MutableEnumerable, { + + /** + Specifies which properties dictate the arrangedContent's sort order. + + When specifying multiple properties the sorting will use properties + from the `sortProperties` array prioritized from first to last. + + @property {Array} sortProperties + */ + sortProperties: null, + + /** + Specifies the arrangedContent's sort direction. + Sorts the content in ascending order by default. Set to `false` to + use descending order. + + @property {Boolean} sortAscending + @default true + */ + sortAscending: true, + + /** + The function used to compare two values. You can override this if you + want to do custom comparisons. Functions must be of the type expected by + Array#sort, i.e. + return 0 if the two parameters are equal, + return a negative value if the first parameter is smaller than the second or + return a positive value otherwise: + + ```javascript + function(x,y) { // These are assumed to be integers + if (x === y) + return 0; + return x < y ? -1 : 1; + } + ``` + + @property sortFunction + @type {Function} + @default Ember.compare + */ + sortFunction: compare, + + orderBy: function(item1, item2) { + var result = 0, + sortProperties = get(this, 'sortProperties'), + sortAscending = get(this, 'sortAscending'), + sortFunction = get(this, 'sortFunction'); + + Ember.assert("you need to define `sortProperties`", !!sortProperties); + + forEach(sortProperties, function(propertyName) { + if (result === 0) { + result = sortFunction.call(this, get(item1, propertyName), get(item2, propertyName)); + if ((result !== 0) && !sortAscending) { + result = (-1) * result; + } + } + }, this); + + return result; + }, + + destroy: function() { + var content = get(this, 'content'), + sortProperties = get(this, 'sortProperties'); + + if (content && sortProperties) { + forEach(content, function(item) { + forEach(sortProperties, function(sortProperty) { + removeObserver(item, sortProperty, this, 'contentItemSortPropertyDidChange'); + }, this); + }, this); + } + + return this._super(); + }, + + isSorted: computed.notEmpty('sortProperties'), + + /** + Overrides the default arrangedContent from arrayProxy in order to sort by sortFunction. + Also sets up observers for each sortProperty on each item in the content Array. + + @property arrangedContent + */ + + arrangedContent: computed('content', 'sortProperties.@each', function(key, value) { + var content = get(this, 'content'), + isSorted = get(this, 'isSorted'), + sortProperties = get(this, 'sortProperties'), + self = this; + + if (content && isSorted) { + content = content.slice(); + content.sort(function(item1, item2) { + return self.orderBy(item1, item2); + }); + forEach(content, function(item) { + forEach(sortProperties, function(sortProperty) { + addObserver(item, sortProperty, this, 'contentItemSortPropertyDidChange'); + }, this); + }, this); + return Ember.A(content); + } + + return content; + }), + + _contentWillChange: beforeObserver('content', function() { + var content = get(this, 'content'), + sortProperties = get(this, 'sortProperties'); + + if (content && sortProperties) { + forEach(content, function(item) { + forEach(sortProperties, function(sortProperty) { + removeObserver(item, sortProperty, this, 'contentItemSortPropertyDidChange'); + }, this); + }, this); + } + + this._super(); + }), + + sortPropertiesWillChange: beforeObserver('sortProperties', function() { + this._lastSortAscending = undefined; + }), + + sortPropertiesDidChange: observer('sortProperties', function() { + this._lastSortAscending = undefined; + }), + + sortAscendingWillChange: beforeObserver('sortAscending', function() { + this._lastSortAscending = get(this, 'sortAscending'); + }), + + sortAscendingDidChange: observer('sortAscending', function() { + if (this._lastSortAscending !== undefined && get(this, 'sortAscending') !== this._lastSortAscending) { + var arrangedContent = get(this, 'arrangedContent'); + arrangedContent.reverseObjects(); + } + }), + + contentArrayWillChange: function(array, idx, removedCount, addedCount) { + var isSorted = get(this, 'isSorted'); + + if (isSorted) { + var arrangedContent = get(this, 'arrangedContent'); + var removedObjects = array.slice(idx, idx+removedCount); + var sortProperties = get(this, 'sortProperties'); + + forEach(removedObjects, function(item) { + arrangedContent.removeObject(item); + + forEach(sortProperties, function(sortProperty) { + removeObserver(item, sortProperty, this, 'contentItemSortPropertyDidChange'); + }, this); + }, this); + } + + return this._super(array, idx, removedCount, addedCount); + }, + + contentArrayDidChange: function(array, idx, removedCount, addedCount) { + var isSorted = get(this, 'isSorted'), + sortProperties = get(this, 'sortProperties'); + + if (isSorted) { + var addedObjects = array.slice(idx, idx+addedCount); + + forEach(addedObjects, function(item) { + this.insertItemSorted(item); + + forEach(sortProperties, function(sortProperty) { + addObserver(item, sortProperty, this, 'contentItemSortPropertyDidChange'); + }, this); + }, this); + } + + return this._super(array, idx, removedCount, addedCount); + }, + + insertItemSorted: function(item) { + var arrangedContent = get(this, 'arrangedContent'); + var length = get(arrangedContent, 'length'); + + var idx = this._binarySearch(item, 0, length); + arrangedContent.insertAt(idx, item); + }, + + contentItemSortPropertyDidChange: function(item) { + var arrangedContent = get(this, 'arrangedContent'), + oldIndex = arrangedContent.indexOf(item), + leftItem = arrangedContent.objectAt(oldIndex - 1), + rightItem = arrangedContent.objectAt(oldIndex + 1), + leftResult = leftItem && this.orderBy(item, leftItem), + rightResult = rightItem && this.orderBy(item, rightItem); + + if (leftResult < 0 || rightResult > 0) { + arrangedContent.removeObject(item); + this.insertItemSorted(item); + } + }, + + _binarySearch: function(item, low, high) { + var mid, midItem, res, arrangedContent; + + if (low === high) { + return low; + } + + arrangedContent = get(this, 'arrangedContent'); + + mid = low + Math.floor((high - low) / 2); + midItem = arrangedContent.objectAt(mid); + + res = this.orderBy(midItem, item); + + if (res < 0) { + return this._binarySearch(item, mid+1, high); + } else if (res > 0) { + return this._binarySearch(item, low, mid); + } + + return mid; + } + }); + + __exports__["default"] = SortableMixin; + }); +define("ember-runtime/mixins/target_action_support", + ["ember-metal/core","ember-metal/property_get","ember-metal/property_set","ember-metal/utils","ember-metal/mixin","ember-metal/computed","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __exports__) { + "use strict"; + /** + @module ember + @submodule ember-runtime + */ + var Ember = __dependency1__["default"]; + // Ember.lookup, Ember.assert + + var get = __dependency2__.get; + var set = __dependency3__.set; + var typeOf = __dependency4__.typeOf; + var Mixin = __dependency5__.Mixin; + var computed = __dependency6__.computed; + + /** + `Ember.TargetActionSupport` is a mixin that can be included in a class + to add a `triggerAction` method with semantics similar to the Handlebars + `{{action}}` helper. In normal Ember usage, the `{{action}}` helper is + usually the best choice. This mixin is most often useful when you are + doing more complex event handling in View objects. + + See also `Ember.ViewTargetActionSupport`, which has + view-aware defaults for target and actionContext. + + @class TargetActionSupport + @namespace Ember + @extends Ember.Mixin + */ + var TargetActionSupport = Mixin.create({ + target: null, + action: null, + actionContext: null, + + targetObject: computed(function() { + var target = get(this, 'target'); + + if (typeOf(target) === "string") { + var value = get(this, target); + if (value === undefined) { value = get(Ember.lookup, target); } + return value; + } else { + return target; + } + }).property('target'), + + actionContextObject: computed(function() { + var actionContext = get(this, 'actionContext'); + + if (typeOf(actionContext) === "string") { + var value = get(this, actionContext); + if (value === undefined) { value = get(Ember.lookup, actionContext); } + return value; + } else { + return actionContext; + } + }).property('actionContext'), + + /** + Send an `action` with an `actionContext` to a `target`. The action, actionContext + and target will be retrieved from properties of the object. For example: + + ```javascript + App.SaveButtonView = Ember.View.extend(Ember.TargetActionSupport, { + target: Ember.computed.alias('controller'), + action: 'save', + actionContext: Ember.computed.alias('context'), + click: function() { + this.triggerAction(); // Sends the `save` action, along with the current context + // to the current controller + } + }); + ``` + + The `target`, `action`, and `actionContext` can be provided as properties of + an optional object argument to `triggerAction` as well. + + ```javascript + App.SaveButtonView = Ember.View.extend(Ember.TargetActionSupport, { + click: function() { + this.triggerAction({ + action: 'save', + target: this.get('controller'), + actionContext: this.get('context'), + }); // Sends the `save` action, along with the current context + // to the current controller + } + }); + ``` + + The `actionContext` defaults to the object you are mixing `TargetActionSupport` into. + But `target` and `action` must be specified either as properties or with the argument + to `triggerAction`, or a combination: + + ```javascript + App.SaveButtonView = Ember.View.extend(Ember.TargetActionSupport, { + target: Ember.computed.alias('controller'), + click: function() { + this.triggerAction({ + action: 'save' + }); // Sends the `save` action, along with a reference to `this`, + // to the current controller + } + }); + ``` + + @method triggerAction + @param opts {Hash} (optional, with the optional keys action, target and/or actionContext) + @return {Boolean} true if the action was sent successfully and did not return false + */ + triggerAction: function(opts) { + opts = opts || {}; + var action = opts.action || get(this, 'action'), + target = opts.target || get(this, 'targetObject'), + actionContext = opts.actionContext; + + function args(options, actionName) { + var ret = []; + if (actionName) { ret.push(actionName); } + + return ret.concat(options); + } + + if (typeof actionContext === 'undefined') { + actionContext = get(this, 'actionContextObject') || this; + } + + if (target && action) { + var ret; + + if (target.send) { + ret = target.send.apply(target, args(actionContext, action)); + } else { + Ember.assert("The action '" + action + "' did not exist on " + target, typeof target[action] === 'function'); + ret = target[action].apply(target, args(actionContext)); + } + + if (ret !== false) ret = true; + + return ret; + } else { + return false; + } + } + }); + + __exports__["default"] = TargetActionSupport; + }); +define("ember-runtime/system/application", + ["ember-runtime/system/namespace","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Namespace = __dependency1__["default"]; + + var Application = Namespace.extend(); + __exports__["default"] = Application; + }); +define("ember-runtime/system/array_proxy", + ["ember-metal/core","ember-metal/property_get","ember-metal/property_set","ember-metal/utils","ember-metal/computed","ember-metal/mixin","ember-metal/property_events","ember-metal/error","ember-runtime/system/object","ember-runtime/mixins/mutable_array","ember-runtime/mixins/enumerable","ember-runtime/system/string","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __dependency11__, __dependency12__, __exports__) { + "use strict"; + var Ember = __dependency1__["default"]; + // Ember.K, Ember.assert + var get = __dependency2__.get; + var set = __dependency3__.set; + var isArray = __dependency4__.isArray; + var apply = __dependency4__.apply; + var computed = __dependency5__.computed; + var beforeObserver = __dependency6__.beforeObserver; + var observer = __dependency6__.observer; + var beginPropertyChanges = __dependency7__.beginPropertyChanges; + var endPropertyChanges = __dependency7__.endPropertyChanges; + var EmberError = __dependency8__["default"]; + var EmberObject = __dependency9__["default"];var MutableArray = __dependency10__["default"];var Enumerable = __dependency11__["default"]; + var fmt = __dependency12__.fmt; + + /** + @module ember + @submodule ember-runtime + */ + + var OUT_OF_RANGE_EXCEPTION = "Index out of range"; + var EMPTY = []; + var alias = computed.alias; + var K = Ember.K; + + /** + An ArrayProxy wraps any other object that implements `Ember.Array` and/or + `Ember.MutableArray,` forwarding all requests. This makes it very useful for + a number of binding use cases or other cases where being able to swap + out the underlying array is useful. + + A simple example of usage: + + ```javascript + var pets = ['dog', 'cat', 'fish']; + var ap = Ember.ArrayProxy.create({ content: Ember.A(pets) }); + + ap.get('firstObject'); // 'dog' + ap.set('content', ['amoeba', 'paramecium']); + ap.get('firstObject'); // 'amoeba' + ``` + + This class can also be useful as a layer to transform the contents of + an array, as they are accessed. This can be done by overriding + `objectAtContent`: + + ```javascript + var pets = ['dog', 'cat', 'fish']; + var ap = Ember.ArrayProxy.create({ + content: Ember.A(pets), + objectAtContent: function(idx) { + return this.get('content').objectAt(idx).toUpperCase(); + } + }); + + ap.get('firstObject'); // . 'DOG' + ``` + + @class ArrayProxy + @namespace Ember + @extends Ember.Object + @uses Ember.MutableArray + */ + var ArrayProxy = EmberObject.extend(MutableArray, { + + /** + The content array. Must be an object that implements `Ember.Array` and/or + `Ember.MutableArray.` + + @property content + @type Ember.Array + */ + content: null, + + /** + The array that the proxy pretends to be. In the default `ArrayProxy` + implementation, this and `content` are the same. Subclasses of `ArrayProxy` + can override this property to provide things like sorting and filtering. + + @property arrangedContent + */ + arrangedContent: alias('content'), + + /** + Should actually retrieve the object at the specified index from the + content. You can override this method in subclasses to transform the + content item to something new. + + This method will only be called if content is non-`null`. + + @method objectAtContent + @param {Number} idx The index to retrieve. + @return {Object} the value or undefined if none found + */ + objectAtContent: function(idx) { + return get(this, 'arrangedContent').objectAt(idx); + }, + + /** + Should actually replace the specified objects on the content array. + You can override this method in subclasses to transform the content item + into something new. + + This method will only be called if content is non-`null`. + + @method replaceContent + @param {Number} idx The starting index + @param {Number} amt The number of items to remove from the content. + @param {Array} objects Optional array of objects to insert or null if no + objects. + @return {void} + */ + replaceContent: function(idx, amt, objects) { + get(this, 'content').replace(idx, amt, objects); + }, + + /** + Invoked when the content property is about to change. Notifies observers that the + entire array content will change. + + @private + @method _contentWillChange + */ + _contentWillChange: beforeObserver('content', function() { + this._teardownContent(); + }), + + _teardownContent: function() { + var content = get(this, 'content'); + + if (content) { + content.removeArrayObserver(this, { + willChange: 'contentArrayWillChange', + didChange: 'contentArrayDidChange' + }); + } + }, + + contentArrayWillChange: K, + contentArrayDidChange: K, + + /** + Invoked when the content property changes. Notifies observers that the + entire array content has changed. + + @private + @method _contentDidChange + */ + _contentDidChange: observer('content', function() { + var content = get(this, 'content'); + + Ember.assert("Can't set ArrayProxy's content to itself", content !== this); + + this._setupContent(); + }), + + _setupContent: function() { + var content = get(this, 'content'); + + if (content) { + Ember.assert(fmt('ArrayProxy expects an Array or ' + + 'Ember.ArrayProxy, but you passed %@', [typeof content]), + isArray(content) || content.isDestroyed); + + content.addArrayObserver(this, { + willChange: 'contentArrayWillChange', + didChange: 'contentArrayDidChange' + }); + } + }, + + _arrangedContentWillChange: beforeObserver('arrangedContent', function() { + var arrangedContent = get(this, 'arrangedContent'), + len = arrangedContent ? get(arrangedContent, 'length') : 0; + + this.arrangedContentArrayWillChange(this, 0, len, undefined); + this.arrangedContentWillChange(this); + + this._teardownArrangedContent(arrangedContent); + }), + + _arrangedContentDidChange: observer('arrangedContent', function() { + var arrangedContent = get(this, 'arrangedContent'), + len = arrangedContent ? get(arrangedContent, 'length') : 0; + + Ember.assert("Can't set ArrayProxy's content to itself", arrangedContent !== this); + + this._setupArrangedContent(); + + this.arrangedContentDidChange(this); + this.arrangedContentArrayDidChange(this, 0, undefined, len); + }), + + _setupArrangedContent: function() { + var arrangedContent = get(this, 'arrangedContent'); + + if (arrangedContent) { + Ember.assert(fmt('ArrayProxy expects an Array or ' + + 'Ember.ArrayProxy, but you passed %@', [typeof arrangedContent]), + isArray(arrangedContent) || arrangedContent.isDestroyed); + + arrangedContent.addArrayObserver(this, { + willChange: 'arrangedContentArrayWillChange', + didChange: 'arrangedContentArrayDidChange' + }); + } + }, + + _teardownArrangedContent: function() { + var arrangedContent = get(this, 'arrangedContent'); + + if (arrangedContent) { + arrangedContent.removeArrayObserver(this, { + willChange: 'arrangedContentArrayWillChange', + didChange: 'arrangedContentArrayDidChange' + }); + } + }, + + arrangedContentWillChange: K, + arrangedContentDidChange: K, + + objectAt: function(idx) { + return get(this, 'content') && this.objectAtContent(idx); + }, + + length: computed(function() { + var arrangedContent = get(this, 'arrangedContent'); + return arrangedContent ? get(arrangedContent, 'length') : 0; + // No dependencies since Enumerable notifies length of change + }), + + _replace: function(idx, amt, objects) { + var content = get(this, 'content'); + Ember.assert('The content property of '+ this.constructor + ' should be set before modifying it', content); + if (content) this.replaceContent(idx, amt, objects); + return this; + }, + + replace: function() { + if (get(this, 'arrangedContent') === get(this, 'content')) { + apply(this, this._replace, arguments); + } else { + throw new EmberError("Using replace on an arranged ArrayProxy is not allowed."); + } + }, + + _insertAt: function(idx, object) { + if (idx > get(this, 'content.length')) throw new EmberError(OUT_OF_RANGE_EXCEPTION); + this._replace(idx, 0, [object]); + return this; + }, + + insertAt: function(idx, object) { + if (get(this, 'arrangedContent') === get(this, 'content')) { + return this._insertAt(idx, object); + } else { + throw new EmberError("Using insertAt on an arranged ArrayProxy is not allowed."); + } + }, + + removeAt: function(start, len) { + if ('number' === typeof start) { + var content = get(this, 'content'), + arrangedContent = get(this, 'arrangedContent'), + indices = [], i; + + if ((start < 0) || (start >= get(this, 'length'))) { + throw new EmberError(OUT_OF_RANGE_EXCEPTION); + } + + if (len === undefined) len = 1; + + // Get a list of indices in original content to remove + for (i=start; i= 0) { + var baseValue = this[keyName]; + + if (baseValue) { + if ('function' === typeof baseValue.concat) { + value = baseValue.concat(value); + } else { + value = makeArray(baseValue).concat(value); + } + } else { + value = makeArray(value); + } + } + + if (desc) { + desc.set(this, keyName, value); + } else { + if (typeof this.setUnknownProperty === 'function' && !(keyName in this)) { + this.setUnknownProperty(keyName, value); + } else if (MANDATORY_SETTER) { + defineProperty(this, keyName, null, value); // setup mandatory setter + } else { + this[keyName] = value; + } + } + } + } + } + finishPartial(this, m); + apply(this, this.init, arguments); + m.proto = proto; + finishChains(this); + sendEvent(this, "init"); + }; + + Class.toString = Mixin.prototype.toString; + Class.willReopen = function() { + if (wasApplied) { + Class.PrototypeMixin = Mixin.create(Class.PrototypeMixin); + } + + wasApplied = false; + }; + Class._initMixins = function(args) { initMixins = args; }; + Class._initProperties = function(args) { initProperties = args; }; + + Class.proto = function() { + var superclass = Class.superclass; + if (superclass) { superclass.proto(); } + + if (!wasApplied) { + wasApplied = true; + Class.PrototypeMixin.applyPartial(Class.prototype); + rewatch(Class.prototype); + } + + return this.prototype; + }; + + return Class; + + } + + /** + @class CoreObject + @namespace Ember + */ + var CoreObject = makeCtor(); + CoreObject.toString = function() { return "Ember.CoreObject"; }; + + CoreObject.PrototypeMixin = Mixin.create({ + reopen: function() { + applyMixin(this, arguments, true); + return this; + }, + + /** + An overridable method called when objects are instantiated. By default, + does nothing unless it is overridden during class definition. + + Example: + + ```javascript + App.Person = Ember.Object.extend({ + init: function() { + alert('Name is ' + this.get('name')); + } + }); + + var steve = App.Person.create({ + name: "Steve" + }); + + // alerts 'Name is Steve'. + ``` + + NOTE: If you do override `init` for a framework class like `Ember.View` or + `Ember.ArrayController`, be sure to call `this._super()` in your + `init` declaration! If you don't, Ember may not have an opportunity to + do important setup work, and you'll see strange behavior in your + application. + + @method init + */ + init: function() {}, + + /** + Defines the properties that will be concatenated from the superclass + (instead of overridden). + + By default, when you extend an Ember class a property defined in + the subclass overrides a property with the same name that is defined + in the superclass. However, there are some cases where it is preferable + to build up a property's value by combining the superclass' property + value with the subclass' value. An example of this in use within Ember + is the `classNames` property of `Ember.View`. + + Here is some sample code showing the difference between a concatenated + property and a normal one: + + ```javascript + App.BarView = Ember.View.extend({ + someNonConcatenatedProperty: ['bar'], + classNames: ['bar'] + }); + + App.FooBarView = App.BarView.extend({ + someNonConcatenatedProperty: ['foo'], + classNames: ['foo'], + }); + + var fooBarView = App.FooBarView.create(); + fooBarView.get('someNonConcatenatedProperty'); // ['foo'] + fooBarView.get('classNames'); // ['ember-view', 'bar', 'foo'] + ``` + + This behavior extends to object creation as well. Continuing the + above example: + + ```javascript + var view = App.FooBarView.create({ + someNonConcatenatedProperty: ['baz'], + classNames: ['baz'] + }) + view.get('someNonConcatenatedProperty'); // ['baz'] + view.get('classNames'); // ['ember-view', 'bar', 'foo', 'baz'] + ``` + Adding a single property that is not an array will just add it in the array: + + ```javascript + var view = App.FooBarView.create({ + classNames: 'baz' + }) + view.get('classNames'); // ['ember-view', 'bar', 'foo', 'baz'] + ``` + + Using the `concatenatedProperties` property, we can tell to Ember that mix + the content of the properties. + + In `Ember.View` the `classNameBindings` and `attributeBindings` properties + are also concatenated, in addition to `classNames`. + + This feature is available for you to use throughout the Ember object model, + although typical app developers are likely to use it infrequently. Since + it changes expectations about behavior of properties, you should properly + document its usage in each individual concatenated property (to not + mislead your users to think they can override the property in a subclass). + + @property concatenatedProperties + @type Array + @default null + */ + concatenatedProperties: null, + + /** + Destroyed object property flag. + + if this property is `true` the observers and bindings were already + removed by the effect of calling the `destroy()` method. + + @property isDestroyed + @default false + */ + isDestroyed: false, + + /** + Destruction scheduled flag. The `destroy()` method has been called. + + The object stays intact until the end of the run loop at which point + the `isDestroyed` flag is set. + + @property isDestroying + @default false + */ + isDestroying: false, + + /** + Destroys an object by setting the `isDestroyed` flag and removing its + metadata, which effectively destroys observers and bindings. + + If you try to set a property on a destroyed object, an exception will be + raised. + + Note that destruction is scheduled for the end of the run loop and does not + happen immediately. It will set an isDestroying flag immediately. + + @method destroy + @return {Ember.Object} receiver + */ + destroy: function() { + if (this.isDestroying) { return; } + this.isDestroying = true; + + schedule('actions', this, this.willDestroy); + schedule('destroy', this, this._scheduledDestroy); + return this; + }, + + /** + Override to implement teardown. + + @method willDestroy + */ + willDestroy: K, + + /** + Invoked by the run loop to actually destroy the object. This is + scheduled for execution by the `destroy` method. + + @private + @method _scheduledDestroy + */ + _scheduledDestroy: function() { + if (this.isDestroyed) { return; } + destroy(this); + this.isDestroyed = true; + }, + + bind: function(to, from) { + if (!(from instanceof Binding)) { from = Binding.from(from); } + from.to(to).connect(this); + return from; + }, + + /** + Returns a string representation which attempts to provide more information + than Javascript's `toString` typically does, in a generic way for all Ember + objects. + + ```javascript + App.Person = Em.Object.extend() + person = App.Person.create() + person.toString() //=> "" + ``` + + If the object's class is not defined on an Ember namespace, it will + indicate it is a subclass of the registered superclass: + + ```javascript + Student = App.Person.extend() + student = Student.create() + student.toString() //=> "<(subclass of App.Person):ember1025>" + ``` + + If the method `toStringExtension` is defined, its return value will be + included in the output. + + ```javascript + App.Teacher = App.Person.extend({ + toStringExtension: function() { + return this.get('fullName'); + } + }); + teacher = App.Teacher.create() + teacher.toString(); //=> "" + ``` + + @method toString + @return {String} string representation + */ + toString: function toString() { + var hasToStringExtension = typeof this.toStringExtension === 'function', + extension = hasToStringExtension ? ":" + this.toStringExtension() : ''; + var ret = '<'+this.constructor.toString()+':'+guidFor(this)+extension+'>'; + this.toString = makeToString(ret); + return ret; + } + }); + + CoreObject.PrototypeMixin.ownerConstructor = CoreObject; + + function makeToString(ret) { + return function() { return ret; }; + } + + if (Ember.config.overridePrototypeMixin) { + Ember.config.overridePrototypeMixin(CoreObject.PrototypeMixin); + } + + CoreObject.__super__ = null; + + var ClassMixin = Mixin.create({ + + ClassMixin: required(), + + PrototypeMixin: required(), + + isClass: true, + + isMethod: false, + + /** + Creates a new subclass. + + ```javascript + App.Person = Ember.Object.extend({ + say: function(thing) { + alert(thing); + } + }); + ``` + + This defines a new subclass of Ember.Object: `App.Person`. It contains one method: `say()`. + + You can also create a subclass from any existing class by calling its `extend()` method. For example, you might want to create a subclass of Ember's built-in `Ember.View` class: + + ```javascript + App.PersonView = Ember.View.extend({ + tagName: 'li', + classNameBindings: ['isAdministrator'] + }); + ``` + + When defining a subclass, you can override methods but still access the implementation of your parent class by calling the special `_super()` method: + + ```javascript + App.Person = Ember.Object.extend({ + say: function(thing) { + var name = this.get('name'); + alert(name + ' says: ' + thing); + } + }); + + App.Soldier = App.Person.extend({ + say: function(thing) { + this._super(thing + ", sir!"); + }, + march: function(numberOfHours) { + alert(this.get('name') + ' marches for ' + numberOfHours + ' hours.') + } + }); + + var yehuda = App.Soldier.create({ + name: "Yehuda Katz" + }); + + yehuda.say("Yes"); // alerts "Yehuda Katz says: Yes, sir!" + ``` + + The `create()` on line #17 creates an *instance* of the `App.Soldier` class. The `extend()` on line #8 creates a *subclass* of `App.Person`. Any instance of the `App.Person` class will *not* have the `march()` method. + + You can also pass `Mixin` classes to add additional properties to the subclass. + + ```javascript + App.Person = Ember.Object.extend({ + say: function(thing) { + alert(this.get('name') + ' says: ' + thing); + } + }); + + App.SingingMixin = Mixin.create({ + sing: function(thing){ + alert(this.get('name') + ' sings: la la la ' + thing); + } + }); + + App.BroadwayStar = App.Person.extend(App.SingingMixin, { + dance: function() { + alert(this.get('name') + ' dances: tap tap tap tap '); + } + }); + ``` + + The `App.BroadwayStar` class contains three methods: `say()`, `sing()`, and `dance()`. + + @method extend + @static + + @param {Mixin} [mixins]* One or more Mixin classes + @param {Object} [arguments]* Object containing values to use within the new class + */ + extend: function() { + var Class = makeCtor(), proto; + Class.ClassMixin = Mixin.create(this.ClassMixin); + Class.PrototypeMixin = Mixin.create(this.PrototypeMixin); + + Class.ClassMixin.ownerConstructor = Class; + Class.PrototypeMixin.ownerConstructor = Class; + + reopen.apply(Class.PrototypeMixin, arguments); + + Class.superclass = this; + Class.__super__ = this.prototype; + + proto = Class.prototype = o_create(this.prototype); + proto.constructor = Class; + generateGuid(proto); + meta(proto).proto = proto; // this will disable observers on prototype + + Class.ClassMixin.apply(Class); + return Class; + }, + + /** + Equivalent to doing `extend(arguments).create()`. + If possible use the normal `create` method instead. + + @method createWithMixins + @static + @param [arguments]* + */ + createWithMixins: function() { + var C = this; + if (arguments.length>0) { this._initMixins(arguments); } + return new C(); + }, + + /** + Creates an instance of a class. Accepts either no arguments, or an object + containing values to initialize the newly instantiated object with. + + ```javascript + App.Person = Ember.Object.extend({ + helloWorld: function() { + alert("Hi, my name is " + this.get('name')); + } + }); + + var tom = App.Person.create({ + name: 'Tom Dale' + }); + + tom.helloWorld(); // alerts "Hi, my name is Tom Dale". + ``` + + `create` will call the `init` function if defined during + `Ember.AnyObject.extend` + + If no arguments are passed to `create`, it will not set values to the new + instance during initialization: + + ```javascript + var noName = App.Person.create(); + noName.helloWorld(); // alerts undefined + ``` + + NOTE: For performance reasons, you cannot declare methods or computed + properties during `create`. You should instead declare methods and computed + properties when using `extend` or use the `createWithMixins` shorthand. + + @method create + @static + @param [arguments]* + */ + create: function() { + var C = this; + if (arguments.length>0) { this._initProperties(arguments); } + return new C(); + }, + + /** + Augments a constructor's prototype with additional + properties and functions: + + ```javascript + MyObject = Ember.Object.extend({ + name: 'an object' + }); + + o = MyObject.create(); + o.get('name'); // 'an object' + + MyObject.reopen({ + say: function(msg){ + console.log(msg); + } + }) + + o2 = MyObject.create(); + o2.say("hello"); // logs "hello" + + o.say("goodbye"); // logs "goodbye" + ``` + + To add functions and properties to the constructor itself, + see `reopenClass` + + @method reopen + */ + reopen: function() { + this.willReopen(); + apply(this.PrototypeMixin, reopen, arguments); + return this; + }, + + /** + Augments a constructor's own properties and functions: + + ```javascript + MyObject = Ember.Object.extend({ + name: 'an object' + }); + + MyObject.reopenClass({ + canBuild: false + }); + + MyObject.canBuild; // false + o = MyObject.create(); + ``` + + In other words, this creates static properties and functions for the class. These are only available on the class + and not on any instance of that class. + + ```javascript + App.Person = Ember.Object.extend({ + name : "", + sayHello : function(){ + alert("Hello. My name is " + this.get('name')); + } + }); + + App.Person.reopenClass({ + species : "Homo sapiens", + createPerson: function(newPersonsName){ + return App.Person.create({ + name:newPersonsName + }); + } + }); + + var tom = App.Person.create({ + name : "Tom Dale" + }); + var yehuda = App.Person.createPerson("Yehuda Katz"); + + tom.sayHello(); // "Hello. My name is Tom Dale" + yehuda.sayHello(); // "Hello. My name is Yehuda Katz" + alert(App.Person.species); // "Homo sapiens" + ``` + + Note that `species` and `createPerson` are *not* valid on the `tom` and `yehuda` + variables. They are only valid on `App.Person`. + + To add functions and properties to instances of + a constructor by extending the constructor's prototype + see `reopen` + + @method reopenClass + */ + reopenClass: function() { + apply(this.ClassMixin, reopen, arguments); + applyMixin(this, arguments, false); + return this; + }, + + detect: function(obj) { + if ('function' !== typeof obj) { return false; } + while(obj) { + if (obj===this) { return true; } + obj = obj.superclass; + } + return false; + }, + + detectInstance: function(obj) { + return obj instanceof this; + }, + + /** + In some cases, you may want to annotate computed properties with additional + metadata about how they function or what values they operate on. For + example, computed property functions may close over variables that are then + no longer available for introspection. + + You can pass a hash of these values to a computed property like this: + + ```javascript + person: function() { + var personId = this.get('personId'); + return App.Person.create({ id: personId }); + }.property().meta({ type: App.Person }) + ``` + + Once you've done this, you can retrieve the values saved to the computed + property from your class like this: + + ```javascript + MyClass.metaForProperty('person'); + ``` + + This will return the original hash that was passed to `meta()`. + + @method metaForProperty + @param key {String} property name + */ + metaForProperty: function(key) { + var meta = this.proto()[META_KEY], + desc = meta && meta.descs[key]; + + Ember.assert("metaForProperty() could not find a computed property with key '"+key+"'.", !!desc && desc instanceof ComputedProperty); + return desc._meta || {}; + }, + + /** + Iterate over each computed property for the class, passing its name + and any associated metadata (see `metaForProperty`) to the callback. + + @method eachComputedProperty + @param {Function} callback + @param {Object} binding + */ + eachComputedProperty: function(callback, binding) { + var proto = this.proto(), + descs = meta(proto).descs, + empty = {}, + property; + + for (var name in descs) { + property = descs[name]; + + if (property instanceof ComputedProperty) { + callback.call(binding || this, name, property._meta || empty); + } + } + } + + }); + + ClassMixin.ownerConstructor = CoreObject; + + if (Ember.config.overrideClassMixin) { + Ember.config.overrideClassMixin(ClassMixin); + } + + CoreObject.ClassMixin = ClassMixin; + ClassMixin.apply(CoreObject); + + __exports__["default"] = CoreObject; + }); +define("ember-runtime/system/deferred", + ["ember-runtime/mixins/deferred","ember-metal/property_get","ember-runtime/system/object","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __exports__) { + "use strict"; + var DeferredMixin = __dependency1__["default"]; + var get = __dependency2__.get; + var EmberObject = __dependency3__["default"]; + + var Deferred = EmberObject.extend(DeferredMixin); + + Deferred.reopenClass({ + promise: function(callback, binding) { + var deferred = Deferred.create(); + callback.call(binding, deferred); + return deferred; + } + }); + + __exports__["default"] = Deferred; + }); +define("ember-runtime/system/each_proxy", + ["ember-metal/core","ember-metal/property_get","ember-metal/property_set","ember-metal/utils","ember-metal/enumerable_utils","ember-metal/array","ember-runtime/mixins/array","ember-runtime/system/object","ember-metal/computed","ember-metal/observer","ember-metal/events","ember-metal/properties","ember-metal/property_events","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __dependency11__, __dependency12__, __dependency13__, __exports__) { + "use strict"; + /** + @module ember + @submodule ember-runtime + */ + + var Ember = __dependency1__["default"]; + // Ember.assert + + var get = __dependency2__.get; + var set = __dependency3__.set; + var guidFor = __dependency4__.guidFor; + var EnumerableUtils = __dependency5__["default"]; + var indexOf = __dependency6__.indexOf; + var EmberArray = __dependency7__["default"]; + // ES6TODO: WAT? Circular dep? + var EmberObject = __dependency8__["default"]; + var computed = __dependency9__.computed; + var addObserver = __dependency10__.addObserver; + var addBeforeObserver = __dependency10__.addBeforeObserver; + var removeBeforeObserver = __dependency10__.removeBeforeObserver; + var removeObserver = __dependency10__.removeObserver; + var typeOf = __dependency4__.typeOf; + var watchedEvents = __dependency11__.watchedEvents; + var defineProperty = __dependency12__.defineProperty; + var beginPropertyChanges = __dependency13__.beginPropertyChanges; + var propertyDidChange = __dependency13__.propertyDidChange; + var propertyWillChange = __dependency13__.propertyWillChange; + var endPropertyChanges = __dependency13__.endPropertyChanges; + var changeProperties = __dependency13__.changeProperties; + + var forEach = EnumerableUtils.forEach; + + var EachArray = EmberObject.extend(EmberArray, { + + init: function(content, keyName, owner) { + this._super(); + this._keyName = keyName; + this._owner = owner; + this._content = content; + }, + + objectAt: function(idx) { + var item = this._content.objectAt(idx); + return item && get(item, this._keyName); + }, + + length: computed(function() { + var content = this._content; + return content ? get(content, 'length') : 0; + }) + + }); + + var IS_OBSERVER = /^.+:(before|change)$/; + + function addObserverForContentKey(content, keyName, proxy, idx, loc) { + var objects = proxy._objects, guid; + if (!objects) objects = proxy._objects = {}; + + while(--loc>=idx) { + var item = content.objectAt(loc); + if (item) { + Ember.assert('When using @each to observe the array ' + content + ', the array must return an object', typeOf(item) === 'instance' || typeOf(item) === 'object'); + addBeforeObserver(item, keyName, proxy, 'contentKeyWillChange'); + addObserver(item, keyName, proxy, 'contentKeyDidChange'); + + // keep track of the index each item was found at so we can map + // it back when the obj changes. + guid = guidFor(item); + if (!objects[guid]) objects[guid] = []; + objects[guid].push(loc); + } + } + } + + function removeObserverForContentKey(content, keyName, proxy, idx, loc) { + var objects = proxy._objects; + if (!objects) objects = proxy._objects = {}; + var indicies, guid; + + while(--loc>=idx) { + var item = content.objectAt(loc); + if (item) { + removeBeforeObserver(item, keyName, proxy, 'contentKeyWillChange'); + removeObserver(item, keyName, proxy, 'contentKeyDidChange'); + + guid = guidFor(item); + indicies = objects[guid]; + indicies[indexOf.call(indicies, loc)] = null; + } + } + } + + /** + This is the object instance returned when you get the `@each` property on an + array. It uses the unknownProperty handler to automatically create + EachArray instances for property names. + + @private + @class EachProxy + @namespace Ember + @extends Ember.Object + */ + var EachProxy = EmberObject.extend({ + + init: function(content) { + this._super(); + this._content = content; + content.addArrayObserver(this); + + // in case someone is already observing some keys make sure they are + // added + forEach(watchedEvents(this), function(eventName) { + this.didAddListener(eventName); + }, this); + }, + + /** + You can directly access mapped properties by simply requesting them. + The `unknownProperty` handler will generate an EachArray of each item. + + @method unknownProperty + @param keyName {String} + @param value {*} + */ + unknownProperty: function(keyName, value) { + var ret; + ret = new EachArray(this._content, keyName, this); + defineProperty(this, keyName, null, ret); + this.beginObservingContentKey(keyName); + return ret; + }, + + // .......................................................... + // ARRAY CHANGES + // Invokes whenever the content array itself changes. + + arrayWillChange: function(content, idx, removedCnt, addedCnt) { + var keys = this._keys, key, lim; + + lim = removedCnt>0 ? idx+removedCnt : -1; + beginPropertyChanges(this); + + for(key in keys) { + if (!keys.hasOwnProperty(key)) { continue; } + + if (lim>0) { removeObserverForContentKey(content, key, this, idx, lim); } + + propertyWillChange(this, key); + } + + propertyWillChange(this._content, '@each'); + endPropertyChanges(this); + }, + + arrayDidChange: function(content, idx, removedCnt, addedCnt) { + var keys = this._keys, lim; + + lim = addedCnt>0 ? idx+addedCnt : -1; + changeProperties(function() { + for(var key in keys) { + if (!keys.hasOwnProperty(key)) { continue; } + + if (lim>0) { addObserverForContentKey(content, key, this, idx, lim); } + + propertyDidChange(this, key); + } + + propertyDidChange(this._content, '@each'); + }, this); + }, + + // .......................................................... + // LISTEN FOR NEW OBSERVERS AND OTHER EVENT LISTENERS + // Start monitoring keys based on who is listening... + + didAddListener: function(eventName) { + if (IS_OBSERVER.test(eventName)) { + this.beginObservingContentKey(eventName.slice(0, -7)); + } + }, + + didRemoveListener: function(eventName) { + if (IS_OBSERVER.test(eventName)) { + this.stopObservingContentKey(eventName.slice(0, -7)); + } + }, + + // .......................................................... + // CONTENT KEY OBSERVING + // Actual watch keys on the source content. + + beginObservingContentKey: function(keyName) { + var keys = this._keys; + if (!keys) keys = this._keys = {}; + if (!keys[keyName]) { + keys[keyName] = 1; + var content = this._content, + len = get(content, 'length'); + addObserverForContentKey(content, keyName, this, 0, len); + } else { + keys[keyName]++; + } + }, + + stopObservingContentKey: function(keyName) { + var keys = this._keys; + if (keys && (keys[keyName]>0) && (--keys[keyName]<=0)) { + var content = this._content, + len = get(content, 'length'); + removeObserverForContentKey(content, keyName, this, 0, len); + } + }, + + contentKeyWillChange: function(obj, keyName) { + propertyWillChange(this, keyName); + }, + + contentKeyDidChange: function(obj, keyName) { + propertyDidChange(this, keyName); + } + + }); + + __exports__.EachArray = EachArray; + __exports__.EachProxy = EachProxy; + }); +define("ember-runtime/system/lazy_load", + ["ember-metal/core","ember-metal/array","ember-runtime/system/native_array","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __exports__) { + "use strict"; + /*globals CustomEvent */ + + var Ember = __dependency1__["default"]; + // Ember.ENV.EMBER_LOAD_HOOKS + var forEach = __dependency2__.forEach; + // make sure Ember.A is setup. + + /** + @module ember + @submodule ember-runtime + */ + + var loadHooks = Ember.ENV.EMBER_LOAD_HOOKS || {}; + var loaded = {}; + + /** + Detects when a specific package of Ember (e.g. 'Ember.Handlebars') + has fully loaded and is available for extension. + + The provided `callback` will be called with the `name` passed + resolved from a string into the object: + + ``` javascript + Ember.onLoad('Ember.Handlebars' function(hbars) { + hbars.registerHelper(...); + }); + ``` + + @method onLoad + @for Ember + @param name {String} name of hook + @param callback {Function} callback to be called + */ + function onLoad(name, callback) { + var object; + + loadHooks[name] = loadHooks[name] || Ember.A(); + loadHooks[name].pushObject(callback); + + if (object = loaded[name]) { + callback(object); + } + }; + + /** + Called when an Ember.js package (e.g Ember.Handlebars) has finished + loading. Triggers any callbacks registered for this event. + + @method runLoadHooks + @for Ember + @param name {String} name of hook + @param object {Object} object to pass to callbacks + */ + function runLoadHooks(name, object) { + loaded[name] = object; + + if (typeof window === 'object' && typeof window.dispatchEvent === 'function' && typeof CustomEvent === "function") { + var event = new CustomEvent(name, {detail: object, name: name}); + window.dispatchEvent(event); + } + + if (loadHooks[name]) { + forEach.call(loadHooks[name], function(callback) { + callback(object); + }); + } + }; + + __exports__.onLoad = onLoad; + __exports__.runLoadHooks = runLoadHooks; + }); +define("ember-runtime/system/namespace", + ["ember-metal/core","ember-metal/property_get","ember-metal/array","ember-metal/utils","ember-metal/mixin","ember-runtime/system/object","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __exports__) { + "use strict"; + /** + @module ember + @submodule ember-runtime + */ + + // Ember.lookup, Ember.BOOTED, Ember.deprecate, Ember.NAME_KEY, Ember.anyUnprocessedMixins + var Ember = __dependency1__["default"]; + var get = __dependency2__.get; + var indexOf = __dependency3__.indexOf; + var GUID_KEY = __dependency4__.GUID_KEY; + var guidFor = __dependency4__.guidFor; + var Mixin = __dependency5__.Mixin; + + var EmberObject = __dependency6__["default"]; + + /** + A Namespace is an object usually used to contain other objects or methods + such as an application or framework. Create a namespace anytime you want + to define one of these new containers. + + # Example Usage + + ```javascript + MyFramework = Ember.Namespace.create({ + VERSION: '1.0.0' + }); + ``` + + @class Namespace + @namespace Ember + @extends Ember.Object + */ + var Namespace = EmberObject.extend({ + isNamespace: true, + + init: function() { + Namespace.NAMESPACES.push(this); + Namespace.PROCESSED = false; + }, + + toString: function() { + var name = get(this, 'name'); + if (name) { return name; } + + findNamespaces(); + return this[NAME_KEY]; + }, + + nameClasses: function() { + processNamespace([this.toString()], this, {}); + }, + + destroy: function() { + var namespaces = Namespace.NAMESPACES, + toString = this.toString(); + + if (toString) { + Ember.lookup[toString] = undefined; + delete Namespace.NAMESPACES_BY_ID[toString]; + } + namespaces.splice(indexOf.call(namespaces, this), 1); + this._super(); + } + }); + + Namespace.reopenClass({ + NAMESPACES: [Ember], + NAMESPACES_BY_ID: {}, + PROCESSED: false, + processAll: processAllNamespaces, + byName: function(name) { + if (!Ember.BOOTED) { + processAllNamespaces(); + } + + return NAMESPACES_BY_ID[name]; + } + }); + + var NAMESPACES_BY_ID = Namespace.NAMESPACES_BY_ID; + + var hasOwnProp = ({}).hasOwnProperty; + + function processNamespace(paths, root, seen) { + var idx = paths.length; + + NAMESPACES_BY_ID[paths.join('.')] = root; + + // Loop over all of the keys in the namespace, looking for classes + for(var key in root) { + if (!hasOwnProp.call(root, key)) { continue; } + var obj = root[key]; + + // If we are processing the `Ember` namespace, for example, the + // `paths` will start with `["Ember"]`. Every iteration through + // the loop will update the **second** element of this list with + // the key, so processing `Ember.View` will make the Array + // `['Ember', 'View']`. + paths[idx] = key; + + // If we have found an unprocessed class + if (obj && obj.toString === classToString) { + // Replace the class' `toString` with the dot-separated path + // and set its `NAME_KEY` + obj.toString = makeToString(paths.join('.')); + obj[NAME_KEY] = paths.join('.'); + + // Support nested namespaces + } else if (obj && obj.isNamespace) { + // Skip aliased namespaces + if (seen[guidFor(obj)]) { continue; } + seen[guidFor(obj)] = true; + + // Process the child namespace + processNamespace(paths, obj, seen); + } + } + + paths.length = idx; // cut out last item + } + + var STARTS_WITH_UPPERCASE = /^[A-Z]/; + + function findNamespaces() { + var lookup = Ember.lookup, obj, isNamespace; + + if (Namespace.PROCESSED) { return; } + + for (var prop in lookup) { + // Only process entities that start with uppercase A-Z + if (!STARTS_WITH_UPPERCASE.test(prop)) { continue; } + + // Unfortunately, some versions of IE don't support window.hasOwnProperty + if (lookup.hasOwnProperty && !lookup.hasOwnProperty(prop)) { continue; } + + // At times we are not allowed to access certain properties for security reasons. + // There are also times where even if we can access them, we are not allowed to access their properties. + try { + obj = lookup[prop]; + isNamespace = obj && obj.isNamespace; + } catch (e) { + continue; + } + + if (isNamespace) { + obj[NAME_KEY] = prop; + } + } + } + + var NAME_KEY = Ember.NAME_KEY = GUID_KEY + '_name'; + + function superClassString(mixin) { + var superclass = mixin.superclass; + if (superclass) { + if (superclass[NAME_KEY]) { return superclass[NAME_KEY]; } + else { return superClassString(superclass); } + } else { + return; + } + } + + function classToString() { + if (!Ember.BOOTED && !this[NAME_KEY]) { + processAllNamespaces(); + } + + var ret; + + if (this[NAME_KEY]) { + ret = this[NAME_KEY]; + } else if (this._toString) { + ret = this._toString; + } else { + var str = superClassString(this); + if (str) { + ret = "(subclass of " + str + ")"; + } else { + ret = "(unknown mixin)"; + } + this.toString = makeToString(ret); + } + + return ret; + } + + function processAllNamespaces() { + var unprocessedNamespaces = !Namespace.PROCESSED, + unprocessedMixins = Ember.anyUnprocessedMixins; + + if (unprocessedNamespaces) { + findNamespaces(); + Namespace.PROCESSED = true; + } + + if (unprocessedNamespaces || unprocessedMixins) { + var namespaces = Namespace.NAMESPACES, namespace; + for (var i=0, l=namespaces.length; i=0;idx--) { + if (this[idx] === object) return idx ; + } + return -1; + }, + + copy: function(deep) { + if (deep) { + return this.map(function(item) { return copy(item, true); }); + } + + return this.slice(); + } + }); + + // Remove any methods implemented natively so we don't override them + var ignore = ['length']; + forEach(NativeArray.keys(), function(methodName) { + if (Array.prototype[methodName]) ignore.push(methodName); + }); + + if (ignore.length>0) { + NativeArray = NativeArray.without.apply(NativeArray, ignore); + } + + /** + Creates an `Ember.NativeArray` from an Array like object. + Does not modify the original object. Ember.A is not needed if + `Ember.EXTEND_PROTOTYPES` is `true` (the default value). However, + it is recommended that you use Ember.A when creating addons for + ember or when you can not guarantee that `Ember.EXTEND_PROTOTYPES` + will be `true`. + + Example + + ```js + var Pagination = Ember.CollectionView.extend({ + tagName: 'ul', + classNames: ['pagination'], + + init: function() { + this._super(); + if (!this.get('content')) { + this.set('content', Ember.A()); + } + } + }); + ``` + + @method A + @for Ember + @return {Ember.NativeArray} + */ + var A = function(arr) { + if (arr === undefined) { arr = []; } + return EmberArray.detect(arr) ? arr : NativeArray.apply(arr); + }; + + /** + Activates the mixin on the Array.prototype if not already applied. Calling + this method more than once is safe. This will be called when ember is loaded + unless you have `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Array` + set to `false`. + + Example + + ```js + if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Array) { + Ember.NativeArray.activate(); + } + ``` + + @method activate + @for Ember.NativeArray + @static + @return {void} + */ + NativeArray.activate = function() { + NativeArray.apply(Array.prototype); + + A = function(arr) { return arr || []; }; + }; + + if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Array) { + NativeArray.activate(); + } + + Ember.A = A; // ES6TODO: Setting A onto the object returned by ember-metal/core to avoid circles + __exports__.A = A; + __exports__.NativeArray = NativeArray;__exports__["default"] = NativeArray; + }); +define("ember-runtime/system/object", + ["ember-runtime/system/core_object","ember-runtime/mixins/observable","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + /** + @module ember + @submodule ember-runtime + */ + + var CoreObject = __dependency1__["default"]; + var Observable = __dependency2__["default"]; + + /** + `Ember.Object` is the main base class for all Ember objects. It is a subclass + of `Ember.CoreObject` with the `Ember.Observable` mixin applied. For details, + see the documentation for each of these. + + @class Object + @namespace Ember + @extends Ember.CoreObject + @uses Ember.Observable + */ + var EmberObject = CoreObject.extend(Observable); + EmberObject.toString = function() { return "Ember.Object"; }; + + __exports__["default"] = EmberObject; + }); +define("ember-runtime/system/object_proxy", + ["ember-metal/core","ember-metal/property_get","ember-metal/property_set","ember-metal/utils","ember-metal/observer","ember-metal/property_events","ember-metal/computed","ember-metal/properties","ember-metal/mixin","ember-runtime/system/string","ember-runtime/system/object","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __dependency11__, __exports__) { + "use strict"; + /** + @module ember + @submodule ember-runtime + */ + var Ember = __dependency1__["default"]; + // Ember.assert + var get = __dependency2__.get; + var set = __dependency3__.set; + var meta = __dependency4__.meta; + var addObserver = __dependency5__.addObserver; + var removeObserver = __dependency5__.removeObserver; + var addBeforeObserver = __dependency5__.addBeforeObserver; + var removeBeforeObserver = __dependency5__.removeBeforeObserver; + var propertyWillChange = __dependency6__.propertyWillChange; + var propertyDidChange = __dependency6__.propertyDidChange; + var computed = __dependency7__.computed; + var defineProperty = __dependency8__.defineProperty; + var observer = __dependency9__.observer; + var fmt = __dependency10__.fmt; + var EmberObject = __dependency11__["default"]; + + function contentPropertyWillChange(content, contentKey) { + var key = contentKey.slice(8); // remove "content." + if (key in this) { return; } // if shadowed in proxy + propertyWillChange(this, key); + } + + function contentPropertyDidChange(content, contentKey) { + var key = contentKey.slice(8); // remove "content." + if (key in this) { return; } // if shadowed in proxy + propertyDidChange(this, key); + } + + /** + `Ember.ObjectProxy` forwards all properties not defined by the proxy itself + to a proxied `content` object. + + ```javascript + object = Ember.Object.create({ + name: 'Foo' + }); + + proxy = Ember.ObjectProxy.create({ + content: object + }); + + // Access and change existing properties + proxy.get('name') // 'Foo' + proxy.set('name', 'Bar'); + object.get('name') // 'Bar' + + // Create new 'description' property on `object` + proxy.set('description', 'Foo is a whizboo baz'); + object.get('description') // 'Foo is a whizboo baz' + ``` + + While `content` is unset, setting a property to be delegated will throw an + Error. + + ```javascript + proxy = Ember.ObjectProxy.create({ + content: null, + flag: null + }); + proxy.set('flag', true); + proxy.get('flag'); // true + proxy.get('foo'); // undefined + proxy.set('foo', 'data'); // throws Error + ``` + + Delegated properties can be bound to and will change when content is updated. + + Computed properties on the proxy itself can depend on delegated properties. + + ```javascript + ProxyWithComputedProperty = Ember.ObjectProxy.extend({ + fullName: function () { + var firstName = this.get('firstName'), + lastName = this.get('lastName'); + if (firstName && lastName) { + return firstName + ' ' + lastName; + } + return firstName || lastName; + }.property('firstName', 'lastName') + }); + + proxy = ProxyWithComputedProperty.create(); + + proxy.get('fullName'); // undefined + proxy.set('content', { + firstName: 'Tom', lastName: 'Dale' + }); // triggers property change for fullName on proxy + + proxy.get('fullName'); // 'Tom Dale' + ``` + + @class ObjectProxy + @namespace Ember + @extends Ember.Object + */ + var ObjectProxy = EmberObject.extend({ + /** + The object whose properties will be forwarded. + + @property content + @type Ember.Object + @default null + */ + content: null, + _contentDidChange: observer('content', function() { + Ember.assert("Can't set ObjectProxy's content to itself", get(this, 'content') !== this); + }), + + isTruthy: computed.bool('content'), + + _debugContainerKey: null, + + willWatchProperty: function (key) { + var contentKey = 'content.' + key; + addBeforeObserver(this, contentKey, null, contentPropertyWillChange); + addObserver(this, contentKey, null, contentPropertyDidChange); + }, + + didUnwatchProperty: function (key) { + var contentKey = 'content.' + key; + removeBeforeObserver(this, contentKey, null, contentPropertyWillChange); + removeObserver(this, contentKey, null, contentPropertyDidChange); + }, + + unknownProperty: function (key) { + var content = get(this, 'content'); + if (content) { + return get(content, key); + } + }, + + setUnknownProperty: function (key, value) { + var m = meta(this); + if (m.proto === this) { + // if marked as prototype then just defineProperty + // rather than delegate + defineProperty(this, key, null, value); + return value; + } + + var content = get(this, 'content'); + Ember.assert(fmt("Cannot delegate set('%@', %@) to the 'content' property of object proxy %@: its 'content' is undefined.", [key, value, this]), content); + return set(content, key, value); + } + + }); + + __exports__["default"] = ObjectProxy; + }); +define("ember-runtime/system/set", + ["ember-metal/core","ember-metal/property_get","ember-metal/property_set","ember-metal/utils","ember-metal/is_none","ember-runtime/system/string","ember-runtime/system/core_object","ember-runtime/mixins/mutable_enumerable","ember-runtime/mixins/enumerable","ember-runtime/mixins/copyable","ember-runtime/mixins/freezable","ember-metal/error","ember-metal/property_events","ember-metal/mixin","ember-metal/computed","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __dependency11__, __dependency12__, __dependency13__, __dependency14__, __dependency15__, __exports__) { + "use strict"; + /** + @module ember + @submodule ember-runtime + */ + var Ember = __dependency1__["default"]; + // Ember.isNone + + var get = __dependency2__.get; + var set = __dependency3__.set; + var guidFor = __dependency4__.guidFor; + var isNone = __dependency5__.isNone; + var fmt = __dependency6__.fmt; + var CoreObject = __dependency7__["default"]; + var MutableEnumerable = __dependency8__["default"]; + var Enumerable = __dependency9__["default"]; + var Copyable = __dependency10__["default"]; + var Freezable = __dependency11__.Freezable; + var FROZEN_ERROR = __dependency11__.FROZEN_ERROR; + var EmberError = __dependency12__["default"]; + var propertyWillChange = __dependency13__.propertyWillChange; + var propertyDidChange = __dependency13__.propertyDidChange; + var aliasMethod = __dependency14__.aliasMethod; + var computed = __dependency15__.computed; + + /** + An unordered collection of objects. + + A Set works a bit like an array except that its items are not ordered. You + can create a set to efficiently test for membership for an object. You can + also iterate through a set just like an array, even accessing objects by + index, however there is no guarantee as to their order. + + All Sets are observable via the Enumerable Observer API - which works + on any enumerable object including both Sets and Arrays. + + ## Creating a Set + + You can create a set like you would most objects using + `new Ember.Set()`. Most new sets you create will be empty, but you can + also initialize the set with some content by passing an array or other + enumerable of objects to the constructor. + + Finally, you can pass in an existing set and the set will be copied. You + can also create a copy of a set by calling `Ember.Set#copy()`. + + ```javascript + // creates a new empty set + var foundNames = new Ember.Set(); + + // creates a set with four names in it. + var names = new Ember.Set(["Charles", "Tom", "Juan", "Alex"]); // :P + + // creates a copy of the names set. + var namesCopy = new Ember.Set(names); + + // same as above. + var anotherNamesCopy = names.copy(); + ``` + + ## Adding/Removing Objects + + You generally add or remove objects from a set using `add()` or + `remove()`. You can add any type of object including primitives such as + numbers, strings, and booleans. + + Unlike arrays, objects can only exist one time in a set. If you call `add()` + on a set with the same object multiple times, the object will only be added + once. Likewise, calling `remove()` with the same object multiple times will + remove the object the first time and have no effect on future calls until + you add the object to the set again. + + NOTE: You cannot add/remove `null` or `undefined` to a set. Any attempt to do + so will be ignored. + + In addition to add/remove you can also call `push()`/`pop()`. Push behaves + just like `add()` but `pop()`, unlike `remove()` will pick an arbitrary + object, remove it and return it. This is a good way to use a set as a job + queue when you don't care which order the jobs are executed in. + + ## Testing for an Object + + To test for an object's presence in a set you simply call + `Ember.Set#contains()`. + + ## Observing changes + + When using `Ember.Set`, you can observe the `"[]"` property to be + alerted whenever the content changes. You can also add an enumerable + observer to the set to be notified of specific objects that are added and + removed from the set. See [Ember.Enumerable](/api/classes/Ember.Enumerable.html) + for more information on enumerables. + + This is often unhelpful. If you are filtering sets of objects, for instance, + it is very inefficient to re-filter all of the items each time the set + changes. It would be better if you could just adjust the filtered set based + on what was changed on the original set. The same issue applies to merging + sets, as well. + + ## Other Methods + + `Ember.Set` primary implements other mixin APIs. For a complete reference + on the methods you will use with `Ember.Set`, please consult these mixins. + The most useful ones will be `Ember.Enumerable` and + `Ember.MutableEnumerable` which implement most of the common iterator + methods you are used to on Array. + + Note that you can also use the `Ember.Copyable` and `Ember.Freezable` + APIs on `Ember.Set` as well. Once a set is frozen it can no longer be + modified. The benefit of this is that when you call `frozenCopy()` on it, + Ember will avoid making copies of the set. This allows you to write + code that can know with certainty when the underlying set data will or + will not be modified. + + @class Set + @namespace Ember + @extends Ember.CoreObject + @uses Ember.MutableEnumerable + @uses Ember.Copyable + @uses Ember.Freezable + @since Ember 0.9 + */ + var Set = CoreObject.extend(MutableEnumerable, Copyable, Freezable, + { + + // .......................................................... + // IMPLEMENT ENUMERABLE APIS + // + + /** + This property will change as the number of objects in the set changes. + + @property length + @type number + @default 0 + */ + length: 0, + + /** + Clears the set. This is useful if you want to reuse an existing set + without having to recreate it. + + ```javascript + var colors = new Ember.Set(["red", "green", "blue"]); + colors.length; // 3 + colors.clear(); + colors.length; // 0 + ``` + + @method clear + @return {Ember.Set} An empty Set + */ + clear: function() { + if (this.isFrozen) { throw new EmberError(FROZEN_ERROR); } + + var len = get(this, 'length'); + if (len === 0) { return this; } + + var guid; + + this.enumerableContentWillChange(len, 0); + propertyWillChange(this, 'firstObject'); + propertyWillChange(this, 'lastObject'); + + for (var i=0; i < len; i++) { + guid = guidFor(this[i]); + delete this[guid]; + delete this[i]; + } + + set(this, 'length', 0); + + propertyDidChange(this, 'firstObject'); + propertyDidChange(this, 'lastObject'); + this.enumerableContentDidChange(len, 0); + + return this; + }, + + /** + Returns true if the passed object is also an enumerable that contains the + same objects as the receiver. + + ```javascript + var colors = ["red", "green", "blue"], + same_colors = new Ember.Set(colors); + + same_colors.isEqual(colors); // true + same_colors.isEqual(["purple", "brown"]); // false + ``` + + @method isEqual + @param {Ember.Set} obj the other object. + @return {Boolean} + */ + isEqual: function(obj) { + // fail fast + if (!Enumerable.detect(obj)) return false; + + var loc = get(this, 'length'); + if (get(obj, 'length') !== loc) return false; + + while(--loc >= 0) { + if (!obj.contains(this[loc])) return false; + } + + return true; + }, + + /** + Adds an object to the set. Only non-`null` objects can be added to a set + and those can only be added once. If the object is already in the set or + the passed value is null this method will have no effect. + + This is an alias for `Ember.MutableEnumerable.addObject()`. + + ```javascript + var colors = new Ember.Set(); + colors.add("blue"); // ["blue"] + colors.add("blue"); // ["blue"] + colors.add("red"); // ["blue", "red"] + colors.add(null); // ["blue", "red"] + colors.add(undefined); // ["blue", "red"] + ``` + + @method add + @param {Object} obj The object to add. + @return {Ember.Set} The set itself. + */ + add: aliasMethod('addObject'), + + /** + Removes the object from the set if it is found. If you pass a `null` value + or an object that is already not in the set, this method will have no + effect. This is an alias for `Ember.MutableEnumerable.removeObject()`. + + ```javascript + var colors = new Ember.Set(["red", "green", "blue"]); + colors.remove("red"); // ["blue", "green"] + colors.remove("purple"); // ["blue", "green"] + colors.remove(null); // ["blue", "green"] + ``` + + @method remove + @param {Object} obj The object to remove + @return {Ember.Set} The set itself. + */ + remove: aliasMethod('removeObject'), + + /** + Removes the last element from the set and returns it, or `null` if it's empty. + + ```javascript + var colors = new Ember.Set(["green", "blue"]); + colors.pop(); // "blue" + colors.pop(); // "green" + colors.pop(); // null + ``` + + @method pop + @return {Object} The removed object from the set or null. + */ + pop: function() { + if (get(this, 'isFrozen')) throw new EmberError(FROZEN_ERROR); + var obj = this.length > 0 ? this[this.length-1] : null; + this.remove(obj); + return obj; + }, + + /** + Inserts the given object on to the end of the set. It returns + the set itself. + + This is an alias for `Ember.MutableEnumerable.addObject()`. + + ```javascript + var colors = new Ember.Set(); + colors.push("red"); // ["red"] + colors.push("green"); // ["red", "green"] + colors.push("blue"); // ["red", "green", "blue"] + ``` + + @method push + @return {Ember.Set} The set itself. + */ + push: aliasMethod('addObject'), + + /** + Removes the last element from the set and returns it, or `null` if it's empty. + + This is an alias for `Ember.Set.pop()`. + + ```javascript + var colors = new Ember.Set(["green", "blue"]); + colors.shift(); // "blue" + colors.shift(); // "green" + colors.shift(); // null + ``` + + @method shift + @return {Object} The removed object from the set or null. + */ + shift: aliasMethod('pop'), + + /** + Inserts the given object on to the end of the set. It returns + the set itself. + + This is an alias of `Ember.Set.push()` + + ```javascript + var colors = new Ember.Set(); + colors.unshift("red"); // ["red"] + colors.unshift("green"); // ["red", "green"] + colors.unshift("blue"); // ["red", "green", "blue"] + ``` + + @method unshift + @return {Ember.Set} The set itself. + */ + unshift: aliasMethod('push'), + + /** + Adds each object in the passed enumerable to the set. + + This is an alias of `Ember.MutableEnumerable.addObjects()` + + ```javascript + var colors = new Ember.Set(); + colors.addEach(["red", "green", "blue"]); // ["red", "green", "blue"] + ``` + + @method addEach + @param {Ember.Enumerable} objects the objects to add. + @return {Ember.Set} The set itself. + */ + addEach: aliasMethod('addObjects'), + + /** + Removes each object in the passed enumerable to the set. + + This is an alias of `Ember.MutableEnumerable.removeObjects()` + + ```javascript + var colors = new Ember.Set(["red", "green", "blue"]); + colors.removeEach(["red", "blue"]); // ["green"] + ``` + + @method removeEach + @param {Ember.Enumerable} objects the objects to remove. + @return {Ember.Set} The set itself. + */ + removeEach: aliasMethod('removeObjects'), + + // .......................................................... + // PRIVATE ENUMERABLE SUPPORT + // + + init: function(items) { + this._super(); + if (items) this.addObjects(items); + }, + + // implement Ember.Enumerable + nextObject: function(idx) { + return this[idx]; + }, + + // more optimized version + firstObject: computed(function() { + return this.length > 0 ? this[0] : undefined; + }), + + // more optimized version + lastObject: computed(function() { + return this.length > 0 ? this[this.length-1] : undefined; + }), + + // implements Ember.MutableEnumerable + addObject: function(obj) { + if (get(this, 'isFrozen')) throw new EmberError(FROZEN_ERROR); + if (isNone(obj)) return this; // nothing to do + + var guid = guidFor(obj), + idx = this[guid], + len = get(this, 'length'), + added ; + + if (idx>=0 && idx=0 && idx=0; + }, + + copy: function() { + var C = this.constructor, ret = new C(), loc = get(this, 'length'); + set(ret, 'length', loc); + while(--loc>=0) { + ret[loc] = this[loc]; + ret[guidFor(this[loc])] = loc; + } + return ret; + }, + + toString: function() { + var len = this.length, idx, array = []; + for(idx = 0; idx < len; idx++) { + array[idx] = this[idx]; + } + return fmt("Ember.Set<%@>", [array.join(',')]); + } + + }); + + + __exports__["default"] = Set; + }); +define("ember-runtime/system/string", + ["ember-metal/core","ember-metal/utils","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + /** + @module ember + @submodule ember-runtime + */ + var Ember = __dependency1__["default"]; + // Ember.STRINGS, Ember.FEATURES + var EmberInspect = __dependency2__.inspect; + + + var STRING_DASHERIZE_REGEXP = (/[ _]/g); + var STRING_DASHERIZE_CACHE = {}; + var STRING_DECAMELIZE_REGEXP = (/([a-z\d])([A-Z])/g); + var STRING_CAMELIZE_REGEXP = (/(\-|_|\.|\s)+(.)?/g); + var STRING_UNDERSCORE_REGEXP_1 = (/([a-z\d])([A-Z]+)/g); + var STRING_UNDERSCORE_REGEXP_2 = (/\-|\s+/g); + + function fmt(str, formats) { + // first, replace any ORDERED replacements. + var idx = 0; // the current index for non-numerical replacements + return str.replace(/%@([0-9]+)?/g, function(s, argIndex) { + argIndex = (argIndex) ? parseInt(argIndex, 10) - 1 : idx++; + s = formats[argIndex]; + return (s === null) ? '(null)' : (s === undefined) ? '' : EmberInspect(s); + }) ; + } + + function loc(str, formats) { + str = Ember.STRINGS[str] || str; + return fmt(str, formats); + } + + function w(str) { + return str.split(/\s+/); + } + + function decamelize(str) { + return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase(); + } + + function dasherize(str) { + var cache = STRING_DASHERIZE_CACHE, + hit = cache.hasOwnProperty(str), + ret; + + if (hit) { + return cache[str]; + } else { + ret = decamelize(str).replace(STRING_DASHERIZE_REGEXP,'-'); + cache[str] = ret; + } + + return ret; + } + + function camelize(str) { + return str.replace(STRING_CAMELIZE_REGEXP, function(match, separator, chr) { + return chr ? chr.toUpperCase() : ''; + }).replace(/^([A-Z])/, function(match, separator, chr) { + return match.toLowerCase(); + }); + } + + function classify(str) { + var parts = str.split("."), + out = []; + + for (var i=0, l=parts.length; i alpha + // > beta + // > gamma + ``` + + @method w + @param {String} str The string to split + @return {Array} array containing the split strings + */ + w: w, + + /** + Converts a camelized string into all lower case separated by underscores. + + ```javascript + 'innerHTML'.decamelize(); // 'inner_html' + 'action_name'.decamelize(); // 'action_name' + 'css-class-name'.decamelize(); // 'css-class-name' + 'my favorite items'.decamelize(); // 'my favorite items' + ``` + + @method decamelize + @param {String} str The string to decamelize. + @return {String} the decamelized string. + */ + decamelize: decamelize, + + /** + Replaces underscores, spaces, or camelCase with dashes. + + ```javascript + 'innerHTML'.dasherize(); // 'inner-html' + 'action_name'.dasherize(); // 'action-name' + 'css-class-name'.dasherize(); // 'css-class-name' + 'my favorite items'.dasherize(); // 'my-favorite-items' + ``` + + @method dasherize + @param {String} str The string to dasherize. + @return {String} the dasherized string. + */ + dasherize: dasherize, + + /** + Returns the lowerCamelCase form of a string. + + ```javascript + 'innerHTML'.camelize(); // 'innerHTML' + 'action_name'.camelize(); // 'actionName' + 'css-class-name'.camelize(); // 'cssClassName' + 'my favorite items'.camelize(); // 'myFavoriteItems' + 'My Favorite Items'.camelize(); // 'myFavoriteItems' + ``` + + @method camelize + @param {String} str The string to camelize. + @return {String} the camelized string. + */ + camelize: camelize, + + /** + Returns the UpperCamelCase form of a string. + + ```javascript + 'innerHTML'.classify(); // 'InnerHTML' + 'action_name'.classify(); // 'ActionName' + 'css-class-name'.classify(); // 'CssClassName' + 'my favorite items'.classify(); // 'MyFavoriteItems' + ``` + + @method classify + @param {String} str the string to classify + @return {String} the classified string + */ + classify: classify, + + /** + More general than decamelize. Returns the lower\_case\_and\_underscored + form of a string. + + ```javascript + 'innerHTML'.underscore(); // 'inner_html' + 'action_name'.underscore(); // 'action_name' + 'css-class-name'.underscore(); // 'css_class_name' + 'my favorite items'.underscore(); // 'my_favorite_items' + ``` + + @method underscore + @param {String} str The string to underscore. + @return {String} the underscored string. + */ + underscore: underscore, + + /** + Returns the Capitalized form of a string + + ```javascript + 'innerHTML'.capitalize() // 'InnerHTML' + 'action_name'.capitalize() // 'Action_name' + 'css-class-name'.capitalize() // 'Css-class-name' + 'my favorite items'.capitalize() // 'My favorite items' + ``` + + @method capitalize + @param {String} str The string to capitalize. + @return {String} The capitalized string. + */ + capitalize: capitalize + }; + + __exports__["default"] = EmberStringUtils; + __exports__.fmt = fmt; + __exports__.loc = loc; + __exports__.w = w; + __exports__.decamelize = decamelize; + __exports__.dasherize = dasherize; + __exports__.camelize = camelize; + __exports__.classify = classify; + __exports__.underscore = underscore; + __exports__.capitalize = capitalize; + }); +define("ember-runtime/system/subarray", + ["ember-metal/property_get","ember-metal/error","ember-metal/enumerable_utils","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __exports__) { + "use strict"; + var get = __dependency1__.get; + var EmberError = __dependency2__["default"]; + var EnumerableUtils = __dependency3__["default"]; + + var RETAIN = 'r', + FILTER = 'f'; + + function Operation (type, count) { + this.type = type; + this.count = count; + } + + /** + An `Ember.SubArray` tracks an array in a way similar to, but more specialized + than, `Ember.TrackedArray`. It is useful for keeping track of the indexes of + items within a filtered array. + + @class SubArray + @namespace Ember + */ + function SubArray (length) { + if (arguments.length < 1) { length = 0; } + + if (length > 0) { + this._operations = [new Operation(RETAIN, length)]; + } else { + this._operations = []; + } + }; + + SubArray.prototype = { + /** + Track that an item was added to the tracked array. + + @method addItem + + @param {number} index The index of the item in the tracked array. + @param {boolean} match `true` iff the item is included in the subarray. + + @return {number} The index of the item in the subarray. + */ + addItem: function(index, match) { + var returnValue = -1, + itemType = match ? RETAIN : FILTER, + self = this; + + this._findOperation(index, function(operation, operationIndex, rangeStart, rangeEnd, seenInSubArray) { + var newOperation, splitOperation; + + if (itemType === operation.type) { + ++operation.count; + } else if (index === rangeStart) { + // insert to the left of `operation` + self._operations.splice(operationIndex, 0, new Operation(itemType, 1)); + } else { + newOperation = new Operation(itemType, 1); + splitOperation = new Operation(operation.type, rangeEnd - index + 1); + operation.count = index - rangeStart; + + self._operations.splice(operationIndex + 1, 0, newOperation, splitOperation); + } + + if (match) { + if (operation.type === RETAIN) { + returnValue = seenInSubArray + (index - rangeStart); + } else { + returnValue = seenInSubArray; + } + } + + self._composeAt(operationIndex); + }, function(seenInSubArray) { + self._operations.push(new Operation(itemType, 1)); + + if (match) { + returnValue = seenInSubArray; + } + + self._composeAt(self._operations.length-1); + }); + + return returnValue; + }, + + /** + Track that an item was removed from the tracked array. + + @method removeItem + + @param {number} index The index of the item in the tracked array. + + @return {number} The index of the item in the subarray, or `-1` if the item + was not in the subarray. + */ + removeItem: function(index) { + var returnValue = -1, + self = this; + + this._findOperation(index, function (operation, operationIndex, rangeStart, rangeEnd, seenInSubArray) { + if (operation.type === RETAIN) { + returnValue = seenInSubArray + (index - rangeStart); + } + + if (operation.count > 1) { + --operation.count; + } else { + self._operations.splice(operationIndex, 1); + self._composeAt(operationIndex); + } + }, function() { + throw new EmberError("Can't remove an item that has never been added."); + }); + + return returnValue; + }, + + + _findOperation: function (index, foundCallback, notFoundCallback) { + var operationIndex, + len, + operation, + rangeStart, + rangeEnd, + seenInSubArray = 0; + + // OPTIMIZE: change to balanced tree + // find leftmost operation to the right of `index` + for (operationIndex = rangeStart = 0, len = this._operations.length; operationIndex < len; rangeStart = rangeEnd + 1, ++operationIndex) { + operation = this._operations[operationIndex]; + rangeEnd = rangeStart + operation.count - 1; + + if (index >= rangeStart && index <= rangeEnd) { + foundCallback(operation, operationIndex, rangeStart, rangeEnd, seenInSubArray); + return; + } else if (operation.type === RETAIN) { + seenInSubArray += operation.count; + } + } + + notFoundCallback(seenInSubArray); + }, + + _composeAt: function(index) { + var op = this._operations[index], + otherOp; + + if (!op) { + // Composing out of bounds is a no-op, as when removing the last operation + // in the list. + return; + } + + if (index > 0) { + otherOp = this._operations[index-1]; + if (otherOp.type === op.type) { + op.count += otherOp.count; + this._operations.splice(index-1, 1); + --index; + } + } + + if (index < this._operations.length-1) { + otherOp = this._operations[index+1]; + if (otherOp.type === op.type) { + op.count += otherOp.count; + this._operations.splice(index+1, 1); + } + } + }, + + toString: function () { + var str = ""; + forEach(this._operations, function (operation) { + str += " " + operation.type + ":" + operation.count; + }); + return str.substring(1); + } + }; + + __exports__["default"] = SubArray; + }); +define("ember-runtime/system/tracked_array", + ["ember-metal/property_get","ember-metal/enumerable_utils","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + var get = __dependency1__.get; + var EnumerableUtils = __dependency2__["default"]; + + var forEach = EnumerableUtils.forEach, + RETAIN = 'r', + INSERT = 'i', + DELETE = 'd'; + + + /** + An `Ember.TrackedArray` tracks array operations. It's useful when you want to + lazily compute the indexes of items in an array after they've been shifted by + subsequent operations. + + @class TrackedArray + @namespace Ember + @param {array} [items=[]] The array to be tracked. This is used just to get + the initial items for the starting state of retain:n. + */ + function TrackedArray(items) { + if (arguments.length < 1) { items = []; } + + var length = get(items, 'length'); + + if (length) { + this._operations = [new ArrayOperation(RETAIN, length, items)]; + } else { + this._operations = []; + } + } + + TrackedArray.RETAIN = RETAIN; + TrackedArray.INSERT = INSERT; + TrackedArray.DELETE = DELETE; + + TrackedArray.prototype = { + + /** + Track that `newItems` were added to the tracked array at `index`. + + @method addItems + @param index + @param newItems + */ + addItems: function (index, newItems) { + var count = get(newItems, 'length'); + if (count < 1) { return; } + + var match = this._findArrayOperation(index), + arrayOperation = match.operation, + arrayOperationIndex = match.index, + arrayOperationRangeStart = match.rangeStart, + composeIndex, + splitIndex, + splitItems, + splitArrayOperation, + newArrayOperation; + + newArrayOperation = new ArrayOperation(INSERT, count, newItems); + + if (arrayOperation) { + if (!match.split) { + // insert left of arrayOperation + this._operations.splice(arrayOperationIndex, 0, newArrayOperation); + composeIndex = arrayOperationIndex; + } else { + this._split(arrayOperationIndex, index - arrayOperationRangeStart, newArrayOperation); + composeIndex = arrayOperationIndex + 1; + } + } else { + // insert at end + this._operations.push(newArrayOperation); + composeIndex = arrayOperationIndex; + } + + this._composeInsert(composeIndex); + }, + + /** + Track that `count` items were removed at `index`. + + @method removeItems + @param index + @param count + */ + removeItems: function (index, count) { + if (count < 1) { return; } + + var match = this._findArrayOperation(index), + arrayOperation = match.operation, + arrayOperationIndex = match.index, + arrayOperationRangeStart = match.rangeStart, + newArrayOperation, + composeIndex; + + newArrayOperation = new ArrayOperation(DELETE, count); + if (!match.split) { + // insert left of arrayOperation + this._operations.splice(arrayOperationIndex, 0, newArrayOperation); + composeIndex = arrayOperationIndex; + } else { + this._split(arrayOperationIndex, index - arrayOperationRangeStart, newArrayOperation); + composeIndex = arrayOperationIndex + 1; + } + + return this._composeDelete(composeIndex); + }, + + /** + Apply all operations, reducing them to retain:n, for `n`, the number of + items in the array. + + `callback` will be called for each operation and will be passed the following arguments: + + * {array} items The items for the given operation + * {number} offset The computed offset of the items, ie the index in the + array of the first item for this operation. + * {string} operation The type of the operation. One of + `Ember.TrackedArray.{RETAIN, DELETE, INSERT}` + + @method apply + @param {function} callback + */ + apply: function (callback) { + var items = [], + offset = 0; + + forEach(this._operations, function (arrayOperation, operationIndex) { + callback(arrayOperation.items, offset, arrayOperation.type, operationIndex); + + if (arrayOperation.type !== DELETE) { + offset += arrayOperation.count; + items = items.concat(arrayOperation.items); + } + }); + + this._operations = [new ArrayOperation(RETAIN, items.length, items)]; + }, + + /** + Return an `ArrayOperationMatch` for the operation that contains the item at `index`. + + @method _findArrayOperation + + @param {number} index the index of the item whose operation information + should be returned. + @private + */ + _findArrayOperation: function (index) { + var arrayOperationIndex, + len, + split = false, + arrayOperation, + arrayOperationRangeStart, + arrayOperationRangeEnd; + + // OPTIMIZE: we could search these faster if we kept a balanced tree. + // find leftmost arrayOperation to the right of `index` + for (arrayOperationIndex = arrayOperationRangeStart = 0, len = this._operations.length; arrayOperationIndex < len; ++arrayOperationIndex) { + arrayOperation = this._operations[arrayOperationIndex]; + + if (arrayOperation.type === DELETE) { continue; } + + arrayOperationRangeEnd = arrayOperationRangeStart + arrayOperation.count - 1; + + if (index === arrayOperationRangeStart) { + break; + } else if (index > arrayOperationRangeStart && index <= arrayOperationRangeEnd) { + split = true; + break; + } else { + arrayOperationRangeStart = arrayOperationRangeEnd + 1; + } + } + + return new ArrayOperationMatch(arrayOperation, arrayOperationIndex, split, arrayOperationRangeStart); + }, + + _split: function (arrayOperationIndex, splitIndex, newArrayOperation) { + var arrayOperation = this._operations[arrayOperationIndex], + splitItems = arrayOperation.items.slice(splitIndex), + splitArrayOperation = new ArrayOperation(arrayOperation.type, splitItems.length, splitItems); + + // truncate LHS + arrayOperation.count = splitIndex; + arrayOperation.items = arrayOperation.items.slice(0, splitIndex); + + this._operations.splice(arrayOperationIndex + 1, 0, newArrayOperation, splitArrayOperation); + }, + + // see SubArray for a better implementation. + _composeInsert: function (index) { + var newArrayOperation = this._operations[index], + leftArrayOperation = this._operations[index-1], // may be undefined + rightArrayOperation = this._operations[index+1], // may be undefined + leftOp = leftArrayOperation && leftArrayOperation.type, + rightOp = rightArrayOperation && rightArrayOperation.type; + + if (leftOp === INSERT) { + // merge left + leftArrayOperation.count += newArrayOperation.count; + leftArrayOperation.items = leftArrayOperation.items.concat(newArrayOperation.items); + + if (rightOp === INSERT) { + // also merge right (we have split an insert with an insert) + leftArrayOperation.count += rightArrayOperation.count; + leftArrayOperation.items = leftArrayOperation.items.concat(rightArrayOperation.items); + this._operations.splice(index, 2); + } else { + // only merge left + this._operations.splice(index, 1); + } + } else if (rightOp === INSERT) { + // merge right + newArrayOperation.count += rightArrayOperation.count; + newArrayOperation.items = newArrayOperation.items.concat(rightArrayOperation.items); + this._operations.splice(index + 1, 1); + } + }, + + _composeDelete: function (index) { + var arrayOperation = this._operations[index], + deletesToGo = arrayOperation.count, + leftArrayOperation = this._operations[index-1], // may be undefined + leftOp = leftArrayOperation && leftArrayOperation.type, + nextArrayOperation, + nextOp, + nextCount, + removeNewAndNextOp = false, + removedItems = []; + + if (leftOp === DELETE) { + arrayOperation = leftArrayOperation; + index -= 1; + } + + for (var i = index + 1; deletesToGo > 0; ++i) { + nextArrayOperation = this._operations[i]; + nextOp = nextArrayOperation.type; + nextCount = nextArrayOperation.count; + + if (nextOp === DELETE) { + arrayOperation.count += nextCount; + continue; + } + + if (nextCount > deletesToGo) { + // d:2 {r,i}:5 we reduce the retain or insert, but it stays + removedItems = removedItems.concat(nextArrayOperation.items.splice(0, deletesToGo)); + nextArrayOperation.count -= deletesToGo; + + // In the case where we truncate the last arrayOperation, we don't need to + // remove it; also the deletesToGo reduction is not the entirety of + // nextCount + i -= 1; + nextCount = deletesToGo; + + deletesToGo = 0; + } else { + if (nextCount === deletesToGo) { + // Handle edge case of d:2 i:2 in which case both operations go away + // during composition. + removeNewAndNextOp = true; + } + removedItems = removedItems.concat(nextArrayOperation.items); + deletesToGo -= nextCount; + } + + if (nextOp === INSERT) { + // d:2 i:3 will result in delete going away + arrayOperation.count -= nextCount; + } + } + + if (arrayOperation.count > 0) { + // compose our new delete with possibly several operations to the right of + // disparate types + this._operations.splice(index+1, i-1-index); + } else { + // The delete operation can go away; it has merely reduced some other + // operation, as in d:3 i:4; it may also have eliminated that operation, + // as in d:3 i:3. + this._operations.splice(index, removeNewAndNextOp ? 2 : 1); + } + + return removedItems; + }, + + toString: function () { + var str = ""; + forEach(this._operations, function (operation) { + str += " " + operation.type + ":" + operation.count; + }); + return str.substring(1); + } + }; + + /** + Internal data structure to represent an array operation. + + @method ArrayOperation + @private + @param {string} type The type of the operation. One of + `Ember.TrackedArray.{RETAIN, INSERT, DELETE}` + @param {number} count The number of items in this operation. + @param {array} items The items of the operation, if included. RETAIN and + INSERT include their items, DELETE does not. + */ + function ArrayOperation (operation, count, items) { + this.type = operation; // RETAIN | INSERT | DELETE + this.count = count; + this.items = items; + } + + /** + Internal data structure used to include information when looking up operations + by item index. + + @method ArrayOperationMatch + @private + @param {ArrayOperation} operation + @param {number} index The index of `operation` in the array of operations. + @param {boolean} split Whether or not the item index searched for would + require a split for a new operation type. + @param {number} rangeStart The index of the first item in the operation, + with respect to the tracked array. The index of the last item can be computed + from `rangeStart` and `operation.count`. + */ + function ArrayOperationMatch(operation, index, split, rangeStart) { + this.operation = operation; + this.index = index; + this.split = split; + this.rangeStart = rangeStart; + } + + __exports__["default"] = TrackedArray; + }); +})(); + +(function() { +define("ember-views", + ["ember-runtime","ember-views/system/jquery","ember-views/system/utils","ember-views/system/render_buffer","ember-views/system/ext","ember-views/views/states","ember-views/views/view","ember-views/views/container_view","ember-views/views/collection_view","ember-views/views/component","ember-views/system/event_dispatcher","ember-views/mixins/view_target_action_support","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __dependency11__, __dependency12__, __exports__) { + "use strict"; + /** + Ember Views + + @module ember + @submodule ember-views + @requires ember-runtime + @main ember-views + */ + + // BEGIN EXPORTS + Ember.$ = __dependency2__["default"]; + + Ember.ViewTargetActionSupport = __dependency12__["default"]; + Ember.RenderBuffer = __dependency4__["default"]; + + var ViewUtils = Ember.ViewUtils = {}; + ViewUtils.setInnerHTML = __dependency3__.setInnerHTML; + ViewUtils.isSimpleClick = __dependency3__.isSimpleClick; + + Ember.CoreView = __dependency7__.CoreView; + Ember.View = __dependency7__.View; + Ember.View.states = __dependency6__.states; + Ember.View.cloneStates = __dependency6__.cloneStates; + + Ember._ViewCollection = __dependency7__.ViewCollection; + Ember.ContainerView = __dependency8__["default"]; + Ember.CollectionView = __dependency9__["default"]; + Ember.Component = __dependency10__["default"]; + Ember.EventDispatcher = __dependency11__["default"]; + // END EXPORTS + + __exports__["default"] = Ember; + }); +define("ember-views/mixins/component_template_deprecation", + ["ember-metal/core","ember-metal/property_get","ember-metal/mixin","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __exports__) { + "use strict"; + var Ember = __dependency1__["default"]; + // Ember.deprecate + var get = __dependency2__.get; + var Mixin = __dependency3__.Mixin; + + /** + The ComponentTemplateDeprecation mixin is used to provide a useful + deprecation warning when using either `template` or `templateName` with + a component. The `template` and `templateName` properties specified at + extend time are moved to `layout` and `layoutName` respectively. + + `Ember.ComponentTemplateDeprecation` is used internally by Ember in + `Ember.Component`. + + @class ComponentTemplateDeprecation + @namespace Ember + */ + var ComponentTemplateDeprecation = Mixin.create({ + /** + @private + + Moves `templateName` to `layoutName` and `template` to `layout` at extend + time if a layout is not also specified. + + Note that this currently modifies the mixin themselves, which is technically + dubious but is practically of little consequence. This may change in the + future. + + @method willMergeMixin + @since 1.4.0 + */ + willMergeMixin: function(props) { + // must call _super here to ensure that the ActionHandler + // mixin is setup properly (moves actions -> _actions) + // + // Calling super is only OK here since we KNOW that + // there is another Mixin loaded first. + this._super.apply(this, arguments); + + var deprecatedProperty, replacementProperty, + layoutSpecified = (props.layoutName || props.layout || get(this, 'layoutName')); + + if (props.templateName && !layoutSpecified) { + deprecatedProperty = 'templateName'; + replacementProperty = 'layoutName'; + + props.layoutName = props.templateName; + delete props['templateName']; + } + + if (props.template && !layoutSpecified) { + deprecatedProperty = 'template'; + replacementProperty = 'layout'; + + props.layout = props.template; + delete props['template']; + } + + if (deprecatedProperty) { + Ember.deprecate('Do not specify ' + deprecatedProperty + ' on a Component, use ' + replacementProperty + ' instead.', false); + } + } + }); + + __exports__["default"] = ComponentTemplateDeprecation; + }); +define("ember-views/mixins/view_target_action_support", + ["ember-metal/mixin","ember-runtime/mixins/target_action_support","ember-metal/computed","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __exports__) { + "use strict"; + var Mixin = __dependency1__.Mixin; + var TargetActionSupport = __dependency2__["default"]; + + // ES6TODO: computed should have its own export path so you can do import {defaultTo} from computed + var computed = __dependency3__.computed; + var alias = computed.alias; + + /** + `Ember.ViewTargetActionSupport` is a mixin that can be included in a + view class to add a `triggerAction` method with semantics similar to + the Handlebars `{{action}}` helper. It provides intelligent defaults + for the action's target: the view's controller; and the context that is + sent with the action: the view's context. + + Note: In normal Ember usage, the `{{action}}` helper is usually the best + choice. This mixin is most often useful when you are doing more complex + event handling in custom View subclasses. + + For example: + + ```javascript + App.SaveButtonView = Ember.View.extend(Ember.ViewTargetActionSupport, { + action: 'save', + click: function() { + this.triggerAction(); // Sends the `save` action, along with the current context + // to the current controller + } + }); + ``` + + The `action` can be provided as properties of an optional object argument + to `triggerAction` as well. + + ```javascript + App.SaveButtonView = Ember.View.extend(Ember.ViewTargetActionSupport, { + click: function() { + this.triggerAction({ + action: 'save' + }); // Sends the `save` action, along with the current context + // to the current controller + } + }); + ``` + + @class ViewTargetActionSupport + @namespace Ember + @extends Ember.TargetActionSupport + */ + var ViewTargetActionSupport = Mixin.create(TargetActionSupport, { + /** + @property target + */ + target: alias('controller'), + /** + @property actionContext + */ + actionContext: alias('context') + }); + + __exports__["default"] = ViewTargetActionSupport; + }); +define("ember-views/system/event_dispatcher", + ["ember-metal/core","ember-metal/property_get","ember-metal/property_set","ember-metal/is_none","ember-metal/run_loop","ember-metal/utils","ember-runtime/system/string","ember-runtime/system/object","ember-views/system/jquery","ember-views/views/view","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __exports__) { + "use strict"; + /** + @module ember + @submodule ember-views + */ + var Ember = __dependency1__["default"]; + // Ember.assert + + var get = __dependency2__.get; + var set = __dependency3__.set; + var isNone = __dependency4__.isNone; + var run = __dependency5__["default"]; + var typeOf = __dependency6__.typeOf; + var fmt = __dependency7__.fmt; + var EmberObject = __dependency8__["default"]; + var jQuery = __dependency9__["default"]; + var View = __dependency10__.View; + + var ActionHelper; + + //ES6TODO: + // find a better way to do Ember.View.views without global state + + /** + `Ember.EventDispatcher` handles delegating browser events to their + corresponding `Ember.Views.` For example, when you click on a view, + `Ember.EventDispatcher` ensures that that view's `mouseDown` method gets + called. + + @class EventDispatcher + @namespace Ember + @private + @extends Ember.Object + */ + var EventDispatcher = EmberObject.extend({ + + /** + The set of events names (and associated handler function names) to be setup + and dispatched by the `EventDispatcher`. Custom events can added to this list at setup + time, generally via the `Ember.Application.customEvents` hash. Only override this + default set to prevent the EventDispatcher from listening on some events all together. + + This set will be modified by `setup` to also include any events added at that time. + + @property events + @type Object + */ + events: { + touchstart : 'touchStart', + touchmove : 'touchMove', + touchend : 'touchEnd', + touchcancel : 'touchCancel', + keydown : 'keyDown', + keyup : 'keyUp', + keypress : 'keyPress', + mousedown : 'mouseDown', + mouseup : 'mouseUp', + contextmenu : 'contextMenu', + click : 'click', + dblclick : 'doubleClick', + mousemove : 'mouseMove', + focusin : 'focusIn', + focusout : 'focusOut', + mouseenter : 'mouseEnter', + mouseleave : 'mouseLeave', + submit : 'submit', + input : 'input', + change : 'change', + dragstart : 'dragStart', + drag : 'drag', + dragenter : 'dragEnter', + dragleave : 'dragLeave', + dragover : 'dragOver', + drop : 'drop', + dragend : 'dragEnd' + }, + + /** + The root DOM element to which event listeners should be attached. Event + listeners will be attached to the document unless this is overridden. + + Can be specified as a DOMElement or a selector string. + + The default body is a string since this may be evaluated before document.body + exists in the DOM. + + @private + @property rootElement + @type DOMElement + @default 'body' + */ + rootElement: 'body', + + /** + Sets up event listeners for standard browser events. + + This will be called after the browser sends a `DOMContentReady` event. By + default, it will set up all of the listeners on the document body. If you + would like to register the listeners on a different element, set the event + dispatcher's `root` property. + + @private + @method setup + @param addedEvents {Hash} + */ + setup: function(addedEvents, rootElement) { + var event, events = get(this, 'events'); + + jQuery.extend(events, addedEvents || {}); + + + if (!isNone(rootElement)) { + set(this, 'rootElement', rootElement); + } + + rootElement = jQuery(get(this, 'rootElement')); + + Ember.assert(fmt('You cannot use the same root element (%@) multiple times in an Ember.Application', [rootElement.selector || rootElement[0].tagName]), !rootElement.is('.ember-application')); + Ember.assert('You cannot make a new Ember.Application using a root element that is a descendent of an existing Ember.Application', !rootElement.closest('.ember-application').length); + Ember.assert('You cannot make a new Ember.Application using a root element that is an ancestor of an existing Ember.Application', !rootElement.find('.ember-application').length); + + rootElement.addClass('ember-application'); + + Ember.assert('Unable to add "ember-application" class to rootElement. Make sure you set rootElement to the body or an element in the body.', rootElement.is('.ember-application')); + + for (event in events) { + if (events.hasOwnProperty(event)) { + this.setupHandler(rootElement, event, events[event]); + } + } + }, + + /** + Registers an event listener on the document. If the given event is + triggered, the provided event handler will be triggered on the target view. + + If the target view does not implement the event handler, or if the handler + returns `false`, the parent view will be called. The event will continue to + bubble to each successive parent view until it reaches the top. + + For example, to have the `mouseDown` method called on the target view when + a `mousedown` event is received from the browser, do the following: + + ```javascript + setupHandler('mousedown', 'mouseDown'); + ``` + + @private + @method setupHandler + @param {Element} rootElement + @param {String} event the browser-originated event to listen to + @param {String} eventName the name of the method to call on the view + */ + setupHandler: function(rootElement, event, eventName) { + var self = this; + + rootElement.on(event + '.ember', '.ember-view', function(evt, triggeringManager) { + var view = View.views[this.id], + result = true, manager = null; + + manager = self._findNearestEventManager(view, eventName); + + if (manager && manager !== triggeringManager) { + result = self._dispatchEvent(manager, evt, eventName, view); + } else if (view) { + result = self._bubbleEvent(view, evt, eventName); + } else { + evt.stopPropagation(); + } + + return result; + }); + + rootElement.on(event + '.ember', '[data-ember-action]', function(evt) { + //ES6TODO: Needed for ActionHelper (generally not available in ember-views test suite) + if (!ActionHelper) { ActionHelper = requireModule("ember-routing/helpers/action")["ActionHelper"]; }; + + var actionId = jQuery(evt.currentTarget).attr('data-ember-action'), + action = ActionHelper.registeredActions[actionId]; + + // We have to check for action here since in some cases, jQuery will trigger + // an event on `removeChild` (i.e. focusout) after we've already torn down the + // action handlers for the view. + if (action && action.eventName === eventName) { + return action.handler(evt); + } + }); + }, + + _findNearestEventManager: function(view, eventName) { + var manager = null; + + while (view) { + manager = get(view, 'eventManager'); + if (manager && manager[eventName]) { break; } + + view = get(view, 'parentView'); + } + + return manager; + }, + + _dispatchEvent: function(object, evt, eventName, view) { + var result = true; + + var handler = object[eventName]; + if (typeOf(handler) === 'function') { + result = run(object, handler, evt, view); + // Do not preventDefault in eventManagers. + evt.stopPropagation(); + } + else { + result = this._bubbleEvent(view, evt, eventName); + } + + return result; + }, + + _bubbleEvent: function(view, evt, eventName) { + return run(view, view.handleEvent, eventName, evt); + }, + + destroy: function() { + var rootElement = get(this, 'rootElement'); + jQuery(rootElement).off('.ember', '**').removeClass('ember-application'); + return this._super(); + } + }); + + __exports__["default"] = EventDispatcher; + }); +define("ember-views/system/ext", + ["ember-metal/run_loop"], + function(__dependency1__) { + "use strict"; + /** + @module ember + @submodule ember-views + */ + + var run = __dependency1__["default"]; + + // Add a new named queue for rendering views that happens + // after bindings have synced, and a queue for scheduling actions + // that that should occur after view rendering. + var queues = run.queues; + run._addQueue('render', 'actions'); + run._addQueue('afterRender', 'render'); + }); +define("ember-views/system/jquery", + ["ember-metal/core","ember-runtime/system/string","ember-metal/enumerable_utils","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __exports__) { + "use strict"; + var Ember = __dependency1__["default"]; + // Ember.assert + var w = __dependency2__.w; + + // ES6TODO: the functions on EnumerableUtils need their own exports + var EnumerableUtils = __dependency3__["default"]; + var forEach = EnumerableUtils.forEach; + + /** + Ember Views + + @module ember + @submodule ember-views + @requires ember-runtime + @main ember-views + */ + + var jQuery = (Ember.imports && Ember.imports.jQuery) || (this && this.jQuery); + if (!jQuery && typeof require === 'function') { + jQuery = require('jquery'); + } + + Ember.assert("Ember Views require jQuery between 1.7 and 2.1", jQuery && (jQuery().jquery.match(/^((1\.(7|8|9|10|11))|(2\.(0|1)))(\.\d+)?(pre|rc\d?)?/) || Ember.ENV.FORCE_JQUERY)); + + /** + @module ember + @submodule ember-views + */ + if (jQuery) { + // http://www.whatwg.org/specs/web-apps/current-work/multipage/dnd.html#dndevents + var dragEvents = w('dragstart drag dragenter dragleave dragover drop dragend'); + + // Copies the `dataTransfer` property from a browser event object onto the + // jQuery event object for the specified events + forEach(dragEvents, function(eventName) { + jQuery.event.fixHooks[eventName] = { props: ['dataTransfer'] }; + }); + } + + __exports__["default"] = jQuery; + }); +define("ember-views/system/render_buffer", + ["ember-metal/core","ember-metal/property_get","ember-metal/property_set","ember-views/system/utils","ember-views/system/jquery","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __exports__) { + "use strict"; + /** + @module ember + @submodule ember-views + */ + + var Ember = __dependency1__["default"]; + // jQuery + + var get = __dependency2__.get; + var set = __dependency3__.set; + var setInnerHTML = __dependency4__.setInnerHTML; + var jQuery = __dependency5__["default"]; + + function ClassSet() { + this.seen = {}; + this.list = []; + }; + + + ClassSet.prototype = { + add: function(string) { + if (string in this.seen) { return; } + this.seen[string] = true; + + this.list.push(string); + }, + + toDOM: function() { + return this.list.join(" "); + } + }; + + var BAD_TAG_NAME_TEST_REGEXP = /[^a-zA-Z0-9\-]/; + var BAD_TAG_NAME_REPLACE_REGEXP = /[^a-zA-Z0-9\-]/g; + + function stripTagName(tagName) { + if (!tagName) { + return tagName; + } + + if (!BAD_TAG_NAME_TEST_REGEXP.test(tagName)) { + return tagName; + } + + return tagName.replace(BAD_TAG_NAME_REPLACE_REGEXP, ''); + } + + var BAD_CHARS_REGEXP = /&(?!\w+;)|[<>"'`]/g; + var POSSIBLE_CHARS_REGEXP = /[&<>"'`]/; + + function escapeAttribute(value) { + // Stolen shamelessly from Handlebars + + var escape = { + "<": "<", + ">": ">", + '"': """, + "'": "'", + "`": "`" + }; + + var escapeChar = function(chr) { + return escape[chr] || "&"; + }; + + var string = value.toString(); + + if(!POSSIBLE_CHARS_REGEXP.test(string)) { return string; } + return string.replace(BAD_CHARS_REGEXP, escapeChar); + } + + // IE 6/7 have bugs around setting names on inputs during creation. + // From http://msdn.microsoft.com/en-us/library/ie/ms536389(v=vs.85).aspx: + // "To include the NAME attribute at run time on objects created with the createElement method, use the eTag." + var canSetNameOnInputs = (function() { + var div = document.createElement('div'), + el = document.createElement('input'); + + el.setAttribute('name', 'foo'); + div.appendChild(el); + + return !!div.innerHTML.match('foo'); + })(); + + /** + `Ember.RenderBuffer` gathers information regarding the view and generates the + final representation. `Ember.RenderBuffer` will generate HTML which can be pushed + to the DOM. + + ```javascript + var buffer = Ember.RenderBuffer('div'); + ``` + + @class RenderBuffer + @namespace Ember + @constructor + @param {String} tagName tag name (such as 'div' or 'p') used for the buffer + */ + var RenderBuffer = function(tagName) { + return new _RenderBuffer(tagName); + }; + + var _RenderBuffer = function(tagName) { + this.tagNames = [tagName || null]; + this.buffer = ""; + }; + + _RenderBuffer.prototype = { + + // The root view's element + _element: null, + + _hasElement: true, + + /** + An internal set used to de-dupe class names when `addClass()` is + used. After each call to `addClass()`, the `classes` property + will be updated. + + @private + @property elementClasses + @type Array + @default null + */ + elementClasses: null, + + /** + Array of class names which will be applied in the class attribute. + + You can use `setClasses()` to set this property directly. If you + use `addClass()`, it will be maintained for you. + + @property classes + @type Array + @default null + */ + classes: null, + + /** + The id in of the element, to be applied in the id attribute. + + You should not set this property yourself, rather, you should use + the `id()` method of `Ember.RenderBuffer`. + + @property elementId + @type String + @default null + */ + elementId: null, + + /** + A hash keyed on the name of the attribute and whose value will be + applied to that attribute. For example, if you wanted to apply a + `data-view="Foo.bar"` property to an element, you would set the + elementAttributes hash to `{'data-view':'Foo.bar'}`. + + You should not maintain this hash yourself, rather, you should use + the `attr()` method of `Ember.RenderBuffer`. + + @property elementAttributes + @type Hash + @default {} + */ + elementAttributes: null, + + /** + A hash keyed on the name of the properties and whose value will be + applied to that property. For example, if you wanted to apply a + `checked=true` property to an element, you would set the + elementProperties hash to `{'checked':true}`. + + You should not maintain this hash yourself, rather, you should use + the `prop()` method of `Ember.RenderBuffer`. + + @property elementProperties + @type Hash + @default {} + */ + elementProperties: null, + + /** + The tagname of the element an instance of `Ember.RenderBuffer` represents. + + Usually, this gets set as the first parameter to `Ember.RenderBuffer`. For + example, if you wanted to create a `p` tag, then you would call + + ```javascript + Ember.RenderBuffer('p') + ``` + + @property elementTag + @type String + @default null + */ + elementTag: null, + + /** + A hash keyed on the name of the style attribute and whose value will + be applied to that attribute. For example, if you wanted to apply a + `background-color:black;` style to an element, you would set the + elementStyle hash to `{'background-color':'black'}`. + + You should not maintain this hash yourself, rather, you should use + the `style()` method of `Ember.RenderBuffer`. + + @property elementStyle + @type Hash + @default {} + */ + elementStyle: null, + + /** + Nested `RenderBuffers` will set this to their parent `RenderBuffer` + instance. + + @property parentBuffer + @type Ember._RenderBuffer + */ + parentBuffer: null, + + /** + Adds a string of HTML to the `RenderBuffer`. + + @method push + @param {String} string HTML to push into the buffer + @chainable + */ + push: function(string) { + this.buffer += string; + return this; + }, + + /** + Adds a class to the buffer, which will be rendered to the class attribute. + + @method addClass + @param {String} className Class name to add to the buffer + @chainable + */ + addClass: function(className) { + // lazily create elementClasses + this.elementClasses = (this.elementClasses || new ClassSet()); + this.elementClasses.add(className); + this.classes = this.elementClasses.list; + + return this; + }, + + setClasses: function(classNames) { + this.elementClasses = null; + var len = classNames.length, i; + for (i = 0; i < len; i++) { + this.addClass(classNames[i]); + } + }, + + /** + Sets the elementID to be used for the element. + + @method id + @param {String} id + @chainable + */ + id: function(id) { + this.elementId = id; + return this; + }, + + // duck type attribute functionality like jQuery so a render buffer + // can be used like a jQuery object in attribute binding scenarios. + + /** + Adds an attribute which will be rendered to the element. + + @method attr + @param {String} name The name of the attribute + @param {String} value The value to add to the attribute + @chainable + @return {Ember.RenderBuffer|String} this or the current attribute value + */ + attr: function(name, value) { + var attributes = this.elementAttributes = (this.elementAttributes || {}); + + if (arguments.length === 1) { + return attributes[name]; + } else { + attributes[name] = value; + } + + return this; + }, + + /** + Remove an attribute from the list of attributes to render. + + @method removeAttr + @param {String} name The name of the attribute + @chainable + */ + removeAttr: function(name) { + var attributes = this.elementAttributes; + if (attributes) { delete attributes[name]; } + + return this; + }, + + /** + Adds a property which will be rendered to the element. + + @method prop + @param {String} name The name of the property + @param {String} value The value to add to the property + @chainable + @return {Ember.RenderBuffer|String} this or the current property value + */ + prop: function(name, value) { + var properties = this.elementProperties = (this.elementProperties || {}); + + if (arguments.length === 1) { + return properties[name]; + } else { + properties[name] = value; + } + + return this; + }, + + /** + Remove an property from the list of properties to render. + + @method removeProp + @param {String} name The name of the property + @chainable + */ + removeProp: function(name) { + var properties = this.elementProperties; + if (properties) { delete properties[name]; } + + return this; + }, + + /** + Adds a style to the style attribute which will be rendered to the element. + + @method style + @param {String} name Name of the style + @param {String} value + @chainable + */ + style: function(name, value) { + this.elementStyle = (this.elementStyle || {}); + + this.elementStyle[name] = value; + return this; + }, + + begin: function(tagName) { + this.tagNames.push(tagName || null); + return this; + }, + + pushOpeningTag: function() { + var tagName = this.currentTagName(); + if (!tagName) { return; } + + if (this._hasElement && !this._element && this.buffer.length === 0) { + this._element = this.generateElement(); + return; + } + + var buffer = this.buffer, + id = this.elementId, + classes = this.classes, + attrs = this.elementAttributes, + props = this.elementProperties, + style = this.elementStyle, + attr, prop; + + buffer += '<' + stripTagName(tagName); + + if (id) { + buffer += ' id="' + escapeAttribute(id) + '"'; + this.elementId = null; + } + if (classes) { + buffer += ' class="' + escapeAttribute(classes.join(' ')) + '"'; + this.classes = null; + this.elementClasses = null; + } + + if (style) { + buffer += ' style="'; + + for (prop in style) { + if (style.hasOwnProperty(prop)) { + buffer += prop + ':' + escapeAttribute(style[prop]) + ';'; + } + } + + buffer += '"'; + + this.elementStyle = null; + } + + if (attrs) { + for (attr in attrs) { + if (attrs.hasOwnProperty(attr)) { + buffer += ' ' + attr + '="' + escapeAttribute(attrs[attr]) + '"'; + } + } + + this.elementAttributes = null; + } + + if (props) { + for (prop in props) { + if (props.hasOwnProperty(prop)) { + var value = props[prop]; + if (value || typeof(value) === 'number') { + if (value === true) { + buffer += ' ' + prop + '="' + prop + '"'; + } else { + buffer += ' ' + prop + '="' + escapeAttribute(props[prop]) + '"'; + } + } + } + } + + this.elementProperties = null; + } + + buffer += '>'; + this.buffer = buffer; + }, + + pushClosingTag: function() { + var tagName = this.tagNames.pop(); + if (tagName) { this.buffer += ''; } + }, + + currentTagName: function() { + return this.tagNames[this.tagNames.length-1]; + }, + + generateElement: function() { + var tagName = this.tagNames.pop(), // pop since we don't need to close + id = this.elementId, + classes = this.classes, + attrs = this.elementAttributes, + props = this.elementProperties, + style = this.elementStyle, + styleBuffer = '', attr, prop, tagString; + + if (attrs && attrs.name && !canSetNameOnInputs) { + // IE allows passing a tag to createElement. See note on `canSetNameOnInputs` above as well. + tagString = '<'+stripTagName(tagName)+' name="'+escapeAttribute(attrs.name)+'">'; + } else { + tagString = tagName; + } + + var element = document.createElement(tagString), + $element = jQuery(element); + + if (id) { + $element.attr('id', id); + this.elementId = null; + } + if (classes) { + $element.attr('class', classes.join(' ')); + this.classes = null; + this.elementClasses = null; + } + + if (style) { + for (prop in style) { + if (style.hasOwnProperty(prop)) { + styleBuffer += (prop + ':' + style[prop] + ';'); + } + } + + $element.attr('style', styleBuffer); + + this.elementStyle = null; + } + + if (attrs) { + for (attr in attrs) { + if (attrs.hasOwnProperty(attr)) { + $element.attr(attr, attrs[attr]); + } + } + + this.elementAttributes = null; + } + + if (props) { + for (prop in props) { + if (props.hasOwnProperty(prop)) { + $element.prop(prop, props[prop]); + } + } + + this.elementProperties = null; + } + + return element; + }, + + /** + @method element + @return {DOMElement} The element corresponding to the generated HTML + of this buffer + */ + element: function() { + var html = this.innerString(); + + if (html) { + this._element = setInnerHTML(this._element, html); + } + + return this._element; + }, + + /** + Generates the HTML content for this buffer. + + @method string + @return {String} The generated HTML + */ + string: function() { + if (this._hasElement && this._element) { + // Firefox versions < 11 do not have support for element.outerHTML. + var thisElement = this.element(), outerHTML = thisElement.outerHTML; + if (typeof outerHTML === 'undefined') { + return jQuery('
    ').append(thisElement).html(); + } + return outerHTML; + } else { + return this.innerString(); + } + }, + + innerString: function() { + return this.buffer; + } + }; + + __exports__["default"] = RenderBuffer; + }); +define("ember-views/system/utils", + ["ember-metal/core","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Ember = __dependency1__["default"]; + // Ember.assert + + /** + @module ember + @submodule ember-views + */ + + /* BEGIN METAMORPH HELPERS */ + + // Internet Explorer prior to 9 does not allow setting innerHTML if the first element + // is a "zero-scope" element. This problem can be worked around by making + // the first node an invisible text node. We, like Modernizr, use ­ + + var needsShy = typeof document !== 'undefined' && (function() { + var testEl = document.createElement('div'); + testEl.innerHTML = "
    "; + testEl.firstChild.innerHTML = ""; + return testEl.firstChild.innerHTML === ''; + })(); + + // IE 8 (and likely earlier) likes to move whitespace preceeding + // a script tag to appear after it. This means that we can + // accidentally remove whitespace when updating a morph. + var movesWhitespace = typeof document !== 'undefined' && (function() { + var testEl = document.createElement('div'); + testEl.innerHTML = "Test: Value"; + return testEl.childNodes[0].nodeValue === 'Test:' && + testEl.childNodes[2].nodeValue === ' Value'; + })(); + + // Use this to find children by ID instead of using jQuery + var findChildById = function(element, id) { + if (element.getAttribute('id') === id) { return element; } + + var len = element.childNodes.length, idx, node, found; + for (idx=0; idx 0) { + var len = matches.length, idx; + for (idx=0; idxTest'); + canSet = el.options.length === 1; + } + + innerHTMLTags[tagName] = canSet; + + return canSet; + }; + + var setInnerHTML = function(element, html) { + var tagName = element.tagName; + + if (canSetInnerHTML(tagName)) { + setInnerHTMLWithoutFix(element, html); + } else { + // Firefox versions < 11 do not have support for element.outerHTML. + var outerHTML = element.outerHTML || new XMLSerializer().serializeToString(element); + Ember.assert("Can't set innerHTML on "+element.tagName+" in this browser", outerHTML); + + var startTag = outerHTML.match(new RegExp("<"+tagName+"([^>]*)>", 'i'))[0], + endTag = ''; + + var wrapper = document.createElement('div'); + setInnerHTMLWithoutFix(wrapper, startTag + html + endTag); + element = wrapper.firstChild; + while (element.tagName !== tagName) { + element = element.nextSibling; + } + } + + return element; + }; + + function isSimpleClick(event) { + var modifier = event.shiftKey || event.metaKey || event.altKey || event.ctrlKey, + secondaryClick = event.which > 1; // IE9 may return undefined + + return !modifier && !secondaryClick; + } + + __exports__.setInnerHTML = setInnerHTML; + __exports__.isSimpleClick = isSimpleClick; + }); +define("ember-views/views/collection_view", + ["ember-metal/core","ember-metal/platform","ember-metal/binding","ember-metal/merge","ember-metal/property_get","ember-metal/property_set","ember-runtime/system/string","ember-views/views/container_view","ember-views/views/view","ember-metal/mixin","ember-runtime/mixins/array","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __dependency11__, __exports__) { + "use strict"; + + /** + @module ember + @submodule ember-views + */ + + var Ember = __dependency1__["default"]; + // Ember.assert + var create = __dependency2__.create; + var isGlobalPath = __dependency3__.isGlobalPath; + var merge = __dependency4__["default"]; + var get = __dependency5__.get; + var set = __dependency6__.set; + var fmt = __dependency7__.fmt; + var ContainerView = __dependency8__["default"]; + var CoreView = __dependency9__.CoreView; + var View = __dependency9__.View; + var observer = __dependency10__.observer; + var beforeObserver = __dependency10__.beforeObserver; + var EmberArray = __dependency11__["default"]; + + /** + `Ember.CollectionView` is an `Ember.View` descendent responsible for managing + a collection (an array or array-like object) by maintaining a child view object + and associated DOM representation for each item in the array and ensuring + that child views and their associated rendered HTML are updated when items in + the array are added, removed, or replaced. + + ## Setting content + + The managed collection of objects is referenced as the `Ember.CollectionView` + instance's `content` property. + + ```javascript + someItemsView = Ember.CollectionView.create({ + content: ['A', 'B','C'] + }) + ``` + + The view for each item in the collection will have its `content` property set + to the item. + + ## Specifying itemViewClass + + By default the view class for each item in the managed collection will be an + instance of `Ember.View`. You can supply a different class by setting the + `CollectionView`'s `itemViewClass` property. + + Given an empty `` and the following code: + + ```javascript + someItemsView = Ember.CollectionView.create({ + classNames: ['a-collection'], + content: ['A','B','C'], + itemViewClass: Ember.View.extend({ + template: Ember.Handlebars.compile("the letter: {{view.content}}") + }) + }); + + someItemsView.appendTo('body'); + ``` + + Will result in the following HTML structure + + ```html +
    +
    the letter: A
    +
    the letter: B
    +
    the letter: C
    +
    + ``` + + ## Automatic matching of parent/child tagNames + + Setting the `tagName` property of a `CollectionView` to any of + "ul", "ol", "table", "thead", "tbody", "tfoot", "tr", or "select" will result + in the item views receiving an appropriately matched `tagName` property. + + Given an empty `` and the following code: + + ```javascript + anUnorderedListView = Ember.CollectionView.create({ + tagName: 'ul', + content: ['A','B','C'], + itemViewClass: Ember.View.extend({ + template: Ember.Handlebars.compile("the letter: {{view.content}}") + }) + }); + + anUnorderedListView.appendTo('body'); + ``` + + Will result in the following HTML structure + + ```html +
      +
    • the letter: A
    • +
    • the letter: B
    • +
    • the letter: C
    • +
    + ``` + + Additional `tagName` pairs can be provided by adding to + `Ember.CollectionView.CONTAINER_MAP ` + + ```javascript + Ember.CollectionView.CONTAINER_MAP['article'] = 'section' + ``` + + ## Programmatic creation of child views + + For cases where additional customization beyond the use of a single + `itemViewClass` or `tagName` matching is required CollectionView's + `createChildView` method can be overidden: + + ```javascript + CustomCollectionView = Ember.CollectionView.extend({ + createChildView: function(viewClass, attrs) { + if (attrs.content.kind == 'album') { + viewClass = App.AlbumView; + } else { + viewClass = App.SongView; + } + return this._super(viewClass, attrs); + } + }); + ``` + + ## Empty View + + You can provide an `Ember.View` subclass to the `Ember.CollectionView` + instance as its `emptyView` property. If the `content` property of a + `CollectionView` is set to `null` or an empty array, an instance of this view + will be the `CollectionView`s only child. + + ```javascript + aListWithNothing = Ember.CollectionView.create({ + classNames: ['nothing'] + content: null, + emptyView: Ember.View.extend({ + template: Ember.Handlebars.compile("The collection is empty") + }) + }); + + aListWithNothing.appendTo('body'); + ``` + + Will result in the following HTML structure + + ```html +
    +
    + The collection is empty +
    +
    + ``` + + ## Adding and Removing items + + The `childViews` property of a `CollectionView` should not be directly + manipulated. Instead, add, remove, replace items from its `content` property. + This will trigger appropriate changes to its rendered HTML. + + + @class CollectionView + @namespace Ember + @extends Ember.ContainerView + @since Ember 0.9 + */ + var CollectionView = ContainerView.extend({ + + /** + A list of items to be displayed by the `Ember.CollectionView`. + + @property content + @type Ember.Array + @default null + */ + content: null, + + /** + This provides metadata about what kind of empty view class this + collection would like if it is being instantiated from another + system (like Handlebars) + + @private + @property emptyViewClass + */ + emptyViewClass: View, + + /** + An optional view to display if content is set to an empty array. + + @property emptyView + @type Ember.View + @default null + */ + emptyView: null, + + /** + @property itemViewClass + @type Ember.View + @default Ember.View + */ + itemViewClass: View, + + /** + Setup a CollectionView + + @method init + */ + init: function() { + var ret = this._super(); + this._contentDidChange(); + return ret; + }, + + /** + Invoked when the content property is about to change. Notifies observers that the + entire array content will change. + + @private + @method _contentWillChange + */ + _contentWillChange: beforeObserver('content', function() { + var content = this.get('content'); + + if (content) { content.removeArrayObserver(this); } + var len = content ? get(content, 'length') : 0; + this.arrayWillChange(content, 0, len); + }), + + /** + Check to make sure that the content has changed, and if so, + update the children directly. This is always scheduled + asynchronously, to allow the element to be created before + bindings have synchronized and vice versa. + + @private + @method _contentDidChange + */ + _contentDidChange: observer('content', function() { + var content = get(this, 'content'); + + if (content) { + this._assertArrayLike(content); + content.addArrayObserver(this); + } + + var len = content ? get(content, 'length') : 0; + this.arrayDidChange(content, 0, null, len); + }), + + /** + Ensure that the content implements Ember.Array + + @private + @method _assertArrayLike + */ + _assertArrayLike: function(content) { + Ember.assert(fmt("an Ember.CollectionView's content must implement Ember.Array. You passed %@", [content]), EmberArray.detect(content)); + }, + + /** + Removes the content and content observers. + + @method destroy + */ + destroy: function() { + if (!this._super()) { return; } + + var content = get(this, 'content'); + if (content) { content.removeArrayObserver(this); } + + if (this._createdEmptyView) { + this._createdEmptyView.destroy(); + } + + return this; + }, + + /** + Called when a mutation to the underlying content array will occur. + + This method will remove any views that are no longer in the underlying + content array. + + Invokes whenever the content array itself will change. + + @method arrayWillChange + @param {Array} content the managed collection of objects + @param {Number} start the index at which the changes will occurr + @param {Number} removed number of object to be removed from content + */ + arrayWillChange: function(content, start, removedCount) { + // If the contents were empty before and this template collection has an + // empty view remove it now. + var emptyView = get(this, 'emptyView'); + if (emptyView && emptyView instanceof View) { + emptyView.removeFromParent(); + } + + // Loop through child views that correspond with the removed items. + // Note that we loop from the end of the array to the beginning because + // we are mutating it as we go. + var childViews = this._childViews, childView, idx, len; + + len = this._childViews.length; + + var removingAll = removedCount === len; + + if (removingAll) { + this.currentState.empty(this); + this.invokeRecursively(function(view) { + view.removedFromDOM = true; + }, false); + } + + for (idx = start + removedCount - 1; idx >= start; idx--) { + childView = childViews[idx]; + childView.destroy(); + } + }, + + /** + Called when a mutation to the underlying content array occurs. + + This method will replay that mutation against the views that compose the + `Ember.CollectionView`, ensuring that the view reflects the model. + + This array observer is added in `contentDidChange`. + + @method arrayDidChange + @param {Array} content the managed collection of objects + @param {Number} start the index at which the changes occurred + @param {Number} removed number of object removed from content + @param {Number} added number of object added to content + */ + arrayDidChange: function(content, start, removed, added) { + var addedViews = [], view, item, idx, len, itemViewClass, + emptyView; + + len = content ? get(content, 'length') : 0; + + if (len) { + itemViewClass = get(this, 'itemViewClass'); + + if ('string' === typeof itemViewClass && isGlobalPath(itemViewClass)) { + itemViewClass = get(itemViewClass) || itemViewClass; + } + + Ember.assert(fmt("itemViewClass must be a subclass of Ember.View, not %@", + [itemViewClass]), + 'string' === typeof itemViewClass || View.detect(itemViewClass)); + + for (idx = start; idx < start+added; idx++) { + item = content.objectAt(idx); + + view = this.createChildView(itemViewClass, { + content: item, + contentIndex: idx + }); + + addedViews.push(view); + } + } else { + emptyView = get(this, 'emptyView'); + + if (!emptyView) { return; } + + if ('string' === typeof emptyView && isGlobalPath(emptyView)) { + emptyView = get(emptyView) || emptyView; + } + + emptyView = this.createChildView(emptyView); + addedViews.push(emptyView); + set(this, 'emptyView', emptyView); + + if (CoreView.detect(emptyView)) { + this._createdEmptyView = emptyView; + } + } + + this.replace(start, 0, addedViews); + }, + + /** + Instantiates a view to be added to the childViews array during view + initialization. You generally will not call this method directly unless + you are overriding `createChildViews()`. Note that this method will + automatically configure the correct settings on the new view instance to + act as a child of the parent. + + The tag name for the view will be set to the tagName of the viewClass + passed in. + + @method createChildView + @param {Class} viewClass + @param {Hash} [attrs] Attributes to add + @return {Ember.View} new instance + */ + createChildView: function(view, attrs) { + view = this._super(view, attrs); + + var itemTagName = get(view, 'tagName'); + + if (itemTagName === null || itemTagName === undefined) { + itemTagName = CollectionView.CONTAINER_MAP[get(this, 'tagName')]; + set(view, 'tagName', itemTagName); + } + + return view; + } + }); + + /** + A map of parent tags to their default child tags. You can add + additional parent tags if you want collection views that use + a particular parent tag to default to a child tag. + + @property CONTAINER_MAP + @type Hash + @static + @final + */ + CollectionView.CONTAINER_MAP = { + ul: 'li', + ol: 'li', + table: 'tr', + thead: 'tr', + tbody: 'tr', + tfoot: 'tr', + tr: 'td', + select: 'option' + }; + + __exports__["default"] = CollectionView; + }); +define("ember-views/views/component", + ["ember-metal/core","ember-views/mixins/component_template_deprecation","ember-runtime/mixins/target_action_support","ember-views/views/view","ember-metal/property_get","ember-metal/property_set","ember-metal/is_none","ember-metal/computed","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __exports__) { + "use strict"; + var Ember = __dependency1__["default"]; + // Ember.assert, Ember.Handlebars + + var ComponentTemplateDeprecation = __dependency2__["default"]; + var TargetActionSupport = __dependency3__["default"]; + var View = __dependency4__.View;var get = __dependency5__.get; + var set = __dependency6__.set; + var isNone = __dependency7__.isNone; + + var computed = __dependency8__.computed; + + var a_slice = Array.prototype.slice; + + /** + @module ember + @submodule ember-views + */ + + /** + An `Ember.Component` is a view that is completely + isolated. Property access in its templates go + to the view object and actions are targeted at + the view object. There is no access to the + surrounding context or outer controller; all + contextual information must be passed in. + + The easiest way to create an `Ember.Component` is via + a template. If you name a template + `components/my-foo`, you will be able to use + `{{my-foo}}` in other templates, which will make + an instance of the isolated component. + + ```handlebars + {{app-profile person=currentUser}} + ``` + + ```handlebars + +

    {{person.title}}

    + +

    {{person.signature}}

    + ``` + + You can use `yield` inside a template to + include the **contents** of any block attached to + the component. The block will be executed in the + context of the surrounding context or outer controller: + + ```handlebars + {{#app-profile person=currentUser}} +

    Admin mode

    + {{! Executed in the controller's context. }} + {{/app-profile}} + ``` + + ```handlebars + +

    {{person.title}}

    + {{! Executed in the components context. }} + {{yield}} {{! block contents }} + ``` + + If you want to customize the component, in order to + handle events or actions, you implement a subclass + of `Ember.Component` named after the name of the + component. Note that `Component` needs to be appended to the name of + your subclass like `AppProfileComponent`. + + For example, you could implement the action + `hello` for the `app-profile` component: + + ```javascript + App.AppProfileComponent = Ember.Component.extend({ + actions: { + hello: function(name) { + console.log("Hello", name); + } + } + }); + ``` + + And then use it in the component's template: + + ```handlebars + + +

    {{person.title}}

    + {{yield}} + + + ``` + + Components must have a `-` in their name to avoid + conflicts with built-in controls that wrap HTML + elements. This is consistent with the same + requirement in web components. + + @class Component + @namespace Ember + @extends Ember.View + */ + var Component = View.extend(TargetActionSupport, ComponentTemplateDeprecation, { + instrumentName: 'component', + instrumentDisplay: computed(function() { + if (this._debugContainerKey) { + return '{{' + this._debugContainerKey.split(':')[1] + '}}'; + } + }), + + init: function() { + this._super(); + set(this, 'context', this); + set(this, 'controller', this); + }, + + defaultLayout: function(context, options){ + Ember.Handlebars.helpers['yield'].call(context, options); + }, + + /** + A components template property is set by passing a block + during its invocation. It is executed within the parent context. + + Example: + + ```handlebars + {{#my-component}} + // something that is run in the context + // of the parent context + {{/my-component}} + ``` + + Specifying a template directly to a component is deprecated without + also specifying the layout property. + + @deprecated + @property template + */ + template: computed(function(key, value) { + if (value !== undefined) { return value; } + + var templateName = get(this, 'templateName'), + template = this.templateForName(templateName, 'template'); + + Ember.assert("You specified the templateName " + templateName + " for " + this + ", but it did not exist.", !templateName || template); + + return template || get(this, 'defaultTemplate'); + }).property('templateName'), + + /** + Specifying a components `templateName` is deprecated without also + providing the `layout` or `layoutName` properties. + + @deprecated + @property templateName + */ + templateName: null, + + // during render, isolate keywords + cloneKeywords: function() { + return { + view: this, + controller: this + }; + }, + + _yield: function(context, options) { + var view = options.data.view, + parentView = this._parentView, + template = get(this, 'template'); + + if (template) { + Ember.assert("A Component must have a parent view in order to yield.", parentView); + + view.appendChild(View, { + isVirtual: true, + tagName: '', + _contextView: parentView, + template: template, + context: get(parentView, 'context'), + controller: get(parentView, 'controller'), + templateData: { keywords: parentView.cloneKeywords() } + }); + } + }, + + /** + If the component is currently inserted into the DOM of a parent view, this + property will point to the controller of the parent view. + + @property targetObject + @type Ember.Controller + @default null + */ + targetObject: computed(function(key) { + var parentView = get(this, '_parentView'); + return parentView ? get(parentView, 'controller') : null; + }).property('_parentView'), + + /** + Triggers a named action on the controller context where the component is used if + this controller has registered for notifications of the action. + + For example a component for playing or pausing music may translate click events + into action notifications of "play" or "stop" depending on some internal state + of the component: + + + ```javascript + App.PlayButtonComponent = Ember.Component.extend({ + click: function(){ + if (this.get('isPlaying')) { + this.sendAction('play'); + } else { + this.sendAction('stop'); + } + } + }); + ``` + + When used inside a template these component actions are configured to + trigger actions in the outer application context: + + ```handlebars + {{! application.hbs }} + {{play-button play="musicStarted" stop="musicStopped"}} + ``` + + When the component receives a browser `click` event it translate this + interaction into application-specific semantics ("play" or "stop") and + triggers the specified action name on the controller for the template + where the component is used: + + + ```javascript + App.ApplicationController = Ember.Controller.extend({ + actions: { + musicStarted: function(){ + // called when the play button is clicked + // and the music started playing + }, + musicStopped: function(){ + // called when the play button is clicked + // and the music stopped playing + } + } + }); + ``` + + If no action name is passed to `sendAction` a default name of "action" + is assumed. + + ```javascript + App.NextButtonComponent = Ember.Component.extend({ + click: function(){ + this.sendAction(); + } + }); + ``` + + ```handlebars + {{! application.hbs }} + {{next-button action="playNextSongInAlbum"}} + ``` + + ```javascript + App.ApplicationController = Ember.Controller.extend({ + actions: { + playNextSongInAlbum: function(){ + ... + } + } + }); + ``` + + @method sendAction + @param [action] {String} the action to trigger + @param [context] {*} a context to send with the action + */ + sendAction: function(action) { + var actionName, + contexts = a_slice.call(arguments, 1); + + // Send the default action + if (action === undefined) { + actionName = get(this, 'action'); + Ember.assert("The default action was triggered on the component " + this.toString() + + ", but the action name (" + actionName + ") was not a string.", + isNone(actionName) || typeof actionName === 'string'); + } else { + actionName = get(this, action); + Ember.assert("The " + action + " action was triggered on the component " + + this.toString() + ", but the action name (" + actionName + + ") was not a string.", + isNone(actionName) || typeof actionName === 'string'); + } + + // If no action name for that action could be found, just abort. + if (actionName === undefined) { return; } + + this.triggerAction({ + action: actionName, + actionContext: contexts + }); + } + }); + + __exports__["default"] = Component; + }); +define("ember-views/views/container_view", + ["ember-metal/core","ember-metal/merge","ember-runtime/mixins/mutable_array","ember-metal/property_get","ember-metal/property_set","ember-views/views/view","ember-views/views/states","ember-metal/error","ember-metal/enumerable_utils","ember-metal/computed","ember-metal/run_loop","ember-metal/properties","ember-views/system/render_buffer","ember-metal/mixin","ember-runtime/system/native_array","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __dependency11__, __dependency12__, __dependency13__, __dependency14__, __dependency15__, __exports__) { + "use strict"; + var Ember = __dependency1__["default"]; + // Ember.assert, Ember.K + + var merge = __dependency2__["default"]; + var MutableArray = __dependency3__["default"]; + var get = __dependency4__.get; + var set = __dependency5__.set; + + var View = __dependency6__.View; + var ViewCollection = __dependency6__.ViewCollection; + var cloneStates = __dependency7__.cloneStates; + var EmberViewStates = __dependency7__.states; + + var EmberError = __dependency8__["default"]; + + // ES6TODO: functions on EnumerableUtils should get their own export + var EnumerableUtils = __dependency9__["default"]; + var forEach = EnumerableUtils.forEach; + + var computed = __dependency10__.computed; + var run = __dependency11__["default"]; + var defineProperty = __dependency12__.defineProperty; + var RenderBuffer = __dependency13__["default"]; + var observer = __dependency14__.observer; + var beforeObserver = __dependency14__.beforeObserver; + var A = __dependency15__.A; + + /** + @module ember + @submodule ember-views + */ + + var states = cloneStates(EmberViewStates); + + /** + A `ContainerView` is an `Ember.View` subclass that implements `Ember.MutableArray` + allowing programmatic management of its child views. + + ## Setting Initial Child Views + + The initial array of child views can be set in one of two ways. You can + provide a `childViews` property at creation time that contains instance of + `Ember.View`: + + ```javascript + aContainer = Ember.ContainerView.create({ + childViews: [Ember.View.create(), Ember.View.create()] + }); + ``` + + You can also provide a list of property names whose values are instances of + `Ember.View`: + + ```javascript + aContainer = Ember.ContainerView.create({ + childViews: ['aView', 'bView', 'cView'], + aView: Ember.View.create(), + bView: Ember.View.create(), + cView: Ember.View.create() + }); + ``` + + The two strategies can be combined: + + ```javascript + aContainer = Ember.ContainerView.create({ + childViews: ['aView', Ember.View.create()], + aView: Ember.View.create() + }); + ``` + + Each child view's rendering will be inserted into the container's rendered + HTML in the same order as its position in the `childViews` property. + + ## Adding and Removing Child Views + + The container view implements `Ember.MutableArray` allowing programmatic management of its child views. + + To remove a view, pass that view into a `removeObject` call on the container view. + + Given an empty `` the following code + + ```javascript + aContainer = Ember.ContainerView.create({ + classNames: ['the-container'], + childViews: ['aView', 'bView'], + aView: Ember.View.create({ + template: Ember.Handlebars.compile("A") + }), + bView: Ember.View.create({ + template: Ember.Handlebars.compile("B") + }) + }); + + aContainer.appendTo('body'); + ``` + + Results in the HTML + + ```html +
    +
    A
    +
    B
    +
    + ``` + + Removing a view + + ```javascript + aContainer.toArray(); // [aContainer.aView, aContainer.bView] + aContainer.removeObject(aContainer.get('bView')); + aContainer.toArray(); // [aContainer.aView] + ``` + + Will result in the following HTML + + ```html +
    +
    A
    +
    + ``` + + Similarly, adding a child view is accomplished by adding `Ember.View` instances to the + container view. + + Given an empty `` the following code + + ```javascript + aContainer = Ember.ContainerView.create({ + classNames: ['the-container'], + childViews: ['aView', 'bView'], + aView: Ember.View.create({ + template: Ember.Handlebars.compile("A") + }), + bView: Ember.View.create({ + template: Ember.Handlebars.compile("B") + }) + }); + + aContainer.appendTo('body'); + ``` + + Results in the HTML + + ```html +
    +
    A
    +
    B
    +
    + ``` + + Adding a view + + ```javascript + AnotherViewClass = Ember.View.extend({ + template: Ember.Handlebars.compile("Another view") + }); + + aContainer.toArray(); // [aContainer.aView, aContainer.bView] + aContainer.pushObject(AnotherViewClass.create()); + aContainer.toArray(); // [aContainer.aView, aContainer.bView, ] + ``` + + Will result in the following HTML + + ```html +
    +
    A
    +
    B
    +
    Another view
    +
    + ``` + + ## Templates and Layout + + A `template`, `templateName`, `defaultTemplate`, `layout`, `layoutName` or + `defaultLayout` property on a container view will not result in the template + or layout being rendered. The HTML contents of a `Ember.ContainerView`'s DOM + representation will only be the rendered HTML of its child views. + + @class ContainerView + @namespace Ember + @extends Ember.View + */ + var ContainerView = View.extend(MutableArray, { + states: states, + + init: function() { + this._super(); + + var childViews = get(this, 'childViews'); + + // redefine view's childViews property that was obliterated + defineProperty(this, 'childViews', View.childViewsProperty); + + var _childViews = this._childViews; + + forEach(childViews, function(viewName, idx) { + var view; + + if ('string' === typeof viewName) { + view = get(this, viewName); + view = this.createChildView(view); + set(this, viewName, view); + } else { + view = this.createChildView(viewName); + } + + _childViews[idx] = view; + }, this); + + var currentView = get(this, 'currentView'); + if (currentView) { + if (!_childViews.length) { _childViews = this._childViews = this._childViews.slice(); } + _childViews.push(this.createChildView(currentView)); + } + }, + + replace: function(idx, removedCount, addedViews) { + var addedCount = addedViews ? get(addedViews, 'length') : 0; + var self = this; + Ember.assert("You can't add a child to a container - the child is already a child of another view", A(addedViews).every(function(item) { return !get(item, '_parentView') || get(item, '_parentView') === self; })); + + this.arrayContentWillChange(idx, removedCount, addedCount); + this.childViewsWillChange(this._childViews, idx, removedCount); + + if (addedCount === 0) { + this._childViews.splice(idx, removedCount) ; + } else { + var args = [idx, removedCount].concat(addedViews); + if (addedViews.length && !this._childViews.length) { this._childViews = this._childViews.slice(); } + this._childViews.splice.apply(this._childViews, args); + } + + this.arrayContentDidChange(idx, removedCount, addedCount); + this.childViewsDidChange(this._childViews, idx, removedCount, addedCount); + + return this; + }, + + objectAt: function(idx) { + return this._childViews[idx]; + }, + + length: computed(function () { + return this._childViews.length; + }).volatile(), + + /** + Instructs each child view to render to the passed render buffer. + + @private + @method render + @param {Ember.RenderBuffer} buffer the buffer to render to + */ + render: function(buffer) { + this.forEachChildView(function(view) { + view.renderToBuffer(buffer); + }); + }, + + instrumentName: 'container', + + /** + When a child view is removed, destroy its element so that + it is removed from the DOM. + + The array observer that triggers this action is set up in the + `renderToBuffer` method. + + @private + @method childViewsWillChange + @param {Ember.Array} views the child views array before mutation + @param {Number} start the start position of the mutation + @param {Number} removed the number of child views removed + **/ + childViewsWillChange: function(views, start, removed) { + this.propertyWillChange('childViews'); + + if (removed > 0) { + var changedViews = views.slice(start, start+removed); + // transition to preRender before clearing parentView + this.currentState.childViewsWillChange(this, views, start, removed); + this.initializeViews(changedViews, null, null); + } + }, + + removeChild: function(child) { + this.removeObject(child); + return this; + }, + + /** + When a child view is added, make sure the DOM gets updated appropriately. + + If the view has already rendered an element, we tell the child view to + create an element and insert it into the DOM. If the enclosing container + view has already written to a buffer, but not yet converted that buffer + into an element, we insert the string representation of the child into the + appropriate place in the buffer. + + @private + @method childViewsDidChange + @param {Ember.Array} views the array of child views after the mutation has occurred + @param {Number} start the start position of the mutation + @param {Number} removed the number of child views removed + @param {Number} added the number of child views added + */ + childViewsDidChange: function(views, start, removed, added) { + if (added > 0) { + var changedViews = views.slice(start, start+added); + this.initializeViews(changedViews, this, get(this, 'templateData')); + this.currentState.childViewsDidChange(this, views, start, added); + } + this.propertyDidChange('childViews'); + }, + + initializeViews: function(views, parentView, templateData) { + forEach(views, function(view) { + set(view, '_parentView', parentView); + + if (!view.container && parentView) { + set(view, 'container', parentView.container); + } + + if (!get(view, 'templateData')) { + set(view, 'templateData', templateData); + } + }); + }, + + currentView: null, + + _currentViewWillChange: beforeObserver('currentView', function() { + var currentView = get(this, 'currentView'); + if (currentView) { + currentView.destroy(); + } + }), + + _currentViewDidChange: observer('currentView', function() { + var currentView = get(this, 'currentView'); + if (currentView) { + Ember.assert("You tried to set a current view that already has a parent. Make sure you don't have multiple outlets in the same view.", !get(currentView, '_parentView')); + this.pushObject(currentView); + } + }), + + _ensureChildrenAreInDOM: function () { + this.currentState.ensureChildrenAreInDOM(this); + } + }); + + merge(states._default, { + childViewsWillChange: Ember.K, + childViewsDidChange: Ember.K, + ensureChildrenAreInDOM: Ember.K + }); + + merge(states.inBuffer, { + childViewsDidChange: function(parentView, views, start, added) { + throw new EmberError('You cannot modify child views while in the inBuffer state'); + } + }); + + merge(states.hasElement, { + childViewsWillChange: function(view, views, start, removed) { + for (var i=start; i + ``` + + ## HTML `class` Attribute + + The HTML `class` attribute of a view's tag can be set by providing a + `classNames` property that is set to an array of strings: + + ```javascript + MyView = Ember.View.extend({ + classNames: ['my-class', 'my-other-class'] + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    + ``` + + `class` attribute values can also be set by providing a `classNameBindings` + property set to an array of properties names for the view. The return value + of these properties will be added as part of the value for the view's `class` + attribute. These properties can be computed properties: + + ```javascript + MyView = Ember.View.extend({ + classNameBindings: ['propertyA', 'propertyB'], + propertyA: 'from-a', + propertyB: function() { + if (someLogic) { return 'from-b'; } + }.property() + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    + ``` + + If the value of a class name binding returns a boolean the property name + itself will be used as the class name if the property is true. The class name + will not be added if the value is `false` or `undefined`. + + ```javascript + MyView = Ember.View.extend({ + classNameBindings: ['hovered'], + hovered: true + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    + ``` + + When using boolean class name bindings you can supply a string value other + than the property name for use as the `class` HTML attribute by appending the + preferred value after a ":" character when defining the binding: + + ```javascript + MyView = Ember.View.extend({ + classNameBindings: ['awesome:so-very-cool'], + awesome: true + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    + ``` + + Boolean value class name bindings whose property names are in a + camelCase-style format will be converted to a dasherized format: + + ```javascript + MyView = Ember.View.extend({ + classNameBindings: ['isUrgent'], + isUrgent: true + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    + ``` + + Class name bindings can also refer to object values that are found by + traversing a path relative to the view itself: + + ```javascript + MyView = Ember.View.extend({ + classNameBindings: ['messages.empty'] + messages: Ember.Object.create({ + empty: true + }) + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    + ``` + + If you want to add a class name for a property which evaluates to true and + and a different class name if it evaluates to false, you can pass a binding + like this: + + ```javascript + // Applies 'enabled' class when isEnabled is true and 'disabled' when isEnabled is false + Ember.View.extend({ + classNameBindings: ['isEnabled:enabled:disabled'] + isEnabled: true + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    + ``` + + When isEnabled is `false`, the resulting HTML reprensentation looks like + this: + + ```html +
    + ``` + + This syntax offers the convenience to add a class if a property is `false`: + + ```javascript + // Applies no class when isEnabled is true and class 'disabled' when isEnabled is false + Ember.View.extend({ + classNameBindings: ['isEnabled::disabled'] + isEnabled: true + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    + ``` + + When the `isEnabled` property on the view is set to `false`, it will result + in view instances with an HTML representation of: + + ```html +
    + ``` + + Updates to the the value of a class name binding will result in automatic + update of the HTML `class` attribute in the view's rendered HTML + representation. If the value becomes `false` or `undefined` the class name + will be removed. + + Both `classNames` and `classNameBindings` are concatenated properties. See + [Ember.Object](/api/classes/Ember.Object.html) documentation for more + information about concatenated properties. + + ## HTML Attributes + + The HTML attribute section of a view's tag can be set by providing an + `attributeBindings` property set to an array of property names on the view. + The return value of these properties will be used as the value of the view's + HTML associated attribute: + + ```javascript + AnchorView = Ember.View.extend({ + tagName: 'a', + attributeBindings: ['href'], + href: 'http://google.com' + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html + + ``` + + One property can be mapped on to another by placing a ":" between + the source property and the destination property: + + ```javascript + AnchorView = Ember.View.extend({ + tagName: 'a', + attributeBindings: ['url:href'], + url: 'http://google.com' + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html + + ``` + + If the return value of an `attributeBindings` monitored property is a boolean + the property will follow HTML's pattern of repeating the attribute's name as + its value: + + ```javascript + MyTextInput = Ember.View.extend({ + tagName: 'input', + attributeBindings: ['disabled'], + disabled: true + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html + + ``` + + `attributeBindings` can refer to computed properties: + + ```javascript + MyTextInput = Ember.View.extend({ + tagName: 'input', + attributeBindings: ['disabled'], + disabled: function() { + if (someLogic) { + return true; + } else { + return false; + } + }.property() + }); + ``` + + Updates to the the property of an attribute binding will result in automatic + update of the HTML attribute in the view's rendered HTML representation. + + `attributeBindings` is a concatenated property. See [Ember.Object](/api/classes/Ember.Object.html) + documentation for more information about concatenated properties. + + ## Templates + + The HTML contents of a view's rendered representation are determined by its + template. Templates can be any function that accepts an optional context + parameter and returns a string of HTML that will be inserted within the + view's tag. Most typically in Ember this function will be a compiled + `Ember.Handlebars` template. + + ```javascript + AView = Ember.View.extend({ + template: Ember.Handlebars.compile('I am the template') + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    I am the template
    + ``` + + Within an Ember application is more common to define a Handlebars templates as + part of a page: + + ```html + + ``` + + And associate it by name using a view's `templateName` property: + + ```javascript + AView = Ember.View.extend({ + templateName: 'some-template' + }); + ``` + + If you have nested resources, your Handlebars template will look like this: + + ```html + + ``` + + And `templateName` property: + + ```javascript + AView = Ember.View.extend({ + templateName: 'posts/new' + }); + ``` + + Using a value for `templateName` that does not have a Handlebars template + with a matching `data-template-name` attribute will throw an error. + + For views classes that may have a template later defined (e.g. as the block + portion of a `{{view}}` Handlebars helper call in another template or in + a subclass), you can provide a `defaultTemplate` property set to compiled + template function. If a template is not later provided for the view instance + the `defaultTemplate` value will be used: + + ```javascript + AView = Ember.View.extend({ + defaultTemplate: Ember.Handlebars.compile('I was the default'), + template: null, + templateName: null + }); + ``` + + Will result in instances with an HTML representation of: + + ```html +
    I was the default
    + ``` + + If a `template` or `templateName` is provided it will take precedence over + `defaultTemplate`: + + ```javascript + AView = Ember.View.extend({ + defaultTemplate: Ember.Handlebars.compile('I was the default') + }); + + aView = AView.create({ + template: Ember.Handlebars.compile('I was the template, not default') + }); + ``` + + Will result in the following HTML representation when rendered: + + ```html +
    I was the template, not default
    + ``` + + ## View Context + + The default context of the compiled template is the view's controller: + + ```javascript + AView = Ember.View.extend({ + template: Ember.Handlebars.compile('Hello {{excitedGreeting}}') + }); + + aController = Ember.Object.create({ + firstName: 'Barry', + excitedGreeting: function() { + return this.get("content.firstName") + "!!!" + }.property() + }); + + aView = AView.create({ + controller: aController, + }); + ``` + + Will result in an HTML representation of: + + ```html +
    Hello Barry!!!
    + ``` + + A context can also be explicitly supplied through the view's `context` + property. If the view has neither `context` nor `controller` properties, the + `parentView`'s context will be used. + + ## Layouts + + Views can have a secondary template that wraps their main template. Like + primary templates, layouts can be any function that accepts an optional + context parameter and returns a string of HTML that will be inserted inside + view's tag. Views whose HTML element is self closing (e.g. ``) + cannot have a layout and this property will be ignored. + + Most typically in Ember a layout will be a compiled `Ember.Handlebars` + template. + + A view's layout can be set directly with the `layout` property or reference + an existing Handlebars template by name with the `layoutName` property. + + A template used as a layout must contain a single use of the Handlebars + `{{yield}}` helper. The HTML contents of a view's rendered `template` will be + inserted at this location: + + ```javascript + AViewWithLayout = Ember.View.extend({ + layout: Ember.Handlebars.compile("
    {{yield}}
    ") + template: Ember.Handlebars.compile("I got wrapped"), + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    +
    + I got wrapped +
    +
    + ``` + + See [Ember.Handlebars.helpers.yield](/api/classes/Ember.Handlebars.helpers.html#method_yield) + for more information. + + ## Responding to Browser Events + + Views can respond to user-initiated events in one of three ways: method + implementation, through an event manager, and through `{{action}}` helper use + in their template or layout. + + ### Method Implementation + + Views can respond to user-initiated events by implementing a method that + matches the event name. A `jQuery.Event` object will be passed as the + argument to this method. + + ```javascript + AView = Ember.View.extend({ + click: function(event) { + // will be called when when an instance's + // rendered element is clicked + } + }); + ``` + + ### Event Managers + + Views can define an object as their `eventManager` property. This object can + then implement methods that match the desired event names. Matching events + that occur on the view's rendered HTML or the rendered HTML of any of its DOM + descendants will trigger this method. A `jQuery.Event` object will be passed + as the first argument to the method and an `Ember.View` object as the + second. The `Ember.View` will be the view whose rendered HTML was interacted + with. This may be the view with the `eventManager` property or one of its + descendent views. + + ```javascript + AView = Ember.View.extend({ + eventManager: Ember.Object.create({ + doubleClick: function(event, view) { + // will be called when when an instance's + // rendered element or any rendering + // of this views's descendent + // elements is clicked + } + }) + }); + ``` + + An event defined for an event manager takes precedence over events of the + same name handled through methods on the view. + + ```javascript + AView = Ember.View.extend({ + mouseEnter: function(event) { + // will never trigger. + }, + eventManager: Ember.Object.create({ + mouseEnter: function(event, view) { + // takes precedence over AView#mouseEnter + } + }) + }); + ``` + + Similarly a view's event manager will take precedence for events of any views + rendered as a descendent. A method name that matches an event name will not + be called if the view instance was rendered inside the HTML representation of + a view that has an `eventManager` property defined that handles events of the + name. Events not handled by the event manager will still trigger method calls + on the descendent. + + ```javascript + OuterView = Ember.View.extend({ + template: Ember.Handlebars.compile("outer {{#view InnerView}}inner{{/view}} outer"), + eventManager: Ember.Object.create({ + mouseEnter: function(event, view) { + // view might be instance of either + // OuterView or InnerView depending on + // where on the page the user interaction occured + } + }) + }); + + InnerView = Ember.View.extend({ + click: function(event) { + // will be called if rendered inside + // an OuterView because OuterView's + // eventManager doesn't handle click events + }, + mouseEnter: function(event) { + // will never be called if rendered inside + // an OuterView. + } + }); + ``` + + ### Handlebars `{{action}}` Helper + + See [Handlebars.helpers.action](/api/classes/Ember.Handlebars.helpers.html#method_action). + + ### Event Names + + All of the event handling approaches described above respond to the same set + of events. The names of the built-in events are listed below. (The hash of + built-in events exists in `Ember.EventDispatcher`.) Additional, custom events + can be registered by using `Ember.Application.customEvents`. + + Touch events: + + * `touchStart` + * `touchMove` + * `touchEnd` + * `touchCancel` + + Keyboard events + + * `keyDown` + * `keyUp` + * `keyPress` + + Mouse events + + * `mouseDown` + * `mouseUp` + * `contextMenu` + * `click` + * `doubleClick` + * `mouseMove` + * `focusIn` + * `focusOut` + * `mouseEnter` + * `mouseLeave` + + Form events: + + * `submit` + * `change` + * `focusIn` + * `focusOut` + * `input` + + HTML5 drag and drop events: + + * `dragStart` + * `drag` + * `dragEnter` + * `dragLeave` + * `dragOver` + * `dragEnd` + * `drop` + + ## Handlebars `{{view}}` Helper + + Other `Ember.View` instances can be included as part of a view's template by + using the `{{view}}` Handlebars helper. See [Ember.Handlebars.helpers.view](/api/classes/Ember.Handlebars.helpers.html#method_view) + for additional information. + + @class View + @namespace Ember + @extends Ember.CoreView + */ + var View = CoreView.extend({ + + concatenatedProperties: ['classNames', 'classNameBindings', 'attributeBindings'], + + /** + @property isView + @type Boolean + @default true + @static + */ + isView: true, + + // .......................................................... + // TEMPLATE SUPPORT + // + + /** + The name of the template to lookup if no template is provided. + + By default `Ember.View` will lookup a template with this name in + `Ember.TEMPLATES` (a shared global object). + + @property templateName + @type String + @default null + */ + templateName: null, + + /** + The name of the layout to lookup if no layout is provided. + + By default `Ember.View` will lookup a template with this name in + `Ember.TEMPLATES` (a shared global object). + + @property layoutName + @type String + @default null + */ + layoutName: null, + + /** + Used to identify this view during debugging + + @property instrumentDisplay + @type String + */ + instrumentDisplay: computed(function() { + if (this.helperName) { + return '{{' + this.helperName + '}}'; + } + }), + + /** + The template used to render the view. This should be a function that + accepts an optional context parameter and returns a string of HTML that + will be inserted into the DOM relative to its parent view. + + In general, you should set the `templateName` property instead of setting + the template yourself. + + @property template + @type Function + */ + template: computed('templateName', function(key, value) { + if (value !== undefined) { return value; } + + var templateName = get(this, 'templateName'), + template = this.templateForName(templateName, 'template'); + + Ember.assert("You specified the templateName " + templateName + " for " + this + ", but it did not exist.", !templateName || template); + + return template || get(this, 'defaultTemplate'); + }), + + /** + The controller managing this view. If this property is set, it will be + made available for use by the template. + + @property controller + @type Object + */ + controller: computed('_parentView', function(key) { + var parentView = get(this, '_parentView'); + return parentView ? get(parentView, 'controller') : null; + }), + + /** + A view may contain a layout. A layout is a regular template but + supersedes the `template` property during rendering. It is the + responsibility of the layout template to retrieve the `template` + property from the view (or alternatively, call `Handlebars.helpers.yield`, + `{{yield}}`) to render it in the correct location. + + This is useful for a view that has a shared wrapper, but which delegates + the rendering of the contents of the wrapper to the `template` property + on a subclass. + + @property layout + @type Function + */ + layout: computed(function(key) { + var layoutName = get(this, 'layoutName'), + layout = this.templateForName(layoutName, 'layout'); + + Ember.assert("You specified the layoutName " + layoutName + " for " + this + ", but it did not exist.", !layoutName || layout); + + return layout || get(this, 'defaultLayout'); + }).property('layoutName'), + + _yield: function(context, options) { + var template = get(this, 'template'); + if (template) { template(context, options); } + }, + + templateForName: function(name, type) { + if (!name) { return; } + Ember.assert("templateNames are not allowed to contain periods: "+name, name.indexOf('.') === -1); + + // the defaultContainer is deprecated + var container = this.container || (Container && Container.defaultContainer); + return container && container.lookup('template:' + name); + }, + + /** + The object from which templates should access properties. + + This object will be passed to the template function each time the render + method is called, but it is up to the individual function to decide what + to do with it. + + By default, this will be the view's controller. + + @property context + @type Object + */ + context: computed(function(key, value) { + if (arguments.length === 2) { + set(this, '_context', value); + return value; + } else { + return get(this, '_context'); + } + }).volatile(), + + /** + Private copy of the view's template context. This can be set directly + by Handlebars without triggering the observer that causes the view + to be re-rendered. + + The context of a view is looked up as follows: + + 1. Supplied context (usually by Handlebars) + 2. Specified controller + 3. `parentView`'s context (for a child of a ContainerView) + + The code in Handlebars that overrides the `_context` property first + checks to see whether the view has a specified controller. This is + something of a hack and should be revisited. + + @property _context + @private + */ + _context: computed(function(key) { + var parentView, controller; + + if (controller = get(this, 'controller')) { + return controller; + } + + parentView = this._parentView; + if (parentView) { + return get(parentView, '_context'); + } + + return null; + }), + + /** + If a value that affects template rendering changes, the view should be + re-rendered to reflect the new value. + + @method _contextDidChange + @private + */ + _contextDidChange: observer('context', function() { + this.rerender(); + }), + + /** + If `false`, the view will appear hidden in DOM. + + @property isVisible + @type Boolean + @default null + */ + isVisible: true, + + /** + Array of child views. You should never edit this array directly. + Instead, use `appendChild` and `removeFromParent`. + + @property childViews + @type Array + @default [] + @private + */ + childViews: childViewsProperty, + + _childViews: EMPTY_ARRAY, + + // When it's a virtual view, we need to notify the parent that their + // childViews will change. + _childViewsWillChange: beforeObserver('childViews', function() { + if (this.isVirtual) { + var parentView = get(this, 'parentView'); + if (parentView) { propertyWillChange(parentView, 'childViews'); } + } + }), + + // When it's a virtual view, we need to notify the parent that their + // childViews did change. + _childViewsDidChange: observer('childViews', function() { + if (this.isVirtual) { + var parentView = get(this, 'parentView'); + if (parentView) { propertyDidChange(parentView, 'childViews'); } + } + }), + + /** + Return the nearest ancestor that is an instance of the provided + class. + + @method nearestInstanceOf + @param {Class} klass Subclass of Ember.View (or Ember.View itself) + @return Ember.View + @deprecated + */ + nearestInstanceOf: function(klass) { + Ember.deprecate("nearestInstanceOf is deprecated and will be removed from future releases. Use nearestOfType."); + var view = get(this, 'parentView'); + + while (view) { + if (view instanceof klass) { return view; } + view = get(view, 'parentView'); + } + }, + + /** + Return the nearest ancestor that is an instance of the provided + class or mixin. + + @method nearestOfType + @param {Class,Mixin} klass Subclass of Ember.View (or Ember.View itself), + or an instance of Ember.Mixin. + @return Ember.View + */ + nearestOfType: function(klass) { + var view = get(this, 'parentView'), + isOfType = klass instanceof Mixin ? + function(view) { return klass.detect(view); } : + function(view) { return klass.detect(view.constructor); }; + + while (view) { + if (isOfType(view)) { return view; } + view = get(view, 'parentView'); + } + }, + + /** + Return the nearest ancestor that has a given property. + + @method nearestWithProperty + @param {String} property A property name + @return Ember.View + */ + nearestWithProperty: function(property) { + var view = get(this, 'parentView'); + + while (view) { + if (property in view) { return view; } + view = get(view, 'parentView'); + } + }, + + /** + Return the nearest ancestor whose parent is an instance of + `klass`. + + @method nearestChildOf + @param {Class} klass Subclass of Ember.View (or Ember.View itself) + @return Ember.View + */ + nearestChildOf: function(klass) { + var view = get(this, 'parentView'); + + while (view) { + if (get(view, 'parentView') instanceof klass) { return view; } + view = get(view, 'parentView'); + } + }, + + /** + When the parent view changes, recursively invalidate `controller` + + @method _parentViewDidChange + @private + */ + _parentViewDidChange: observer('_parentView', function() { + if (this.isDestroying) { return; } + + this.trigger('parentViewDidChange'); + + if (get(this, 'parentView.controller') && !get(this, 'controller')) { + this.notifyPropertyChange('controller'); + } + }), + + _controllerDidChange: observer('controller', function() { + if (this.isDestroying) { return; } + + this.rerender(); + + this.forEachChildView(function(view) { + view.propertyDidChange('controller'); + }); + }), + + cloneKeywords: function() { + var templateData = get(this, 'templateData'); + + var keywords = templateData ? copy(templateData.keywords) : {}; + set(keywords, 'view', get(this, 'concreteView')); + set(keywords, '_view', this); + set(keywords, 'controller', get(this, 'controller')); + + return keywords; + }, + + /** + Called on your view when it should push strings of HTML into a + `Ember.RenderBuffer`. Most users will want to override the `template` + or `templateName` properties instead of this method. + + By default, `Ember.View` will look for a function in the `template` + property and invoke it with the value of `context`. The value of + `context` will be the view's controller unless you override it. + + @method render + @param {Ember.RenderBuffer} buffer The render buffer + */ + render: function(buffer) { + // If this view has a layout, it is the responsibility of the + // the layout to render the view's template. Otherwise, render the template + // directly. + var template = get(this, 'layout') || get(this, 'template'); + + if (template) { + var context = get(this, 'context'); + var keywords = this.cloneKeywords(); + var output; + + var data = { + view: this, + buffer: buffer, + isRenderData: true, + keywords: keywords, + insideGroup: get(this, 'templateData.insideGroup') + }; + + // Invoke the template with the provided template context, which + // is the view's controller by default. A hash of data is also passed that provides + // the template with access to the view and render buffer. + + Ember.assert('template must be a function. Did you mean to call Ember.Handlebars.compile("...") or specify templateName instead?', typeof template === 'function'); + // The template should write directly to the render buffer instead + // of returning a string. + output = template(context, { data: data }); + + // If the template returned a string instead of writing to the buffer, + // push the string onto the buffer. + if (output !== undefined) { buffer.push(output); } + } + }, + + /** + Renders the view again. This will work regardless of whether the + view is already in the DOM or not. If the view is in the DOM, the + rendering process will be deferred to give bindings a chance + to synchronize. + + If children were added during the rendering process using `appendChild`, + `rerender` will remove them, because they will be added again + if needed by the next `render`. + + In general, if the display of your view changes, you should modify + the DOM element directly instead of manually calling `rerender`, which can + be slow. + + @method rerender + */ + rerender: function() { + return this.currentState.rerender(this); + }, + + clearRenderedChildren: function() { + var lengthBefore = this.lengthBeforeRender, + lengthAfter = this.lengthAfterRender; + + // If there were child views created during the last call to render(), + // remove them under the assumption that they will be re-created when + // we re-render. + + // VIEW-TODO: Unit test this path. + var childViews = this._childViews; + for (var i=lengthAfter-1; i>=lengthBefore; i--) { + if (childViews[i]) { childViews[i].destroy(); } + } + }, + + /** + Iterates over the view's `classNameBindings` array, inserts the value + of the specified property into the `classNames` array, then creates an + observer to update the view's element if the bound property ever changes + in the future. + + @method _applyClassNameBindings + @private + */ + _applyClassNameBindings: function(classBindings) { + var classNames = this.classNames, + elem, newClass, dasherizedClass; + + // Loop through all of the configured bindings. These will be either + // property names ('isUrgent') or property paths relative to the view + // ('content.isUrgent') + a_forEach(classBindings, function(binding) { + + Ember.assert("classNameBindings must not have spaces in them. Multiple class name bindings can be provided as elements of an array, e.g. ['foo', ':bar']", binding.indexOf(' ') === -1); + + // Variable in which the old class value is saved. The observer function + // closes over this variable, so it knows which string to remove when + // the property changes. + var oldClass; + // Extract just the property name from bindings like 'foo:bar' + var parsedPath = View._parsePropertyPath(binding); + + // Set up an observer on the context. If the property changes, toggle the + // class name. + var observer = function() { + // Get the current value of the property + newClass = this._classStringForProperty(binding); + elem = this.$(); + + // If we had previously added a class to the element, remove it. + if (oldClass) { + elem.removeClass(oldClass); + // Also remove from classNames so that if the view gets rerendered, + // the class doesn't get added back to the DOM. + classNames.removeObject(oldClass); + } + + // If necessary, add a new class. Make sure we keep track of it so + // it can be removed in the future. + if (newClass) { + elem.addClass(newClass); + oldClass = newClass; + } else { + oldClass = null; + } + }; + + // Get the class name for the property at its current value + dasherizedClass = this._classStringForProperty(binding); + + if (dasherizedClass) { + // Ensure that it gets into the classNames array + // so it is displayed when we render. + a_addObject(classNames, dasherizedClass); + + // Save a reference to the class name so we can remove it + // if the observer fires. Remember that this variable has + // been closed over by the observer. + oldClass = dasherizedClass; + } + + this.registerObserver(this, parsedPath.path, observer); + // Remove className so when the view is rerendered, + // the className is added based on binding reevaluation + this.one('willClearRender', function() { + if (oldClass) { + classNames.removeObject(oldClass); + oldClass = null; + } + }); + + }, this); + }, + + _unspecifiedAttributeBindings: null, + + /** + Iterates through the view's attribute bindings, sets up observers for each, + then applies the current value of the attributes to the passed render buffer. + + @method _applyAttributeBindings + @param {Ember.RenderBuffer} buffer + @private + */ + _applyAttributeBindings: function(buffer, attributeBindings) { + var attributeValue, + unspecifiedAttributeBindings = this._unspecifiedAttributeBindings = this._unspecifiedAttributeBindings || {}; + + a_forEach(attributeBindings, function(binding) { + var split = binding.split(':'), + property = split[0], + attributeName = split[1] || property; + + if (property in this) { + this._setupAttributeBindingObservation(property, attributeName); + + // Determine the current value and add it to the render buffer + // if necessary. + attributeValue = get(this, property); + View.applyAttributeBindings(buffer, attributeName, attributeValue); + } else { + unspecifiedAttributeBindings[property] = attributeName; + } + }, this); + + // Lazily setup setUnknownProperty after attributeBindings are initially applied + this.setUnknownProperty = this._setUnknownProperty; + }, + + _setupAttributeBindingObservation: function(property, attributeName) { + var attributeValue, elem; + + // Create an observer to add/remove/change the attribute if the + // JavaScript property changes. + var observer = function() { + elem = this.$(); + + attributeValue = get(this, property); + + View.applyAttributeBindings(elem, attributeName, attributeValue); + }; + + this.registerObserver(this, property, observer); + }, + + /** + We're using setUnknownProperty as a hook to setup attributeBinding observers for + properties that aren't defined on a view at initialization time. + + Note: setUnknownProperty will only be called once for each property. + + @method setUnknownProperty + @param key + @param value + @private + */ + setUnknownProperty: null, // Gets defined after initialization by _applyAttributeBindings + + _setUnknownProperty: function(key, value) { + var attributeName = this._unspecifiedAttributeBindings && this._unspecifiedAttributeBindings[key]; + if (attributeName) { + this._setupAttributeBindingObservation(key, attributeName); + } + + defineProperty(this, key); + return set(this, key, value); + }, + + /** + Given a property name, returns a dasherized version of that + property name if the property evaluates to a non-falsy value. + + For example, if the view has property `isUrgent` that evaluates to true, + passing `isUrgent` to this method will return `"is-urgent"`. + + @method _classStringForProperty + @param property + @private + */ + _classStringForProperty: function(property) { + var parsedPath = View._parsePropertyPath(property); + var path = parsedPath.path; + + var val = get(this, path); + if (val === undefined && isGlobalPath(path)) { + val = get(Ember.lookup, path); + } + + return View._classStringForValue(path, val, parsedPath.className, parsedPath.falsyClassName); + }, + + // .......................................................... + // ELEMENT SUPPORT + // + + /** + Returns the current DOM element for the view. + + @property element + @type DOMElement + */ + element: computed('_parentView', function(key, value) { + if (value !== undefined) { + return this.currentState.setElement(this, value); + } else { + return this.currentState.getElement(this); + } + }), + + /** + Returns a jQuery object for this view's element. If you pass in a selector + string, this method will return a jQuery object, using the current element + as its buffer. + + For example, calling `view.$('li')` will return a jQuery object containing + all of the `li` elements inside the DOM element of this view. + + @method $ + @param {String} [selector] a jQuery-compatible selector string + @return {jQuery} the jQuery object for the DOM node + */ + $: function(sel) { + return this.currentState.$(this, sel); + }, + + mutateChildViews: function(callback) { + var childViews = this._childViews, + idx = childViews.length, + view; + + while(--idx >= 0) { + view = childViews[idx]; + callback(this, view, idx); + } + + return this; + }, + + forEachChildView: function(callback) { + var childViews = this._childViews; + + if (!childViews) { return this; } + + var len = childViews.length, + view, idx; + + for (idx = 0; idx < len; idx++) { + view = childViews[idx]; + callback(view); + } + + return this; + }, + + /** + Appends the view's element to the specified parent element. + + If the view does not have an HTML representation yet, `createElement()` + will be called automatically. + + Note that this method just schedules the view to be appended; the DOM + element will not be appended to the given element until all bindings have + finished synchronizing. + + This is not typically a function that you will need to call directly when + building your application. You might consider using `Ember.ContainerView` + instead. If you do need to use `appendTo`, be sure that the target element + you are providing is associated with an `Ember.Application` and does not + have an ancestor element that is associated with an Ember view. + + @method appendTo + @param {String|DOMElement|jQuery} A selector, element, HTML string, or jQuery object + @return {Ember.View} receiver + */ + appendTo: function(target) { + // Schedule the DOM element to be created and appended to the given + // element after bindings have synchronized. + this._insertElementLater(function() { + Ember.assert("You tried to append to (" + target + ") but that isn't in the DOM", jQuery(target).length > 0); + Ember.assert("You cannot append to an existing Ember.View. Consider using Ember.ContainerView instead.", !jQuery(target).is('.ember-view') && !jQuery(target).parents().is('.ember-view')); + this.$().appendTo(target); + }); + + return this; + }, + + /** + Replaces the content of the specified parent element with this view's + element. If the view does not have an HTML representation yet, + `createElement()` will be called automatically. + + Note that this method just schedules the view to be appended; the DOM + element will not be appended to the given element until all bindings have + finished synchronizing + + @method replaceIn + @param {String|DOMElement|jQuery} target A selector, element, HTML string, or jQuery object + @return {Ember.View} received + */ + replaceIn: function(target) { + Ember.assert("You tried to replace in (" + target + ") but that isn't in the DOM", jQuery(target).length > 0); + Ember.assert("You cannot replace an existing Ember.View. Consider using Ember.ContainerView instead.", !jQuery(target).is('.ember-view') && !jQuery(target).parents().is('.ember-view')); + + this._insertElementLater(function() { + jQuery(target).empty(); + this.$().appendTo(target); + }); + + return this; + }, + + /** + Schedules a DOM operation to occur during the next render phase. This + ensures that all bindings have finished synchronizing before the view is + rendered. + + To use, pass a function that performs a DOM operation. + + Before your function is called, this view and all child views will receive + the `willInsertElement` event. After your function is invoked, this view + and all of its child views will receive the `didInsertElement` event. + + ```javascript + view._insertElementLater(function() { + this.createElement(); + this.$().appendTo('body'); + }); + ``` + + @method _insertElementLater + @param {Function} fn the function that inserts the element into the DOM + @private + */ + _insertElementLater: function(fn) { + this._scheduledInsert = run.scheduleOnce('render', this, '_insertElement', fn); + }, + + _insertElement: function (fn) { + this._scheduledInsert = null; + this.currentState.insertElement(this, fn); + }, + + /** + Appends the view's element to the document body. If the view does + not have an HTML representation yet, `createElement()` will be called + automatically. + + If your application uses the `rootElement` property, you must append + the view within that element. Rendering views outside of the `rootElement` + is not supported. + + Note that this method just schedules the view to be appended; the DOM + element will not be appended to the document body until all bindings have + finished synchronizing. + + @method append + @return {Ember.View} receiver + */ + append: function() { + return this.appendTo(document.body); + }, + + /** + Removes the view's element from the element to which it is attached. + + @method remove + @return {Ember.View} receiver + */ + remove: function() { + // What we should really do here is wait until the end of the run loop + // to determine if the element has been re-appended to a different + // element. + // In the interim, we will just re-render if that happens. It is more + // important than elements get garbage collected. + if (!this.removedFromDOM) { this.destroyElement(); } + this.invokeRecursively(function(view) { + if (view.clearRenderedChildren) { view.clearRenderedChildren(); } + }); + }, + + elementId: null, + + /** + Attempts to discover the element in the parent element. The default + implementation looks for an element with an ID of `elementId` (or the + view's guid if `elementId` is null). You can override this method to + provide your own form of lookup. For example, if you want to discover your + element using a CSS class name instead of an ID. + + @method findElementInParentElement + @param {DOMElement} parentElement The parent's DOM element + @return {DOMElement} The discovered element + */ + findElementInParentElement: function(parentElem) { + var id = "#" + this.elementId; + return jQuery(id)[0] || jQuery(id, parentElem)[0]; + }, + + /** + Creates a DOM representation of the view and all of its + child views by recursively calling the `render()` method. + + After the element has been created, `didInsertElement` will + be called on this view and all of its child views. + + @method createElement + @return {Ember.View} receiver + */ + createElement: function() { + if (get(this, 'element')) { return this; } + + var buffer = this.renderToBuffer(); + set(this, 'element', buffer.element()); + + return this; + }, + + /** + Called when a view is going to insert an element into the DOM. + + @event willInsertElement + */ + willInsertElement: Ember.K, + + /** + Called when the element of the view has been inserted into the DOM + or after the view was re-rendered. Override this function to do any + set up that requires an element in the document body. + + @event didInsertElement + */ + didInsertElement: Ember.K, + + /** + Called when the view is about to rerender, but before anything has + been torn down. This is a good opportunity to tear down any manual + observers you have installed based on the DOM state + + @event willClearRender + */ + willClearRender: Ember.K, + + /** + Run this callback on the current view (unless includeSelf is false) and recursively on child views. + + @method invokeRecursively + @param fn {Function} + @param includeSelf {Boolean} Includes itself if true. + @private + */ + invokeRecursively: function(fn, includeSelf) { + var childViews = (includeSelf === false) ? this._childViews : [this]; + var currentViews, view, currentChildViews; + + while (childViews.length) { + currentViews = childViews.slice(); + childViews = []; + + for (var i=0, l=currentViews.length; i` tag for views. + + @property tagName + @type String + @default null + */ + + // We leave this null by default so we can tell the difference between + // the default case and a user-specified tag. + tagName: null, + + /** + The WAI-ARIA role of the control represented by this view. For example, a + button may have a role of type 'button', or a pane may have a role of + type 'alertdialog'. This property is used by assistive software to help + visually challenged users navigate rich web applications. + + The full list of valid WAI-ARIA roles is available at: + [http://www.w3.org/TR/wai-aria/roles#roles_categorization](http://www.w3.org/TR/wai-aria/roles#roles_categorization) + + @property ariaRole + @type String + @default null + */ + ariaRole: null, + + /** + Standard CSS class names to apply to the view's outer element. This + property automatically inherits any class names defined by the view's + superclasses as well. + + @property classNames + @type Array + @default ['ember-view'] + */ + classNames: ['ember-view'], + + /** + A list of properties of the view to apply as class names. If the property + is a string value, the value of that string will be applied as a class + name. + + ```javascript + // Applies the 'high' class to the view element + Ember.View.extend({ + classNameBindings: ['priority'] + priority: 'high' + }); + ``` + + If the value of the property is a Boolean, the name of that property is + added as a dasherized class name. + + ```javascript + // Applies the 'is-urgent' class to the view element + Ember.View.extend({ + classNameBindings: ['isUrgent'] + isUrgent: true + }); + ``` + + If you would prefer to use a custom value instead of the dasherized + property name, you can pass a binding like this: + + ```javascript + // Applies the 'urgent' class to the view element + Ember.View.extend({ + classNameBindings: ['isUrgent:urgent'] + isUrgent: true + }); + ``` + + This list of properties is inherited from the view's superclasses as well. + + @property classNameBindings + @type Array + @default [] + */ + classNameBindings: EMPTY_ARRAY, + + /** + A list of properties of the view to apply as attributes. If the property is + a string value, the value of that string will be applied as the attribute. + + ```javascript + // Applies the type attribute to the element + // with the value "button", like
    + Ember.View.extend({ + attributeBindings: ['type'], + type: 'button' + }); + ``` + + If the value of the property is a Boolean, the name of that property is + added as an attribute. + + ```javascript + // Renders something like
    + Ember.View.extend({ + attributeBindings: ['enabled'], + enabled: true + }); + ``` + + @property attributeBindings + */ + attributeBindings: EMPTY_ARRAY, + + // ....................................................... + // CORE DISPLAY METHODS + // + + /** + Setup a view, but do not finish waking it up. + + * configure `childViews` + * register the view with the global views hash, which is used for event + dispatch + + @method init + @private + */ + init: function() { + this.elementId = this.elementId || guidFor(this); + + this._super(); + + // setup child views. be sure to clone the child views array first + this._childViews = this._childViews.slice(); + + Ember.assert("Only arrays are allowed for 'classNameBindings'", typeOf(this.classNameBindings) === 'array'); + this.classNameBindings = A(this.classNameBindings.slice()); + + Ember.assert("Only arrays are allowed for 'classNames'", typeOf(this.classNames) === 'array'); + this.classNames = A(this.classNames.slice()); + }, + + appendChild: function(view, options) { + return this.currentState.appendChild(this, view, options); + }, + + /** + Removes the child view from the parent view. + + @method removeChild + @param {Ember.View} view + @return {Ember.View} receiver + */ + removeChild: function(view) { + // If we're destroying, the entire subtree will be + // freed, and the DOM will be handled separately, + // so no need to mess with childViews. + if (this.isDestroying) { return; } + + // update parent node + set(view, '_parentView', null); + + // remove view from childViews array. + var childViews = this._childViews; + + a_removeObject(childViews, view); + + this.propertyDidChange('childViews'); // HUH?! what happened to will change? + + return this; + }, + + /** + Removes all children from the `parentView`. + + @method removeAllChildren + @return {Ember.View} receiver + */ + removeAllChildren: function() { + return this.mutateChildViews(function(parentView, view) { + parentView.removeChild(view); + }); + }, + + destroyAllChildren: function() { + return this.mutateChildViews(function(parentView, view) { + view.destroy(); + }); + }, + + /** + Removes the view from its `parentView`, if one is found. Otherwise + does nothing. + + @method removeFromParent + @return {Ember.View} receiver + */ + removeFromParent: function() { + var parent = this._parentView; + + // Remove DOM element from parent + this.remove(); + + if (parent) { parent.removeChild(this); } + return this; + }, + + /** + You must call `destroy` on a view to destroy the view (and all of its + child views). This will remove the view from any parent node, then make + sure that the DOM element managed by the view can be released by the + memory manager. + + @method destroy + */ + destroy: function() { + var childViews = this._childViews, + // get parentView before calling super because it'll be destroyed + nonVirtualParentView = get(this, 'parentView'), + viewName = this.viewName, + childLen, i; + + if (!this._super()) { return; } + + childLen = childViews.length; + for (i=childLen-1; i>=0; i--) { + childViews[i].removedFromDOM = true; + } + + // remove from non-virtual parent view if viewName was specified + if (viewName && nonVirtualParentView) { + nonVirtualParentView.set(viewName, null); + } + + childLen = childViews.length; + for (i=childLen-1; i>=0; i--) { + childViews[i].destroy(); + } + + return this; + }, + + /** + Instantiates a view to be added to the childViews array during view + initialization. You generally will not call this method directly unless + you are overriding `createChildViews()`. Note that this method will + automatically configure the correct settings on the new view instance to + act as a child of the parent. + + @method createChildView + @param {Class|String} viewClass + @param {Hash} [attrs] Attributes to add + @return {Ember.View} new instance + */ + createChildView: function(view, attrs) { + if (!view) { + throw new TypeError("createChildViews first argument must exist"); + } + + if (view.isView && view._parentView === this && view.container === this.container) { + return view; + } + + attrs = attrs || {}; + attrs._parentView = this; + + if (CoreView.detect(view)) { + attrs.templateData = attrs.templateData || get(this, 'templateData'); + + attrs.container = this.container; + view = view.create(attrs); + + // don't set the property on a virtual view, as they are invisible to + // consumers of the view API + if (view.viewName) { + set(get(this, 'concreteView'), view.viewName, view); + } + } else if ('string' === typeof view) { + var fullName = 'view:' + view; + var ViewKlass = this.container.lookupFactory(fullName); + + Ember.assert("Could not find view: '" + fullName + "'", !!ViewKlass); + + attrs.templateData = get(this, 'templateData'); + view = ViewKlass.create(attrs); + } else { + Ember.assert('You must pass instance or subclass of View', view.isView); + attrs.container = this.container; + + if (!get(view, 'templateData')) { + attrs.templateData = get(this, 'templateData'); + } + + setProperties(view, attrs); + + } + + return view; + }, + + becameVisible: Ember.K, + becameHidden: Ember.K, + + /** + When the view's `isVisible` property changes, toggle the visibility + element of the actual DOM element. + + @method _isVisibleDidChange + @private + */ + _isVisibleDidChange: observer('isVisible', function() { + if (this._isVisible === get(this, 'isVisible')) { return ; } + run.scheduleOnce('render', this, this._toggleVisibility); + }), + + _toggleVisibility: function() { + var $el = this.$(); + if (!$el) { return; } + + var isVisible = get(this, 'isVisible'); + + if (this._isVisible === isVisible) { return ; } + + $el.toggle(isVisible); + + this._isVisible = isVisible; + + if (this._isAncestorHidden()) { return; } + + if (isVisible) { + this._notifyBecameVisible(); + } else { + this._notifyBecameHidden(); + } + }, + + _notifyBecameVisible: function() { + this.trigger('becameVisible'); + + this.forEachChildView(function(view) { + var isVisible = get(view, 'isVisible'); + + if (isVisible || isVisible === null) { + view._notifyBecameVisible(); + } + }); + }, + + _notifyBecameHidden: function() { + this.trigger('becameHidden'); + this.forEachChildView(function(view) { + var isVisible = get(view, 'isVisible'); + + if (isVisible || isVisible === null) { + view._notifyBecameHidden(); + } + }); + }, + + _isAncestorHidden: function() { + var parent = get(this, 'parentView'); + + while (parent) { + if (get(parent, 'isVisible') === false) { return true; } + + parent = get(parent, 'parentView'); + } + + return false; + }, + + clearBuffer: function() { + this.invokeRecursively(nullViewsBuffer); + }, + + transitionTo: function(state, children) { + var priorState = this.currentState, + currentState = this.currentState = this.states[state]; + this.state = state; + + if (priorState && priorState.exit) { priorState.exit(this); } + if (currentState.enter) { currentState.enter(this); } + if (state === 'inDOM') { meta(this).cache.element = undefined; } + + if (children !== false) { + this.forEachChildView(function(view) { + view.transitionTo(state); + }); + } + }, + + // ....................................................... + // EVENT HANDLING + // + + /** + Handle events from `Ember.EventDispatcher` + + @method handleEvent + @param eventName {String} + @param evt {Event} + @private + */ + handleEvent: function(eventName, evt) { + return this.currentState.handleEvent(this, eventName, evt); + }, + + registerObserver: function(root, path, target, observer) { + if (!observer && 'function' === typeof target) { + observer = target; + target = null; + } + + if (!root || typeof root !== 'object') { + return; + } + + var view = this, + stateCheckedObserver = function() { + view.currentState.invokeObserver(this, observer); + }, + scheduledObserver = function() { + run.scheduleOnce('render', this, stateCheckedObserver); + }; + + addObserver(root, path, target, scheduledObserver); + + this.one('willClearRender', function() { + removeObserver(root, path, target, scheduledObserver); + }); + } + + }); + + /* + Describe how the specified actions should behave in the various + states that a view can exist in. Possible states: + + * preRender: when a view is first instantiated, and after its + element was destroyed, it is in the preRender state + * inBuffer: once a view has been rendered, but before it has + been inserted into the DOM, it is in the inBuffer state + * hasElement: the DOM representation of the view is created, + and is ready to be inserted + * inDOM: once a view has been inserted into the DOM it is in + the inDOM state. A view spends the vast majority of its + existence in this state. + * destroyed: once a view has been destroyed (using the destroy + method), it is in this state. No further actions can be invoked + on a destroyed view. + */ + + // in the destroyed state, everything is illegal + + // before rendering has begun, all legal manipulations are noops. + + // inside the buffer, legal manipulations are done on the buffer + + // once the view has been inserted into the DOM, legal manipulations + // are done on the DOM element. + + function notifyMutationListeners() { + run.once(View, 'notifyMutationListeners'); + } + + var DOMManager = { + prepend: function(view, html) { + view.$().prepend(html); + notifyMutationListeners(); + }, + + after: function(view, html) { + view.$().after(html); + notifyMutationListeners(); + }, + + html: function(view, html) { + view.$().html(html); + notifyMutationListeners(); + }, + + replace: function(view) { + var element = get(view, 'element'); + + set(view, 'element', null); + + view._insertElementLater(function() { + jQuery(element).replaceWith(get(view, 'element')); + notifyMutationListeners(); + }); + }, + + remove: function(view) { + view.$().remove(); + notifyMutationListeners(); + }, + + empty: function(view) { + view.$().empty(); + notifyMutationListeners(); + } + }; + + View.reopen({ + domManager: DOMManager + }); + + View.reopenClass({ + + /** + Parse a path and return an object which holds the parsed properties. + + For example a path like "content.isEnabled:enabled:disabled" will return the + following object: + + ```javascript + { + path: "content.isEnabled", + className: "enabled", + falsyClassName: "disabled", + classNames: ":enabled:disabled" + } + ``` + + @method _parsePropertyPath + @static + @private + */ + _parsePropertyPath: function(path) { + var split = path.split(':'), + propertyPath = split[0], + classNames = "", + className, + falsyClassName; + + // check if the property is defined as prop:class or prop:trueClass:falseClass + if (split.length > 1) { + className = split[1]; + if (split.length === 3) { falsyClassName = split[2]; } + + classNames = ':' + className; + if (falsyClassName) { classNames += ":" + falsyClassName; } + } + + return { + path: propertyPath, + classNames: classNames, + className: (className === '') ? undefined : className, + falsyClassName: falsyClassName + }; + }, + + /** + Get the class name for a given value, based on the path, optional + `className` and optional `falsyClassName`. + + - if a `className` or `falsyClassName` has been specified: + - if the value is truthy and `className` has been specified, + `className` is returned + - if the value is falsy and `falsyClassName` has been specified, + `falsyClassName` is returned + - otherwise `null` is returned + - if the value is `true`, the dasherized last part of the supplied path + is returned + - if the value is not `false`, `undefined` or `null`, the `value` + is returned + - if none of the above rules apply, `null` is returned + + @method _classStringForValue + @param path + @param val + @param className + @param falsyClassName + @static + @private + */ + _classStringForValue: function(path, val, className, falsyClassName) { + if(isArray(val)) { + val = get(val, 'length') !== 0; + } + + // When using the colon syntax, evaluate the truthiness or falsiness + // of the value to determine which className to return + if (className || falsyClassName) { + if (className && !!val) { + return className; + + } else if (falsyClassName && !val) { + return falsyClassName; + + } else { + return null; + } + + // If value is a Boolean and true, return the dasherized property + // name. + } else if (val === true) { + // Normalize property path to be suitable for use + // as a class name. For exaple, content.foo.barBaz + // becomes bar-baz. + var parts = path.split('.'); + return dasherize(parts[parts.length-1]); + + // If the value is not false, undefined, or null, return the current + // value of the property. + } else if (val !== false && val != null) { + return val; + + // Nothing to display. Return null so that the old class is removed + // but no new class is added. + } else { + return null; + } + } + }); + + var mutation = EmberObject.extend(Evented).create(); + + View.addMutationListener = function(callback) { + mutation.on('change', callback); + }; + + View.removeMutationListener = function(callback) { + mutation.off('change', callback); + }; + + View.notifyMutationListeners = function() { + mutation.trigger('change'); + }; + + /** + Global views hash + + @property views + @static + @type Hash + */ + View.views = {}; + + // If someone overrides the child views computed property when + // defining their class, we want to be able to process the user's + // supplied childViews and then restore the original computed property + // at view initialization time. This happens in Ember.ContainerView's init + // method. + View.childViewsProperty = childViewsProperty; + + View.applyAttributeBindings = function(elem, name, value) { + var type = typeOf(value); + + // if this changes, also change the logic in ember-handlebars/lib/helpers/binding.js + if (name !== 'value' && (type === 'string' || (type === 'number' && !isNaN(value)))) { + if (value !== elem.attr(name)) { + elem.attr(name, value); + } + } else if (name === 'value' || type === 'boolean') { + if (isNone(value) || value === false) { + // `null`, `undefined` or `false` should remove attribute + elem.removeAttr(name); + elem.prop(name, ''); + } else if (value !== elem.prop(name)) { + // value should always be properties + elem.prop(name, value); + } + } else if (!value) { + elem.removeAttr(name); + } + }; + + __exports__.CoreView = CoreView; + __exports__.View = View; + __exports__.ViewCollection = ViewCollection; + }); +})(); + +(function() { +define("metamorph", + [], + function() { + "use strict"; + // ========================================================================== + // Project: metamorph + // Copyright: ©2014 Tilde, Inc. All rights reserved. + // ========================================================================== + + var K = function() {}, + guid = 0, + disableRange = (function(){ + if ('undefined' !== typeof MetamorphENV) { + return MetamorphENV.DISABLE_RANGE_API; + } else if ('undefined' !== ENV) { + return ENV.DISABLE_RANGE_API; + } else { + return false; + } + })(), + + // Feature-detect the W3C range API, the extended check is for IE9 which only partially supports ranges + supportsRange = (!disableRange) && typeof document !== 'undefined' && ('createRange' in document) && (typeof Range !== 'undefined') && Range.prototype.createContextualFragment, + + // Internet Explorer prior to 9 does not allow setting innerHTML if the first element + // is a "zero-scope" element. This problem can be worked around by making + // the first node an invisible text node. We, like Modernizr, use ­ + needsShy = typeof document !== 'undefined' && (function() { + var testEl = document.createElement('div'); + testEl.innerHTML = "
    "; + testEl.firstChild.innerHTML = ""; + return testEl.firstChild.innerHTML === ''; + })(), + + + // IE 8 (and likely earlier) likes to move whitespace preceeding + // a script tag to appear after it. This means that we can + // accidentally remove whitespace when updating a morph. + movesWhitespace = document && (function() { + var testEl = document.createElement('div'); + testEl.innerHTML = "Test: Value"; + return testEl.childNodes[0].nodeValue === 'Test:' && + testEl.childNodes[2].nodeValue === ' Value'; + })(); + + // Constructor that supports either Metamorph('foo') or new + // Metamorph('foo'); + // + // Takes a string of HTML as the argument. + + var Metamorph = function(html) { + var self; + + if (this instanceof Metamorph) { + self = this; + } else { + self = new K(); + } + + self.innerHTML = html; + var myGuid = 'metamorph-'+(guid++); + self.start = myGuid + '-start'; + self.end = myGuid + '-end'; + + return self; + }; + + K.prototype = Metamorph.prototype; + + var rangeFor, htmlFunc, removeFunc, outerHTMLFunc, appendToFunc, afterFunc, prependFunc, startTagFunc, endTagFunc; + + outerHTMLFunc = function() { + return this.startTag() + this.innerHTML + this.endTag(); + }; + + startTagFunc = function() { + /* + * We replace chevron by its hex code in order to prevent escaping problems. + * Check this thread for more explaination: + * http://stackoverflow.com/questions/8231048/why-use-x3c-instead-of-when-generating-html-from-javascript + */ + return "hi"; + * div.firstChild.firstChild.tagName //=> "" + * + * If our script markers are inside such a node, we need to find that + * node and use *it* as the marker. + */ + var realNode = function(start) { + while (start.parentNode.tagName === "") { + start = start.parentNode; + } + + return start; + }; + + /* + * When automatically adding a tbody, Internet Explorer inserts the + * tbody immediately before the first . Other browsers create it + * before the first node, no matter what. + * + * This means the the following code: + * + * div = document.createElement("div"); + * div.innerHTML = "
    hi
    + * + * Generates the following DOM in IE: + * + * + div + * + table + * - script id='first' + * + tbody + * + tr + * + td + * - "hi" + * - script id='last' + * + * Which means that the two script tags, even though they were + * inserted at the same point in the hierarchy in the original + * HTML, now have different parents. + * + * This code reparents the first script tag by making it the tbody's + * first child. + * + */ + var fixParentage = function(start, end) { + if (start.parentNode !== end.parentNode) { + end.parentNode.insertBefore(start, end.parentNode.firstChild); + } + }; + + htmlFunc = function(html, outerToo) { + // get the real starting node. see realNode for details. + var start = realNode(document.getElementById(this.start)); + var end = document.getElementById(this.end); + var parentNode = end.parentNode; + var node, nextSibling, last; + + // make sure that the start and end nodes share the same + // parent. If not, fix it. + fixParentage(start, end); + + // remove all of the nodes after the starting placeholder and + // before the ending placeholder. + node = start.nextSibling; + while (node) { + nextSibling = node.nextSibling; + last = node === end; + + // if this is the last node, and we want to remove it as well, + // set the `end` node to the next sibling. This is because + // for the rest of the function, we insert the new nodes + // before the end (note that insertBefore(node, null) is + // the same as appendChild(node)). + // + // if we do not want to remove it, just break. + if (last) { + if (outerToo) { end = node.nextSibling; } else { break; } + } + + node.parentNode.removeChild(node); + + // if this is the last node and we didn't break before + // (because we wanted to remove the outer nodes), break + // now. + if (last) { break; } + + node = nextSibling; + } + + // get the first node for the HTML string, even in cases like + // tables and lists where a simple innerHTML on a div would + // swallow some of the content. + node = firstNodeFor(start.parentNode, html); + + if (outerToo) { + start.parentNode.removeChild(start); + } + + // copy the nodes for the HTML between the starting and ending + // placeholder. + while (node) { + nextSibling = node.nextSibling; + parentNode.insertBefore(node, end); + node = nextSibling; + } + }; + + // remove the nodes in the DOM representing this metamorph. + // + // this includes the starting and ending placeholders. + removeFunc = function() { + var start = realNode(document.getElementById(this.start)); + var end = document.getElementById(this.end); + + this.html(''); + start.parentNode.removeChild(start); + end.parentNode.removeChild(end); + }; + + appendToFunc = function(parentNode) { + var node = firstNodeFor(parentNode, this.outerHTML()); + var nextSibling; + + while (node) { + nextSibling = node.nextSibling; + parentNode.appendChild(node); + node = nextSibling; + } + }; + + afterFunc = function(html) { + // get the real starting node. see realNode for details. + var end = document.getElementById(this.end); + var insertBefore = end.nextSibling; + var parentNode = end.parentNode; + var nextSibling; + var node; + + // get the first node for the HTML string, even in cases like + // tables and lists where a simple innerHTML on a div would + // swallow some of the content. + node = firstNodeFor(parentNode, html); + + // copy the nodes for the HTML between the starting and ending + // placeholder. + while (node) { + nextSibling = node.nextSibling; + parentNode.insertBefore(node, insertBefore); + node = nextSibling; + } + }; + + prependFunc = function(html) { + var start = document.getElementById(this.start); + var parentNode = start.parentNode; + var nextSibling; + var node; + + node = firstNodeFor(parentNode, html); + var insertBefore = start.nextSibling; + + while (node) { + nextSibling = node.nextSibling; + parentNode.insertBefore(node, insertBefore); + node = nextSibling; + } + }; + } + + Metamorph.prototype.html = function(html) { + this.checkRemoved(); + if (html === undefined) { return this.innerHTML; } + + htmlFunc.call(this, html); + + this.innerHTML = html; + }; + + Metamorph.prototype.replaceWith = function(html) { + this.checkRemoved(); + htmlFunc.call(this, html, true); + }; + + Metamorph.prototype.remove = removeFunc; + Metamorph.prototype.outerHTML = outerHTMLFunc; + Metamorph.prototype.appendTo = appendToFunc; + Metamorph.prototype.after = afterFunc; + Metamorph.prototype.prepend = prependFunc; + Metamorph.prototype.startTag = startTagFunc; + Metamorph.prototype.endTag = endTagFunc; + + Metamorph.prototype.isRemoved = function() { + var before = document.getElementById(this.start); + var after = document.getElementById(this.end); + + return !before || !after; + }; + + Metamorph.prototype.checkRemoved = function() { + if (this.isRemoved()) { + throw new Error("Cannot perform operations on a Metamorph that is not in the DOM."); + } + }; + + return Metamorph; + }); + +})(); + +(function() { +define("ember-handlebars-compiler", + ["ember-metal/core","exports"], + function(__dependency1__, __exports__) { + "use strict"; + /** + @module ember + @submodule ember-handlebars-compiler + */ + + var Ember = __dependency1__["default"]; + + // ES6Todo: you'll need to import debugger once debugger is es6'd. + if (typeof Ember.assert === 'undefined') { Ember.assert = function(){}; }; + if (typeof Ember.FEATURES === 'undefined') { Ember.FEATURES = { isEnabled: function(){} }; }; + + var objectCreate = Object.create || function(parent) { + function F() {} + F.prototype = parent; + return new F(); + }; + + // set up for circular references later + var View, Component; + + // ES6Todo: when ember-debug is es6'ed import this. + // var emberAssert = Ember.assert; + var Handlebars = (Ember.imports && Ember.imports.Handlebars) || (this && this.Handlebars); + if (!Handlebars && typeof require === 'function') { + Handlebars = require('handlebars'); + } + + Ember.assert("Ember Handlebars requires Handlebars version 1.0 or 1.1. Include " + + "a SCRIPT tag in the HTML HEAD linking to the Handlebars file " + + "before you link to Ember.", Handlebars); + + Ember.assert("Ember Handlebars requires Handlebars version 1.0 or 1.1, " + + "COMPILER_REVISION expected: 4, got: " + Handlebars.COMPILER_REVISION + + " - Please note: Builds of master may have other COMPILER_REVISION values.", + Handlebars.COMPILER_REVISION === 4); + + /** + Prepares the Handlebars templating library for use inside Ember's view + system. + + The `Ember.Handlebars` object is the standard Handlebars library, extended to + use Ember's `get()` method instead of direct property access, which allows + computed properties to be used inside templates. + + To create an `Ember.Handlebars` template, call `Ember.Handlebars.compile()`. + This will return a function that can be used by `Ember.View` for rendering. + + @class Handlebars + @namespace Ember + */ + var EmberHandlebars = Ember.Handlebars = objectCreate(Handlebars); + + /** + Register a bound helper or custom view helper. + + ## Simple bound helper example + + ```javascript + Ember.Handlebars.helper('capitalize', function(value) { + return value.toUpperCase(); + }); + ``` + + The above bound helper can be used inside of templates as follows: + + ```handlebars + {{capitalize name}} + ``` + + In this case, when the `name` property of the template's context changes, + the rendered value of the helper will update to reflect this change. + + For more examples of bound helpers, see documentation for + `Ember.Handlebars.registerBoundHelper`. + + ## Custom view helper example + + Assuming a view subclass named `App.CalendarView` were defined, a helper + for rendering instances of this view could be registered as follows: + + ```javascript + Ember.Handlebars.helper('calendar', App.CalendarView): + ``` + + The above bound helper can be used inside of templates as follows: + + ```handlebars + {{calendar}} + ``` + + Which is functionally equivalent to: + + ```handlebars + {{view App.CalendarView}} + ``` + + Options in the helper will be passed to the view in exactly the same + manner as with the `view` helper. + + @method helper + @for Ember.Handlebars + @param {String} name + @param {Function|Ember.View} function or view class constructor + @param {String} dependentKeys* + */ + EmberHandlebars.helper = function(name, value) { + if (!View) { View = requireModule('ember-views/views/view')['View']; } // ES6TODO: stupid circular dep + if (!Component) { Component = requireModule('ember-views/views/component')['default']; } // ES6TODO: stupid circular dep + + Ember.assert("You tried to register a component named '" + name + "', but component names must include a '-'", !Component.detect(value) || name.match(/-/)); + + if (View.detect(value)) { + EmberHandlebars.registerHelper(name, EmberHandlebars.makeViewHelper(value)); + } else { + EmberHandlebars.registerBoundHelper.apply(null, arguments); + } + }; + + /** + Returns a helper function that renders the provided ViewClass. + + Used internally by Ember.Handlebars.helper and other methods + involving helper/component registration. + + @private + @method makeViewHelper + @for Ember.Handlebars + @param {Function} ViewClass view class constructor + @since 1.2.0 + */ + EmberHandlebars.makeViewHelper = function(ViewClass) { + return function(options) { + Ember.assert("You can only pass attributes (such as name=value) not bare values to a helper for a View found in '" + ViewClass.toString() + "'", arguments.length < 2); + return EmberHandlebars.helpers.view.call(this, ViewClass, options); + }; + }; + + /** + @class helpers + @namespace Ember.Handlebars + */ + EmberHandlebars.helpers = objectCreate(Handlebars.helpers); + + /** + Override the the opcode compiler and JavaScript compiler for Handlebars. + + @class Compiler + @namespace Ember.Handlebars + @private + @constructor + */ + EmberHandlebars.Compiler = function() {}; + + // Handlebars.Compiler doesn't exist in runtime-only + if (Handlebars.Compiler) { + EmberHandlebars.Compiler.prototype = objectCreate(Handlebars.Compiler.prototype); + } + + EmberHandlebars.Compiler.prototype.compiler = EmberHandlebars.Compiler; + + /** + @class JavaScriptCompiler + @namespace Ember.Handlebars + @private + @constructor + */ + EmberHandlebars.JavaScriptCompiler = function() {}; + + // Handlebars.JavaScriptCompiler doesn't exist in runtime-only + if (Handlebars.JavaScriptCompiler) { + EmberHandlebars.JavaScriptCompiler.prototype = objectCreate(Handlebars.JavaScriptCompiler.prototype); + EmberHandlebars.JavaScriptCompiler.prototype.compiler = EmberHandlebars.JavaScriptCompiler; + } + + + EmberHandlebars.JavaScriptCompiler.prototype.namespace = "Ember.Handlebars"; + + EmberHandlebars.JavaScriptCompiler.prototype.initializeBuffer = function() { + return "''"; + }; + + /** + Override the default buffer for Ember Handlebars. By default, Handlebars + creates an empty String at the beginning of each invocation and appends to + it. Ember's Handlebars overrides this to append to a single shared buffer. + + @private + @method appendToBuffer + @param string {String} + */ + EmberHandlebars.JavaScriptCompiler.prototype.appendToBuffer = function(string) { + return "data.buffer.push("+string+");"; + }; + + // Hacks ahead: + // Handlebars presently has a bug where the `blockHelperMissing` hook + // doesn't get passed the name of the missing helper name, but rather + // gets passed the value of that missing helper evaluated on the current + // context, which is most likely `undefined` and totally useless. + // + // So we alter the compiled template function to pass the name of the helper + // instead, as expected. + // + // This can go away once the following is closed: + // https://github.com/wycats/handlebars.js/issues/634 + + var DOT_LOOKUP_REGEX = /helpers\.(.*?)\)/, + BRACKET_STRING_LOOKUP_REGEX = /helpers\['(.*?)'/, + INVOCATION_SPLITTING_REGEX = /(.*blockHelperMissing\.call\(.*)(stack[0-9]+)(,.*)/; + + EmberHandlebars.JavaScriptCompiler.stringifyLastBlockHelperMissingInvocation = function(source) { + var helperInvocation = source[source.length - 1], + helperName = (DOT_LOOKUP_REGEX.exec(helperInvocation) || BRACKET_STRING_LOOKUP_REGEX.exec(helperInvocation))[1], + matches = INVOCATION_SPLITTING_REGEX.exec(helperInvocation); + + source[source.length - 1] = matches[1] + "'" + helperName + "'" + matches[3]; + }; + + var stringifyBlockHelperMissing = EmberHandlebars.JavaScriptCompiler.stringifyLastBlockHelperMissingInvocation; + + var originalBlockValue = EmberHandlebars.JavaScriptCompiler.prototype.blockValue; + EmberHandlebars.JavaScriptCompiler.prototype.blockValue = function() { + originalBlockValue.apply(this, arguments); + stringifyBlockHelperMissing(this.source); + }; + + var originalAmbiguousBlockValue = EmberHandlebars.JavaScriptCompiler.prototype.ambiguousBlockValue; + EmberHandlebars.JavaScriptCompiler.prototype.ambiguousBlockValue = function() { + originalAmbiguousBlockValue.apply(this, arguments); + stringifyBlockHelperMissing(this.source); + }; + + /** + Rewrite simple mustaches from `{{foo}}` to `{{bind "foo"}}`. This means that + all simple mustaches in Ember's Handlebars will also set up an observer to + keep the DOM up to date when the underlying property changes. + + @private + @method mustache + @for Ember.Handlebars.Compiler + @param mustache + */ + EmberHandlebars.Compiler.prototype.mustache = function(mustache) { + if (!(mustache.params.length || mustache.hash)) { + var id = new Handlebars.AST.IdNode([{ part: '_triageMustache' }]); + + // Update the mustache node to include a hash value indicating whether the original node + // was escaped. This will allow us to properly escape values when the underlying value + // changes and we need to re-render the value. + if (!mustache.escaped) { + mustache.hash = mustache.hash || new Handlebars.AST.HashNode([]); + mustache.hash.pairs.push(["unescaped", new Handlebars.AST.StringNode("true")]); + } + mustache = new Handlebars.AST.MustacheNode([id].concat([mustache.id]), mustache.hash, !mustache.escaped); + } + + return Handlebars.Compiler.prototype.mustache.call(this, mustache); + }; + + /** + Used for precompilation of Ember Handlebars templates. This will not be used + during normal app execution. + + @method precompile + @for Ember.Handlebars + @static + @param {String} string The template to precompile + @param {Boolean} asObject optional parameter, defaulting to true, of whether or not the + compiled template should be returned as an Object or a String + */ + EmberHandlebars.precompile = function(string, asObject) { + var ast = Handlebars.parse(string); + + var options = { + knownHelpers: { + action: true, + unbound: true, + 'bind-attr': true, + template: true, + view: true, + _triageMustache: true + }, + data: true, + stringParams: true + }; + + asObject = asObject === undefined ? true : asObject; + + var environment = new EmberHandlebars.Compiler().compile(ast, options); + return new EmberHandlebars.JavaScriptCompiler().compile(environment, options, undefined, asObject); + }; + + // We don't support this for Handlebars runtime-only + if (Handlebars.compile) { + /** + The entry point for Ember Handlebars. This replaces the default + `Handlebars.compile` and turns on template-local data and String + parameters. + + @method compile + @for Ember.Handlebars + @static + @param {String} string The template to compile + @return {Function} + */ + EmberHandlebars.compile = function(string) { + var ast = Handlebars.parse(string); + var options = { data: true, stringParams: true }; + var environment = new EmberHandlebars.Compiler().compile(ast, options); + var templateSpec = new EmberHandlebars.JavaScriptCompiler().compile(environment, options, undefined, true); + + var template = EmberHandlebars.template(templateSpec); + template.isMethod = false; //Make sure we don't wrap templates with ._super + + return template; + }; + } + + __exports__["default"] = EmberHandlebars; + }); +})(); + +(function() { +define("ember-handlebars/component_lookup", + ["ember-runtime/system/object","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var EmberObject = __dependency1__["default"]; + + var ComponentLookup = EmberObject.extend({ + lookupFactory: function(name, container) { + + container = container || this.container; + + var fullName = 'component:' + name, + templateFullName = 'template:components/' + name, + templateRegistered = container && container.has(templateFullName); + + if (templateRegistered) { + container.injection(fullName, 'layout', templateFullName); + } + + var Component = container.lookupFactory(fullName); + + // Only treat as a component if either the component + // or a template has been registered. + if (templateRegistered || Component) { + if (!Component) { + container.register(fullName, Ember.Component); + Component = container.lookupFactory(fullName); + } + return Component; + } + } + }); + + __exports__["default"] = ComponentLookup; + }); +define("ember-handlebars/controls", + ["ember-handlebars/controls/checkbox","ember-handlebars/controls/text_field","ember-handlebars/controls/text_area","ember-metal/core","ember-handlebars-compiler","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __exports__) { + "use strict"; + var Checkbox = __dependency1__["default"]; + var TextField = __dependency2__["default"]; + var TextArea = __dependency3__["default"]; + + var Ember = __dependency4__["default"]; + // Ember.assert + // var emberAssert = Ember.assert; + + var EmberHandlebars = __dependency5__["default"]; + var helpers = EmberHandlebars.helpers; + /** + @module ember + @submodule ember-handlebars-compiler + */ + + /** + + The `{{input}}` helper inserts an HTML `` tag into the template, + with a `type` value of either `text` or `checkbox`. If no `type` is provided, + `text` will be the default value applied. The attributes of `{{input}}` + match those of the native HTML tag as closely as possible for these two types. + + ## Use as text field + An `{{input}}` with no `type` or a `type` of `text` will render an HTML text input. + The following HTML attributes can be set via the helper: + + + + + + + + + + + + +
    `readonly``required``autofocus`
    `value``placeholder``disabled`
    `size``tabindex``maxlength`
    `name``min``max`
    `pattern``accept``autocomplete`
    `autosave``formaction``formenctype`
    `formmethod``formnovalidate``formtarget`
    `height``inputmode``multiple`
    `step``width``form`
    `selectionDirection``spellcheck` 
    + + + When set to a quoted string, these values will be directly applied to the HTML + element. When left unquoted, these values will be bound to a property on the + template's current rendering context (most typically a controller instance). + + ## Unbound: + + ```handlebars + {{input value="http://www.facebook.com"}} + ``` + + + ```html + + ``` + + ## Bound: + + ```javascript + App.ApplicationController = Ember.Controller.extend({ + firstName: "Stanley", + entryNotAllowed: true + }); + ``` + + + ```handlebars + {{input type="text" value=firstName disabled=entryNotAllowed size="50"}} + ``` + + + ```html + + ``` + + ## Extension + + Internally, `{{input type="text"}}` creates an instance of `Ember.TextField`, passing + arguments from the helper to `Ember.TextField`'s `create` method. You can extend the + capabilities of text inputs in your applications by reopening this class. For example, + if you are building a Bootstrap project where `data-*` attributes are used, you + can add one to the `TextField`'s `attributeBindings` property: + + + ```javascript + Ember.TextField.reopen({ + attributeBindings: ['data-error'] + }); + ``` + + Keep in mind when writing `Ember.TextField` subclasses that `Ember.TextField` + itself extends `Ember.Component`, meaning that it does NOT inherit + the `controller` of the parent view. + + See more about [Ember components](api/classes/Ember.Component.html) + + + ## Use as checkbox + + An `{{input}}` with a `type` of `checkbox` will render an HTML checkbox input. + The following HTML attributes can be set via the helper: + + * `checked` + * `disabled` + * `tabindex` + * `indeterminate` + * `name` + * `autofocus` + * `form` + + + When set to a quoted string, these values will be directly applied to the HTML + element. When left unquoted, these values will be bound to a property on the + template's current rendering context (most typically a controller instance). + + ## Unbound: + + ```handlebars + {{input type="checkbox" name="isAdmin"}} + ``` + + ```html + + ``` + + ## Bound: + + ```javascript + App.ApplicationController = Ember.Controller.extend({ + isAdmin: true + }); + ``` + + + ```handlebars + {{input type="checkbox" checked=isAdmin }} + ``` + + + ```html + + ``` + + ## Extension + + Internally, `{{input type="checkbox"}}` creates an instance of `Ember.Checkbox`, passing + arguments from the helper to `Ember.Checkbox`'s `create` method. You can extend the + capablilties of checkbox inputs in your applications by reopening this class. For example, + if you wanted to add a css class to all checkboxes in your application: + + + ```javascript + Ember.Checkbox.reopen({ + classNames: ['my-app-checkbox'] + }); + ``` + + + @method input + @for Ember.Handlebars.helpers + @param {Hash} options + */ + function inputHelper(options) { + Ember.assert('You can only pass attributes to the `input` helper, not arguments', arguments.length < 2); + + var hash = options.hash, + types = options.hashTypes, + inputType = hash.type, + onEvent = hash.on; + + delete hash.type; + delete hash.on; + + if (inputType === 'checkbox') { + Ember.assert("{{input type='checkbox'}} does not support setting `value=someBooleanValue`; you must use `checked=someBooleanValue` instead.", options.hashTypes.value !== 'ID'); + return helpers.view.call(this, Checkbox, options); + } else { + if (inputType) { hash.type = inputType; } + hash.onEvent = onEvent || 'enter'; + return helpers.view.call(this, TextField, options); + } + } + + /** + `{{textarea}}` inserts a new instance of ` + ``` + + Bound: + + In the following example, the `writtenWords` property on `App.ApplicationController` + will be updated live as the user types 'Lots of text that IS bound' into + the text area of their browser's window. + + ```javascript + App.ApplicationController = Ember.Controller.extend({ + writtenWords: "Lots of text that IS bound" + }); + ``` + + ```handlebars + {{textarea value=writtenWords}} + ``` + + Would result in the following HTML: + + ```html + + ``` + + If you wanted a one way binding between the text area and a div tag + somewhere else on your screen, you could use `Ember.computed.oneWay`: + + ```javascript + App.ApplicationController = Ember.Controller.extend({ + writtenWords: "Lots of text that IS bound", + outputWrittenWords: Ember.computed.oneWay("writtenWords") + }); + ``` + + ```handlebars + {{textarea value=writtenWords}} + +
    + {{outputWrittenWords}} +
    + ``` + + Would result in the following HTML: + + ```html + + + <-- the following div will be updated in real time as you type --> + +
    + Lots of text that IS bound +
    + ``` + + Finally, this example really shows the power and ease of Ember when two + properties are bound to eachother via `Ember.computed.alias`. Type into + either text area box and they'll both stay in sync. Note that + `Ember.computed.alias` costs more in terms of performance, so only use it when + your really binding in both directions: + + ```javascript + App.ApplicationController = Ember.Controller.extend({ + writtenWords: "Lots of text that IS bound", + twoWayWrittenWords: Ember.computed.alias("writtenWords") + }); + ``` + + ```handlebars + {{textarea value=writtenWords}} + {{textarea value=twoWayWrittenWords}} + ``` + + ```html + + + <-- both updated in real time --> + + + ``` + + ## Extension + + Internally, `{{textarea}}` creates an instance of `Ember.TextArea`, passing + arguments from the helper to `Ember.TextArea`'s `create` method. You can + extend the capabilities of text areas in your application by reopening this + class. For example, if you are building a Bootstrap project where `data-*` + attributes are used, you can globally add support for a `data-*` attribute + on all `{{textarea}}`s' in your app by reopening `Ember.TextArea` or + `Ember.TextSupport` and adding it to the `attributeBindings` concatenated + property: + + ```javascript + Ember.TextArea.reopen({ + attributeBindings: ['data-error'] + }); + ``` + + Keep in mind when writing `Ember.TextArea` subclasses that `Ember.TextArea` + itself extends `Ember.Component`, meaning that it does NOT inherit + the `controller` of the parent view. + + See more about [Ember components](api/classes/Ember.Component.html) + + @method textarea + @for Ember.Handlebars.helpers + @param {Hash} options + */ + function textareaHelper(options) { + Ember.assert('You can only pass attributes to the `textarea` helper, not arguments', arguments.length < 2); + + var hash = options.hash, + types = options.hashTypes; + + return helpers.view.call(this, TextArea, options); + } + + __exports__.inputHelper = inputHelper; + __exports__.textareaHelper = textareaHelper; + }); +define("ember-handlebars/controls/checkbox", + ["ember-metal/property_get","ember-metal/property_set","ember-views/views/view","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __exports__) { + "use strict"; + var get = __dependency1__.get; + var set = __dependency2__.set; + var View = __dependency3__.View; + + /** + @module ember + @submodule ember-handlebars + */ + + /** + The internal class used to create text inputs when the `{{input}}` + helper is used with `type` of `checkbox`. + + See [handlebars.helpers.input](/api/classes/Ember.Handlebars.helpers.html#method_input) for usage details. + + ## Direct manipulation of `checked` + + The `checked` attribute of an `Ember.Checkbox` object should always be set + through the Ember object or by interacting with its rendered element + representation via the mouse, keyboard, or touch. Updating the value of the + checkbox via jQuery will result in the checked value of the object and its + element losing synchronization. + + ## Layout and LayoutName properties + + Because HTML `input` elements are self closing `layout` and `layoutName` + properties will not be applied. See [Ember.View](/api/classes/Ember.View.html)'s + layout section for more information. + + @class Checkbox + @namespace Ember + @extends Ember.View + */ + var Checkbox = View.extend({ + instrumentDisplay: '{{input type="checkbox"}}', + + classNames: ['ember-checkbox'], + + tagName: 'input', + + attributeBindings: ['type', 'checked', 'indeterminate', 'disabled', 'tabindex', 'name', + 'autofocus', 'required', 'form'], + + type: "checkbox", + checked: false, + disabled: false, + indeterminate: false, + + init: function() { + this._super(); + this.on("change", this, this._updateElementValue); + }, + + didInsertElement: function() { + this._super(); + get(this, 'element').indeterminate = !!get(this, 'indeterminate'); + }, + + _updateElementValue: function() { + set(this, 'checked', this.$().prop('checked')); + } + }); + + __exports__["default"] = Checkbox; + }); +define("ember-handlebars/controls/select", + ["ember-handlebars-compiler","ember-metal/enumerable_utils","ember-metal/property_get","ember-metal/property_set","ember-views/views/view","ember-views/views/collection_view","ember-metal/utils","ember-metal/is_none","ember-metal/computed","ember-runtime/system/native_array","ember-metal/mixin","ember-metal/properties","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __dependency11__, __dependency12__, __exports__) { + "use strict"; + /*jshint eqeqeq:false newcap:false */ + + /** + @module ember + @submodule ember-handlebars + */ + + var EmberHandlebars = __dependency1__["default"]; + var EnumerableUtils = __dependency2__["default"]; + var get = __dependency3__.get; + var set = __dependency4__.set; + var View = __dependency5__.View; + var CollectionView = __dependency6__["default"]; + var isArray = __dependency7__.isArray; + var isNone = __dependency8__["default"]; + var computed = __dependency9__.computed; + var A = __dependency10__.A; + var observer = __dependency11__.observer; + var defineProperty = __dependency12__.defineProperty; + + var indexOf = EnumerableUtils.indexOf, + indexesOf = EnumerableUtils.indexesOf, + forEach = EnumerableUtils.forEach, + replace = EnumerableUtils.replace, + precompileTemplate = EmberHandlebars.compile; + + var SelectOption = View.extend({ + instrumentDisplay: 'Ember.SelectOption', + + tagName: 'option', + attributeBindings: ['value', 'selected'], + + defaultTemplate: function(context, options) { + options = { data: options.data, hash: {} }; + EmberHandlebars.helpers.bind.call(context, "view.label", options); + }, + + init: function() { + this.labelPathDidChange(); + this.valuePathDidChange(); + + this._super(); + }, + + selected: computed(function() { + var content = get(this, 'content'), + selection = get(this, 'parentView.selection'); + if (get(this, 'parentView.multiple')) { + return selection && indexOf(selection, content.valueOf()) > -1; + } else { + // Primitives get passed through bindings as objects... since + // `new Number(4) !== 4`, we use `==` below + return content == selection; + } + }).property('content', 'parentView.selection'), + + labelPathDidChange: observer('parentView.optionLabelPath', function() { + var labelPath = get(this, 'parentView.optionLabelPath'); + + if (!labelPath) { return; } + + defineProperty(this, 'label', computed(function() { + return get(this, labelPath); + }).property(labelPath)); + }), + + valuePathDidChange: observer('parentView.optionValuePath', function() { + var valuePath = get(this, 'parentView.optionValuePath'); + + if (!valuePath) { return; } + + defineProperty(this, 'value', computed(function() { + return get(this, valuePath); + }).property(valuePath)); + }) + }); + + var SelectOptgroup = CollectionView.extend({ + instrumentDisplay: 'Ember.SelectOptgroup', + + tagName: 'optgroup', + attributeBindings: ['label'], + + selectionBinding: 'parentView.selection', + multipleBinding: 'parentView.multiple', + optionLabelPathBinding: 'parentView.optionLabelPath', + optionValuePathBinding: 'parentView.optionValuePath', + + itemViewClassBinding: 'parentView.optionView' + }); + + /** + The `Ember.Select` view class renders a + [select](https://developer.mozilla.org/en/HTML/Element/select) HTML element, + allowing the user to choose from a list of options. + + The text and `value` property of each ` + + + ``` + + The `value` attribute of the selected `"); + return buffer; + } + +function program3(depth0,data) { + + var stack1; + stack1 = helpers.each.call(depth0, "view.groupedContent", {hash:{},hashTypes:{},hashContexts:{},inverse:self.noop,fn:self.program(4, program4, data),contexts:[depth0],types:["ID"],data:data}); + if(stack1 || stack1 === 0) { data.buffer.push(stack1); } + else { data.buffer.push(''); } + } +function program4(depth0,data) { + + + data.buffer.push(escapeExpression(helpers.view.call(depth0, "view.groupView", {hash:{ + 'content': ("content"), + 'label': ("label") + },hashTypes:{'content': "ID",'label': "ID"},hashContexts:{'content': depth0,'label': depth0},contexts:[depth0],types:["ID"],data:data}))); + } + +function program6(depth0,data) { + + var stack1; + stack1 = helpers.each.call(depth0, "view.content", {hash:{},hashTypes:{},hashContexts:{},inverse:self.noop,fn:self.program(7, program7, data),contexts:[depth0],types:["ID"],data:data}); + if(stack1 || stack1 === 0) { data.buffer.push(stack1); } + else { data.buffer.push(''); } + } +function program7(depth0,data) { + + + data.buffer.push(escapeExpression(helpers.view.call(depth0, "view.optionView", {hash:{ + 'content': ("") + },hashTypes:{'content': "ID"},hashContexts:{'content': depth0},contexts:[depth0],types:["ID"],data:data}))); + } + + stack1 = helpers['if'].call(depth0, "view.prompt", {hash:{},hashTypes:{},hashContexts:{},inverse:self.noop,fn:self.program(1, program1, data),contexts:[depth0],types:["ID"],data:data}); + if(stack1 || stack1 === 0) { data.buffer.push(stack1); } + stack1 = helpers['if'].call(depth0, "view.optionGroupPath", {hash:{},hashTypes:{},hashContexts:{},inverse:self.program(6, program6, data),fn:self.program(3, program3, data),contexts:[depth0],types:["ID"],data:data}); + if(stack1 || stack1 === 0) { data.buffer.push(stack1); } + return buffer; + +}), + attributeBindings: ['multiple', 'disabled', 'tabindex', 'name', 'required', 'autofocus', + 'form', 'size'], + + /** + The `multiple` attribute of the select element. Indicates whether multiple + options can be selected. + + @property multiple + @type Boolean + @default false + */ + multiple: false, + + /** + The `disabled` attribute of the select element. Indicates whether + the element is disabled from interactions. + + @property disabled + @type Boolean + @default false + */ + disabled: false, + + /** + The `required` attribute of the select element. Indicates whether + a selected option is required for form validation. + + @property required + @type Boolean + @default false + @since 1.5.0 + */ + required: false, + + /** + The list of options. + + If `optionLabelPath` and `optionValuePath` are not overridden, this should + be a list of strings, which will serve simultaneously as labels and values. + + Otherwise, this should be a list of objects. For instance: + + ```javascript + Ember.Select.create({ + content: A([ + { id: 1, firstName: 'Yehuda' }, + { id: 2, firstName: 'Tom' } + ]), + optionLabelPath: 'content.firstName', + optionValuePath: 'content.id' + }); + ``` + + @property content + @type Array + @default null + */ + content: null, + + /** + When `multiple` is `false`, the element of `content` that is currently + selected, if any. + + When `multiple` is `true`, an array of such elements. + + @property selection + @type Object or Array + @default null + */ + selection: null, + + /** + In single selection mode (when `multiple` is `false`), value can be used to + get the current selection's value or set the selection by it's value. + + It is not currently supported in multiple selection mode. + + @property value + @type String + @default null + */ + value: computed(function(key, value) { + if (arguments.length === 2) { return value; } + var valuePath = get(this, 'optionValuePath').replace(/^content\.?/, ''); + return valuePath ? get(this, 'selection.' + valuePath) : get(this, 'selection'); + }).property('selection'), + + /** + If given, a top-most dummy option will be rendered to serve as a user + prompt. + + @property prompt + @type String + @default null + */ + prompt: null, + + /** + The path of the option labels. See [content](/api/classes/Ember.Select.html#property_content). + + @property optionLabelPath + @type String + @default 'content' + */ + optionLabelPath: 'content', + + /** + The path of the option values. See [content](/api/classes/Ember.Select.html#property_content). + + @property optionValuePath + @type String + @default 'content' + */ + optionValuePath: 'content', + + /** + The path of the option group. + When this property is used, `content` should be sorted by `optionGroupPath`. + + @property optionGroupPath + @type String + @default null + */ + optionGroupPath: null, + + /** + The view class for optgroup. + + @property groupView + @type Ember.View + @default Ember.SelectOptgroup + */ + groupView: SelectOptgroup, + + groupedContent: computed(function() { + var groupPath = get(this, 'optionGroupPath'); + var groupedContent = A(); + var content = get(this, 'content') || []; + + forEach(content, function(item) { + var label = get(item, groupPath); + + if (get(groupedContent, 'lastObject.label') !== label) { + groupedContent.pushObject({ + label: label, + content: A() + }); + } + + get(groupedContent, 'lastObject.content').push(item); + }); + + return groupedContent; + }).property('optionGroupPath', 'content.@each'), + + /** + The view class for option. + + @property optionView + @type Ember.View + @default Ember.SelectOption + */ + optionView: SelectOption, + + _change: function() { + if (get(this, 'multiple')) { + this._changeMultiple(); + } else { + this._changeSingle(); + } + }, + + selectionDidChange: observer('selection.@each', function() { + var selection = get(this, 'selection'); + if (get(this, 'multiple')) { + if (!isArray(selection)) { + set(this, 'selection', A([selection])); + return; + } + this._selectionDidChangeMultiple(); + } else { + this._selectionDidChangeSingle(); + } + }), + + valueDidChange: observer('value', function() { + var content = get(this, 'content'), + value = get(this, 'value'), + valuePath = get(this, 'optionValuePath').replace(/^content\.?/, ''), + selectedValue = (valuePath ? get(this, 'selection.' + valuePath) : get(this, 'selection')), + selection; + + if (value !== selectedValue) { + selection = content ? content.find(function(obj) { + return value === (valuePath ? get(obj, valuePath) : obj); + }) : null; + + this.set('selection', selection); + } + }), + + + _triggerChange: function() { + var selection = get(this, 'selection'); + var value = get(this, 'value'); + + if (!isNone(selection)) { this.selectionDidChange(); } + if (!isNone(value)) { this.valueDidChange(); } + + this._change(); + }, + + _changeSingle: function() { + var selectedIndex = this.$()[0].selectedIndex, + content = get(this, 'content'), + prompt = get(this, 'prompt'); + + if (!content || !get(content, 'length')) { return; } + if (prompt && selectedIndex === 0) { set(this, 'selection', null); return; } + + if (prompt) { selectedIndex -= 1; } + set(this, 'selection', content.objectAt(selectedIndex)); + }, + + + _changeMultiple: function() { + var options = this.$('option:selected'), + prompt = get(this, 'prompt'), + offset = prompt ? 1 : 0, + content = get(this, 'content'), + selection = get(this, 'selection'); + + if (!content) { return; } + if (options) { + var selectedIndexes = options.map(function() { + return this.index - offset; + }).toArray(); + var newSelection = content.objectsAt(selectedIndexes); + + if (isArray(selection)) { + replace(selection, 0, get(selection, 'length'), newSelection); + } else { + set(this, 'selection', newSelection); + } + } + }, + + _selectionDidChangeSingle: function() { + var el = this.get('element'); + if (!el) { return; } + + var content = get(this, 'content'), + selection = get(this, 'selection'), + selectionIndex = content ? indexOf(content, selection) : -1, + prompt = get(this, 'prompt'); + + if (prompt) { selectionIndex += 1; } + if (el) { el.selectedIndex = selectionIndex; } + }, + + _selectionDidChangeMultiple: function() { + var content = get(this, 'content'), + selection = get(this, 'selection'), + selectedIndexes = content ? indexesOf(content, selection) : [-1], + prompt = get(this, 'prompt'), + offset = prompt ? 1 : 0, + options = this.$('option'), + adjusted; + + if (options) { + options.each(function() { + adjusted = this.index > -1 ? this.index - offset : -1; + this.selected = indexOf(selectedIndexes, adjusted) > -1; + }); + } + }, + + init: function() { + this._super(); + this.on("didInsertElement", this, this._triggerChange); + this.on("change", this, this._change); + } + }); + + __exports__["default"] = Select + __exports__.Select = Select; + __exports__.SelectOption = SelectOption; + __exports__.SelectOptgroup = SelectOptgroup; + }); +define("ember-handlebars/controls/text_area", + ["ember-metal/property_get","ember-views/views/component","ember-handlebars/controls/text_support","ember-metal/mixin","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __exports__) { + "use strict"; + + /** + @module ember + @submodule ember-handlebars + */ + var get = __dependency1__.get; + var Component = __dependency2__["default"]; + var TextSupport = __dependency3__["default"]; + var observer = __dependency4__.observer; + + /** + The internal class used to create textarea element when the `{{textarea}}` + helper is used. + + See [handlebars.helpers.textarea](/api/classes/Ember.Handlebars.helpers.html#method_textarea) for usage details. + + ## Layout and LayoutName properties + + Because HTML `textarea` elements do not contain inner HTML the `layout` and + `layoutName` properties will not be applied. See [Ember.View](/api/classes/Ember.View.html)'s + layout section for more information. + + @class TextArea + @namespace Ember + @extends Ember.Component + @uses Ember.TextSupport + */ + var TextArea = Component.extend(TextSupport, { + instrumentDisplay: '{{textarea}}', + + classNames: ['ember-text-area'], + + tagName: "textarea", + attributeBindings: ['rows', 'cols', 'name', 'selectionEnd', 'selectionStart', 'wrap'], + rows: null, + cols: null, + + _updateElementValue: observer('value', function() { + // We do this check so cursor position doesn't get affected in IE + var value = get(this, 'value'), + $el = this.$(); + if ($el && value !== $el.val()) { + $el.val(value); + } + }), + + init: function() { + this._super(); + this.on("didInsertElement", this, this._updateElementValue); + } + + }); + + __exports__["default"] = TextArea; + }); +define("ember-handlebars/controls/text_field", + ["ember-metal/property_get","ember-metal/property_set","ember-views/views/component","ember-handlebars/controls/text_support","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __exports__) { + "use strict"; + /** + @module ember + @submodule ember-handlebars + */ + + var get = __dependency1__.get; + var set = __dependency2__.set; + var Component = __dependency3__["default"]; + var TextSupport = __dependency4__["default"]; + + /** + + The internal class used to create text inputs when the `{{input}}` + helper is used with `type` of `text`. + + See [Handlebars.helpers.input](/api/classes/Ember.Handlebars.helpers.html#method_input) for usage details. + + ## Layout and LayoutName properties + + Because HTML `input` elements are self closing `layout` and `layoutName` + properties will not be applied. See [Ember.View](/api/classes/Ember.View.html)'s + layout section for more information. + + @class TextField + @namespace Ember + @extends Ember.Component + @uses Ember.TextSupport + */ + var TextField = Component.extend(TextSupport, { + instrumentDisplay: '{{input type="text"}}', + + classNames: ['ember-text-field'], + tagName: "input", + attributeBindings: ['type', 'value', 'size', 'pattern', 'name', 'min', 'max', + 'accept', 'autocomplete', 'autosave', 'formaction', + 'formenctype', 'formmethod', 'formnovalidate', 'formtarget', + 'height', 'inputmode', 'list', 'multiple', 'pattern', 'step', + 'width'], + + /** + The `value` attribute of the input element. As the user inputs text, this + property is updated live. + + @property value + @type String + @default "" + */ + value: "", + + /** + The `type` attribute of the input element. + + @property type + @type String + @default "text" + */ + type: "text", + + /** + The `size` of the text field in characters. + + @property size + @type String + @default null + */ + size: null, + + /** + The `pattern` attribute of input element. + + @property pattern + @type String + @default null + */ + pattern: null, + + /** + The `min` attribute of input element used with `type="number"` or `type="range"`. + + @property min + @type String + @default null + @since 1.4.0 + */ + min: null, + + /** + The `max` attribute of input element used with `type="number"` or `type="range"`. + + @property max + @type String + @default null + @since 1.4.0 + */ + max: null + }); + + __exports__["default"] = TextField; + }); +define("ember-handlebars/controls/text_support", + ["ember-metal/property_get","ember-metal/property_set","ember-metal/mixin","ember-runtime/mixins/target_action_support","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __exports__) { + "use strict"; + /** + @module ember + @submodule ember-handlebars + */ + + var get = __dependency1__.get; + var set = __dependency2__.set; + var Mixin = __dependency3__.Mixin; + var TargetActionSupport = __dependency4__["default"]; + + /** + Shared mixin used by `Ember.TextField` and `Ember.TextArea`. + + @class TextSupport + @namespace Ember + @uses Ember.TargetActionSupport + @extends Ember.Mixin + @private + */ + var TextSupport = Mixin.create(TargetActionSupport, { + value: "", + + attributeBindings: ['placeholder', 'disabled', 'maxlength', 'tabindex', 'readonly', + 'autofocus', 'form', 'selectionDirection', 'spellcheck', 'required', + 'title', 'autocapitalize', 'autocorrect'], + placeholder: null, + disabled: false, + maxlength: null, + + init: function() { + this._super(); + this.on("focusOut", this, this._elementValueDidChange); + this.on("change", this, this._elementValueDidChange); + this.on("paste", this, this._elementValueDidChange); + this.on("cut", this, this._elementValueDidChange); + this.on("input", this, this._elementValueDidChange); + this.on("keyUp", this, this.interpretKeyEvents); + }, + + /** + The action to be sent when the user presses the return key. + + This is similar to the `{{action}}` helper, but is fired when + the user presses the return key when editing a text field, and sends + the value of the field as the context. + + @property action + @type String + @default null + */ + action: null, + + /** + The event that should send the action. + + Options are: + + * `enter`: the user pressed enter + * `keyPress`: the user pressed a key + + @property onEvent + @type String + @default enter + */ + onEvent: 'enter', + + /** + Whether they `keyUp` event that triggers an `action` to be sent continues + propagating to other views. + + By default, when the user presses the return key on their keyboard and + the text field has an `action` set, the action will be sent to the view's + controller and the key event will stop propagating. + + If you would like parent views to receive the `keyUp` event even after an + action has been dispatched, set `bubbles` to true. + + @property bubbles + @type Boolean + @default false + */ + bubbles: false, + + interpretKeyEvents: function(event) { + var map = TextSupport.KEY_EVENTS; + var method = map[event.keyCode]; + + this._elementValueDidChange(); + if (method) { return this[method](event); } + }, + + _elementValueDidChange: function() { + set(this, 'value', this.$().val()); + }, + + /** + The action to be sent when the user inserts a new line. + + Called by the `Ember.TextSupport` mixin on keyUp if keycode matches 13. + Uses sendAction to send the `enter` action to the controller. + + @method insertNewline + @param {Event} event + */ + insertNewline: function(event) { + sendAction('enter', this, event); + sendAction('insert-newline', this, event); + }, + + /** + Called when the user hits escape. + + Called by the `Ember.TextSupport` mixin on keyUp if keycode matches 27. + Uses sendAction to send the `escape-press` action to the controller. + + @method cancel + @param {Event} event + */ + cancel: function(event) { + sendAction('escape-press', this, event); + }, + + /** + Called when the text area is focused. + + @method focusIn + @param {Event} event + */ + focusIn: function(event) { + sendAction('focus-in', this, event); + }, + + /** + Called when the text area is blurred. + + @method focusOut + @param {Event} event + */ + focusOut: function(event) { + sendAction('focus-out', this, event); + }, + + /** + The action to be sent when the user presses a key. Enabled by setting + the `onEvent` property to `keyPress`. + + Uses sendAction to send the `keyPress` action to the controller. + + @method keyPress + @param {Event} event + */ + keyPress: function(event) { + sendAction('key-press', this, event); + } + + }); + + TextSupport.KEY_EVENTS = { + 13: 'insertNewline', + 27: 'cancel' + }; + + // In principle, this shouldn't be necessary, but the legacy + // sendAction semantics for TextField are different from + // the component semantics so this method normalizes them. + function sendAction(eventName, view, event) { + var action = get(view, eventName), + on = get(view, 'onEvent'), + value = get(view, 'value'); + + // back-compat support for keyPress as an event name even though + // it's also a method name that consumes the event (and therefore + // incompatible with sendAction semantics). + if (on === eventName || (on === 'keyPress' && eventName === 'key-press')) { + view.sendAction('action', value); + } + + view.sendAction(eventName, value); + + if (action || on === eventName) { + if(!get(view, 'bubbles')) { + event.stopPropagation(); + } + } + } + + __exports__["default"] = TextSupport; + }); +define("ember-handlebars/ext", + ["ember-metal/core","ember-runtime/system/string","ember-handlebars-compiler","ember-metal/property_get","ember-metal/binding","ember-metal/error","ember-metal/mixin","ember-metal/is_empty","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __exports__) { + "use strict"; + var Ember = __dependency1__["default"]; + // Ember.FEATURES, Ember.assert, Ember.Handlebars, Ember.lookup + // var emberAssert = Ember.assert; + + var fmt = __dependency2__.fmt; + + var EmberHandlebars = __dependency3__["default"]; + var helpers = EmberHandlebars.helpers; + + var get = __dependency4__.get; + var isGlobalPath = __dependency5__.isGlobalPath; + var EmberError = __dependency6__["default"]; + var IS_BINDING = __dependency7__.IS_BINDING; + + // late bound via requireModule because of circular dependencies. + var resolveHelper, + SimpleHandlebarsView; + + var isEmpty = __dependency8__["default"]; + + var slice = [].slice, originalTemplate = EmberHandlebars.template; + + /** + If a path starts with a reserved keyword, returns the root + that should be used. + + @private + @method normalizePath + @for Ember + @param root {Object} + @param path {String} + @param data {Hash} + */ + function normalizePath(root, path, data) { + var keywords = (data && data.keywords) || {}, + keyword, isKeyword; + + // Get the first segment of the path. For example, if the + // path is "foo.bar.baz", returns "foo". + keyword = path.split('.', 1)[0]; + + // Test to see if the first path is a keyword that has been + // passed along in the view's data hash. If so, we will treat + // that object as the new root. + if (keywords.hasOwnProperty(keyword)) { + // Look up the value in the template's data hash. + root = keywords[keyword]; + isKeyword = true; + + // Handle cases where the entire path is the reserved + // word. In that case, return the object itself. + if (path === keyword) { + path = ''; + } else { + // Strip the keyword from the path and look up + // the remainder from the newly found root. + path = path.substr(keyword.length+1); + } + } + + return { root: root, path: path, isKeyword: isKeyword }; + }; + + + /** + Lookup both on root and on window. If the path starts with + a keyword, the corresponding object will be looked up in the + template's data hash and used to resolve the path. + + @method get + @for Ember.Handlebars + @param {Object} root The object to look up the property on + @param {String} path The path to be lookedup + @param {Object} options The template's option hash + */ + function handlebarsGet(root, path, options) { + var data = options && options.data, + normalizedPath = normalizePath(root, path, data), + value; + + + root = normalizedPath.root; + path = normalizedPath.path; + + value = get(root, path); + + if (value === undefined && root !== Ember.lookup && isGlobalPath(path)) { + value = get(Ember.lookup, path); + } + + + return value; + } + + /** + This method uses `Ember.Handlebars.get` to lookup a value, then ensures + that the value is escaped properly. + + If `unescaped` is a truthy value then the escaping will not be performed. + + @method getEscaped + @for Ember.Handlebars + @param {Object} root The object to look up the property on + @param {String} path The path to be lookedup + @param {Object} options The template's option hash + @since 1.4.0 + */ + function getEscaped(root, path, options) { + var result = handlebarsGet(root, path, options); + + if (result === null || result === undefined) { + result = ""; + } else if (!(result instanceof Handlebars.SafeString)) { + result = String(result); + } + if (!options.hash.unescaped){ + result = Handlebars.Utils.escapeExpression(result); + } + + return result; + }; + + function resolveParams(context, params, options) { + var resolvedParams = [], types = options.types, param, type; + + for (var i=0, l=params.length; i{{user.name}} + +
    +
    {{user.role.label}}
    + {{user.role.id}} + +

    {{user.role.description}}

    +
    + ``` + + `{{with}}` can be our best friend in these cases, + instead of writing `user.role.*` over and over, we use `{{#with user.role}}`. + Now the context within the `{{#with}} .. {{/with}}` block is `user.role` so you can do the following: + + ```handlebars +
    {{user.name}}
    + +
    + {{#with user.role}} +
    {{label}}
    + {{id}} + +

    {{description}}

    + {{/with}} +
    + ``` + + ### `as` operator + + This operator aliases the scope to a new name. It's helpful for semantic clarity and to retain + default scope or to reference from another `{{with}}` block. + + ```handlebars + // posts might not be + {{#with user.posts as blogPosts}} +
    + There are {{blogPosts.length}} blog posts written by {{user.name}}. +
    + + {{#each post in blogPosts}} +
  • {{post.title}}
  • + {{/each}} + {{/with}} + ``` + + Without the `as` operator, it would be impossible to reference `user.name` in the example above. + + NOTE: The alias should not reuse a name from the bound property path. + For example: `{{#with foo.bar as foo}}` is not supported because it attempts to alias using + the first part of the property path, `foo`. Instead, use `{{#with foo.bar as baz}}`. + + ### `controller` option + + Adding `controller='something'` instructs the `{{with}}` helper to create and use an instance of + the specified controller with the new context as its content. + + This is very similar to using an `itemController` option with the `{{each}}` helper. + + ```handlebars + {{#with users.posts controller='userBlogPosts'}} + {{!- The current context is wrapped in our controller instance }} + {{/with}} + ``` + + In the above example, the template provided to the `{{with}}` block is now wrapped in the + `userBlogPost` controller, which provides a very elegant way to decorate the context with custom + functions/properties. + + @method with + @for Ember.Handlebars.helpers + @param {Function} context + @param {Hash} options + @return {String} HTML string + */ + function withHelper(context, options) { + var bindContext, preserveContext, controller, helperName = 'with'; + + if (arguments.length === 4) { + var keywordName, path, rootPath, normalized, contextPath; + + Ember.assert("If you pass more than one argument to the with helper, it must be in the form #with foo as bar", arguments[1] === "as"); + options = arguments[3]; + keywordName = arguments[2]; + path = arguments[0]; + + if (path) { + helperName += ' ' + path + ' as ' + keywordName; + } + + Ember.assert("You must pass a block to the with helper", options.fn && options.fn !== Handlebars.VM.noop); + + var localizedOptions = o_create(options); + localizedOptions.data = o_create(options.data); + localizedOptions.data.keywords = o_create(options.data.keywords || {}); + + if (isGlobalPath(path)) { + contextPath = path; + } else { + normalized = normalizePath(this, path, options.data); + path = normalized.path; + rootPath = normalized.root; + + // This is a workaround for the fact that you cannot bind separate objects + // together. When we implement that functionality, we should use it here. + var contextKey = jQuery.expando + guidFor(rootPath); + localizedOptions.data.keywords[contextKey] = rootPath; + // if the path is '' ("this"), just bind directly to the current context + contextPath = path ? contextKey + '.' + path : contextKey; + } + + localizedOptions.hash.keywordName = keywordName; + localizedOptions.hash.keywordPath = contextPath; + + bindContext = this; + context = path; + options = localizedOptions; + preserveContext = true; + } else { + Ember.assert("You must pass exactly one argument to the with helper", arguments.length === 2); + Ember.assert("You must pass a block to the with helper", options.fn && options.fn !== Handlebars.VM.noop); + + helperName += ' ' + context; + bindContext = options.contexts[0]; + preserveContext = false; + } + + options.helperName = helperName; + options.isWithHelper = true; + + return bind.call(bindContext, context, options, preserveContext, exists); + } + /** + See [boundIf](/api/classes/Ember.Handlebars.helpers.html#method_boundIf) + and [unboundIf](/api/classes/Ember.Handlebars.helpers.html#method_unboundIf) + + @method if + @for Ember.Handlebars.helpers + @param {Function} context + @param {Hash} options + @return {String} HTML string + */ + function ifHelper(context, options) { + Ember.assert("You must pass exactly one argument to the if helper", arguments.length === 2); + Ember.assert("You must pass a block to the if helper", options.fn && options.fn !== Handlebars.VM.noop); + + options.helperName = options.helperName || ('if ' + context); + + if (options.data.isUnbound) { + return helpers.unboundIf.call(options.contexts[0], context, options); + } else { + return helpers.boundIf.call(options.contexts[0], context, options); + } + } + + /** + @method unless + @for Ember.Handlebars.helpers + @param {Function} context + @param {Hash} options + @return {String} HTML string + */ + function unlessHelper(context, options) { + Ember.assert("You must pass exactly one argument to the unless helper", arguments.length === 2); + Ember.assert("You must pass a block to the unless helper", options.fn && options.fn !== Handlebars.VM.noop); + + var fn = options.fn, inverse = options.inverse, helperName = 'unless'; + + if (context) { + helperName += ' ' + context; + } + + options.fn = inverse; + options.inverse = fn; + + options.helperName = options.helperName || helperName; + + if (options.data.isUnbound) { + return helpers.unboundIf.call(options.contexts[0], context, options); + } else { + return helpers.boundIf.call(options.contexts[0], context, options); + } + } + + /** + `bind-attr` allows you to create a binding between DOM element attributes and + Ember objects. For example: + + ```handlebars + imageTitle + ``` + + The above handlebars template will fill the ``'s `src` attribute will + the value of the property referenced with `"imageUrl"` and its `alt` + attribute with the value of the property referenced with `"imageTitle"`. + + If the rendering context of this template is the following object: + + ```javascript + { + imageUrl: 'http://lolcats.info/haz-a-funny', + imageTitle: 'A humorous image of a cat' + } + ``` + + The resulting HTML output will be: + + ```html + A humorous image of a cat + ``` + + `bind-attr` cannot redeclare existing DOM element attributes. The use of `src` + in the following `bind-attr` example will be ignored and the hard coded value + of `src="/failwhale.gif"` will take precedence: + + ```handlebars + imageTitle + ``` + + ### `bind-attr` and the `class` attribute + + `bind-attr` supports a special syntax for handling a number of cases unique + to the `class` DOM element attribute. The `class` attribute combines + multiple discrete values into a single attribute as a space-delimited + list of strings. Each string can be: + + * a string return value of an object's property. + * a boolean return value of an object's property + * a hard-coded value + + A string return value works identically to other uses of `bind-attr`. The + return value of the property will become the value of the attribute. For + example, the following view and template: + + ```javascript + AView = View.extend({ + someProperty: function() { + return "aValue"; + }.property() + }) + ``` + + ```handlebars + + ``` + + A boolean return value will insert a specified class name if the property + returns `true` and remove the class name if the property returns `false`. + + A class name is provided via the syntax + `somePropertyName:class-name-if-true`. + + ```javascript + AView = View.extend({ + someBool: true + }) + ``` + + ```handlebars + + ``` + + Result in the following rendered output: + + ```html + + ``` + + An additional section of the binding can be provided if you want to + replace the existing class instead of removing it when the boolean + value changes: + + ```handlebars + + ``` + + A hard-coded value can be used by prepending `:` to the desired + class name: `:class-name-to-always-apply`. + + ```handlebars + + ``` + + Results in the following rendered output: + + ```html + + ``` + + All three strategies - string return value, boolean return value, and + hard-coded value – can be combined in a single declaration: + + ```handlebars + + ``` + + @method bind-attr + @for Ember.Handlebars.helpers + @param {Hash} options + @return {String} HTML string + */ + function bindAttrHelper(options) { + var attrs = options.hash; + + Ember.assert("You must specify at least one hash argument to bind-attr", !!keys(attrs).length); + + var view = options.data.view; + var ret = []; + + // we relied on the behavior of calling without + // context to mean this === window, but when running + // "use strict", it's possible for this to === undefined; + var ctx = this || window; + + // Generate a unique id for this element. This will be added as a + // data attribute to the element so it can be looked up when + // the bound property changes. + var dataId = ++Ember.uuid; + + // Handle classes differently, as we can bind multiple classes + var classBindings = attrs['class']; + if (classBindings != null) { + var classResults = bindClasses(ctx, classBindings, view, dataId, options); + + ret.push('class="' + Handlebars.Utils.escapeExpression(classResults.join(' ')) + '"'); + delete attrs['class']; + } + + var attrKeys = keys(attrs); + + // For each attribute passed, create an observer and emit the + // current value of the property as an attribute. + forEach.call(attrKeys, function(attr) { + var path = attrs[attr], + normalized; + + Ember.assert(fmt("You must provide an expression as the value of bound attribute. You specified: %@=%@", [attr, path]), typeof path === 'string'); + + normalized = normalizePath(ctx, path, options.data); + + var value = (path === 'this') ? normalized.root : handlebarsGet(ctx, path, options), + type = typeOf(value); + + Ember.assert(fmt("Attributes must be numbers, strings or booleans, not %@", [value]), value === null || value === undefined || type === 'number' || type === 'string' || type === 'boolean'); + + var observer, invoker; + + observer = function observer() { + var result = handlebarsGet(ctx, path, options); + + Ember.assert(fmt("Attributes must be numbers, strings or booleans, not %@", [result]), + result === null || result === undefined || typeof result === 'number' || + typeof result === 'string' || typeof result === 'boolean'); + + var elem = view.$("[data-bindattr-" + dataId + "='" + dataId + "']"); + + // If we aren't able to find the element, it means the element + // to which we were bound has been removed from the view. + // In that case, we can assume the template has been re-rendered + // and we need to clean up the observer. + if (!elem || elem.length === 0) { + removeObserver(normalized.root, normalized.path, invoker); + return; + } + + View.applyAttributeBindings(elem, attr, result); + }; + + // Add an observer to the view for when the property changes. + // When the observer fires, find the element using the + // unique data id and update the attribute to the new value. + // Note: don't add observer when path is 'this' or path + // is whole keyword e.g. {{#each x in list}} ... {{bind-attr attr="x"}} + if (path !== 'this' && !(normalized.isKeyword && normalized.path === '' )) { + view.registerObserver(normalized.root, normalized.path, observer); + } + + // if this changes, also change the logic in ember-views/lib/views/view.js + if ((type === 'string' || (type === 'number' && !isNaN(value)))) { + ret.push(attr + '="' + Handlebars.Utils.escapeExpression(value) + '"'); + } else if (value && type === 'boolean') { + // The developer controls the attr name, so it should always be safe + ret.push(attr + '="' + attr + '"'); + } + }, this); + + // Add the unique identifier + // NOTE: We use all lower-case since Firefox has problems with mixed case in SVG + ret.push('data-bindattr-' + dataId + '="' + dataId + '"'); + return new SafeString(ret.join(' ')); + } + + /** + See `bind-attr` + + @method bindAttr + @for Ember.Handlebars.helpers + @deprecated + @param {Function} context + @param {Hash} options + @return {String} HTML string + */ + function bindAttrHelperDeprecated() { + Ember.warn("The 'bindAttr' view helper is deprecated in favor of 'bind-attr'"); + return helpers['bind-attr'].apply(this, arguments); + } + + /** + Helper that, given a space-separated string of property paths and a context, + returns an array of class names. Calling this method also has the side + effect of setting up observers at those property paths, such that if they + change, the correct class name will be reapplied to the DOM element. + + For example, if you pass the string "fooBar", it will first look up the + "fooBar" value of the context. If that value is true, it will add the + "foo-bar" class to the current element (i.e., the dasherized form of + "fooBar"). If the value is a string, it will add that string as the class. + Otherwise, it will not add any new class name. + + @private + @method bindClasses + @for Ember.Handlebars + @param {Ember.Object} context The context from which to lookup properties + @param {String} classBindings A string, space-separated, of class bindings + to use + @param {View} view The view in which observers should look for the + element to update + @param {Srting} bindAttrId Optional bindAttr id used to lookup elements + @return {Array} An array of class names to add + */ + function bindClasses(context, classBindings, view, bindAttrId, options) { + var ret = [], newClass, value, elem; + + // Helper method to retrieve the property from the context and + // determine which class string to return, based on whether it is + // a Boolean or not. + var classStringForPath = function(root, parsedPath, options) { + var val, + path = parsedPath.path; + + if (path === 'this') { + val = root; + } else if (path === '') { + val = true; + } else { + val = handlebarsGet(root, path, options); + } + + return View._classStringForValue(path, val, parsedPath.className, parsedPath.falsyClassName); + }; + + // For each property passed, loop through and setup + // an observer. + forEach.call(classBindings.split(' '), function(binding) { + + // Variable in which the old class value is saved. The observer function + // closes over this variable, so it knows which string to remove when + // the property changes. + var oldClass; + + var observer, invoker; + + var parsedPath = View._parsePropertyPath(binding), + path = parsedPath.path, + pathRoot = context, + normalized; + + if (path !== '' && path !== 'this') { + normalized = normalizePath(context, path, options.data); + + pathRoot = normalized.root; + path = normalized.path; + } + + // Set up an observer on the context. If the property changes, toggle the + // class name. + observer = function() { + // Get the current value of the property + newClass = classStringForPath(context, parsedPath, options); + elem = bindAttrId ? view.$("[data-bindattr-" + bindAttrId + "='" + bindAttrId + "']") : view.$(); + + // If we can't find the element anymore, a parent template has been + // re-rendered and we've been nuked. Remove the observer. + if (!elem || elem.length === 0) { + removeObserver(pathRoot, path, invoker); + } else { + // If we had previously added a class to the element, remove it. + if (oldClass) { + elem.removeClass(oldClass); + } + + // If necessary, add a new class. Make sure we keep track of it so + // it can be removed in the future. + if (newClass) { + elem.addClass(newClass); + oldClass = newClass; + } else { + oldClass = null; + } + } + }; + + if (path !== '' && path !== 'this') { + view.registerObserver(pathRoot, path, observer); + } + + // We've already setup the observer; now we just need to figure out the + // correct behavior right now on the first pass through. + value = classStringForPath(context, parsedPath, options); + + if (value) { + ret.push(value); + + // Make sure we save the current value so that it can be removed if the + // observer fires. + oldClass = value; + } + }); + + return ret; + }; + + __exports__.bind = bind; + __exports__._triageMustacheHelper = _triageMustacheHelper; + __exports__.resolveHelper = resolveHelper; + __exports__.bindHelper = bindHelper; + __exports__.boundIfHelper = boundIfHelper; + __exports__.unboundIfHelper = unboundIfHelper; + __exports__.withHelper = withHelper; + __exports__.ifHelper = ifHelper; + __exports__.unlessHelper = unlessHelper; + __exports__.bindAttrHelper = bindAttrHelper; + __exports__.bindAttrHelperDeprecated = bindAttrHelperDeprecated; + __exports__.bindClasses = bindClasses; + }); +define("ember-handlebars/helpers/collection", + ["ember-metal/core","ember-metal/utils","ember-handlebars-compiler","ember-runtime/system/string","ember-metal/property_get","ember-handlebars/ext","ember-handlebars/helpers/view","ember-metal/computed","ember-views/views/collection_view","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __exports__) { + "use strict"; + /** + @module ember + @submodule ember-handlebars + */ + + var Ember = __dependency1__["default"]; + // Ember.assert, Ember.deprecate + var inspect = __dependency2__.inspect; + + // var emberAssert = Ember.assert; + // emberDeprecate = Ember.deprecate; + + var EmberHandlebars = __dependency3__["default"]; + var helpers = EmberHandlebars.helpers; + + var fmt = __dependency4__.fmt; + var get = __dependency5__.get; + var handlebarsGet = __dependency6__.handlebarsGet; + var ViewHelper = __dependency7__.ViewHelper; + var computed = __dependency8__.computed; + var CollectionView = __dependency9__["default"]; + + var alias = computed.alias; + /** + `{{collection}}` is a `Ember.Handlebars` helper for adding instances of + `Ember.CollectionView` to a template. See [Ember.CollectionView](/api/classes/Ember.CollectionView.html) + for additional information on how a `CollectionView` functions. + + `{{collection}}`'s primary use is as a block helper with a `contentBinding` + option pointing towards an `Ember.Array`-compatible object. An `Ember.View` + instance will be created for each item in its `content` property. Each view + will have its own `content` property set to the appropriate item in the + collection. + + The provided block will be applied as the template for each item's view. + + Given an empty `` the following template: + + ```handlebars + {{#collection contentBinding="App.items"}} + Hi {{view.content.name}} + {{/collection}} + ``` + + And the following application code + + ```javascript + App = Ember.Application.create() + App.items = [ + Ember.Object.create({name: 'Dave'}), + Ember.Object.create({name: 'Mary'}), + Ember.Object.create({name: 'Sara'}) + ] + ``` + + Will result in the HTML structure below + + ```html +
    +
    Hi Dave
    +
    Hi Mary
    +
    Hi Sara
    +
    + ``` + + ### Blockless use in a collection + + If you provide an `itemViewClass` option that has its own `template` you can + omit the block. + + The following template: + + ```handlebars + {{collection contentBinding="App.items" itemViewClass="App.AnItemView"}} + ``` + + And application code + + ```javascript + App = Ember.Application.create(); + App.items = [ + Ember.Object.create({name: 'Dave'}), + Ember.Object.create({name: 'Mary'}), + Ember.Object.create({name: 'Sara'}) + ]; + + App.AnItemView = Ember.View.extend({ + template: Ember.Handlebars.compile("Greetings {{view.content.name}}") + }); + ``` + + Will result in the HTML structure below + + ```html +
    +
    Greetings Dave
    +
    Greetings Mary
    +
    Greetings Sara
    +
    + ``` + + ### Specifying a CollectionView subclass + + By default the `{{collection}}` helper will create an instance of + `Ember.CollectionView`. You can supply a `Ember.CollectionView` subclass to + the helper by passing it as the first argument: + + ```handlebars + {{#collection App.MyCustomCollectionClass contentBinding="App.items"}} + Hi {{view.content.name}} + {{/collection}} + ``` + + ### Forwarded `item.*`-named Options + + As with the `{{view}}`, helper options passed to the `{{collection}}` will be + set on the resulting `Ember.CollectionView` as properties. Additionally, + options prefixed with `item` will be applied to the views rendered for each + item (note the camelcasing): + + ```handlebars + {{#collection contentBinding="App.items" + itemTagName="p" + itemClassNames="greeting"}} + Howdy {{view.content.name}} + {{/collection}} + ``` + + Will result in the following HTML structure: + + ```html +
    +

    Howdy Dave

    +

    Howdy Mary

    +

    Howdy Sara

    +
    + ``` + + @method collection + @for Ember.Handlebars.helpers + @param {String} path + @param {Hash} options + @return {String} HTML string + @deprecated Use `{{each}}` helper instead. + */ + function collectionHelper(path, options) { + Ember.deprecate("Using the {{collection}} helper without specifying a class has been deprecated as the {{each}} helper now supports the same functionality.", path !== 'collection'); + + // If no path is provided, treat path param as options. + if (path && path.data && path.data.isRenderData) { + options = path; + path = undefined; + Ember.assert("You cannot pass more than one argument to the collection helper", arguments.length === 1); + } else { + Ember.assert("You cannot pass more than one argument to the collection helper", arguments.length === 2); + } + + var fn = options.fn; + var data = options.data; + var inverse = options.inverse; + var view = options.data.view; + + + var controller, container; + // If passed a path string, convert that into an object. + // Otherwise, just default to the standard class. + var collectionClass; + if (path) { + controller = data.keywords.controller; + container = controller && controller.container; + collectionClass = handlebarsGet(this, path, options) || container.lookupFactory('view:' + path); + Ember.assert(fmt("%@ #collection: Could not find collection class %@", [data.view, path]), !!collectionClass); + } + else { + collectionClass = CollectionView; + } + + var hash = options.hash, itemHash = {}, match; + + // Extract item view class if provided else default to the standard class + var collectionPrototype = collectionClass.proto(), itemViewClass; + + if (hash.itemView) { + controller = data.keywords.controller; + Ember.assert('You specified an itemView, but the current context has no ' + + 'container to look the itemView up in. This probably means ' + + 'that you created a view manually, instead of through the ' + + 'container. Instead, use container.lookup("view:viewName"), ' + + 'which will properly instantiate your view.', + controller && controller.container); + container = controller.container; + itemViewClass = container.lookupFactory('view:' + hash.itemView); + Ember.assert('You specified the itemView ' + hash.itemView + ", but it was " + + "not found at " + container.describe("view:" + hash.itemView) + + " (and it was not registered in the container)", !!itemViewClass); + } else if (hash.itemViewClass) { + itemViewClass = handlebarsGet(collectionPrototype, hash.itemViewClass, options); + } else { + itemViewClass = collectionPrototype.itemViewClass; + } + + Ember.assert(fmt("%@ #collection: Could not find itemViewClass %@", [data.view, itemViewClass]), !!itemViewClass); + + delete hash.itemViewClass; + delete hash.itemView; + + // Go through options passed to the {{collection}} helper and extract options + // that configure item views instead of the collection itself. + for (var prop in hash) { + if (hash.hasOwnProperty(prop)) { + match = prop.match(/^item(.)(.*)$/); + + if (match && prop !== 'itemController') { + // Convert itemShouldFoo -> shouldFoo + itemHash[match[1].toLowerCase() + match[2]] = hash[prop]; + // Delete from hash as this will end up getting passed to the + // {{view}} helper method. + delete hash[prop]; + } + } + } + + if (fn) { + itemHash.template = fn; + delete options.fn; + } + + var emptyViewClass; + if (inverse && inverse !== EmberHandlebars.VM.noop) { + emptyViewClass = get(collectionPrototype, 'emptyViewClass'); + emptyViewClass = emptyViewClass.extend({ + template: inverse, + tagName: itemHash.tagName + }); + } else if (hash.emptyViewClass) { + emptyViewClass = handlebarsGet(this, hash.emptyViewClass, options); + } + if (emptyViewClass) { hash.emptyView = emptyViewClass; } + + if (hash.keyword) { + itemHash._context = this; + } else { + itemHash._context = alias('content'); + } + + var viewOptions = ViewHelper.propertiesFromHTMLOptions({ data: data, hash: itemHash }, this); + hash.itemViewClass = itemViewClass.extend(viewOptions); + + options.helperName = options.helperName || 'collection'; + + return helpers.view.call(this, collectionClass, options); + } + + __exports__["default"] = collectionHelper; + }); +define("ember-handlebars/helpers/debug", + ["ember-metal/core","ember-metal/utils","ember-metal/logger","ember-metal/property_get","ember-handlebars/ext","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __exports__) { + "use strict"; + /*jshint debug:true*/ + + /** + @module ember + @submodule ember-handlebars + */ + var Ember = __dependency1__["default"]; + // Ember.FEATURES, + var inspect = __dependency2__.inspect; + var Logger = __dependency3__["default"]; + + var get = __dependency4__.get; + var normalizePath = __dependency5__.normalizePath; + var handlebarsGet = __dependency5__.handlebarsGet; + + var a_slice = [].slice; + + /** + `log` allows you to output the value of variables in the current rendering + context. `log` also accepts primitive types such as strings or numbers. + + ```handlebars + {{log "myVariable:" myVariable }} + ``` + + @method log + @for Ember.Handlebars.helpers + @param {String} property + */ + function logHelper() { + var params = a_slice.call(arguments, 0, -1), + options = arguments[arguments.length - 1], + logger = Logger.log, + values = [], + allowPrimitives = true; + + for (var i = 0; i < params.length; i++) { + var type = options.types[i]; + + if (type === 'ID' || !allowPrimitives) { + var context = (options.contexts && options.contexts[i]) || this, + normalized = normalizePath(context, params[i], options.data); + + if (normalized.path === 'this') { + values.push(normalized.root); + } else { + values.push(handlebarsGet(normalized.root, normalized.path, options)); + } + } else { + values.push(params[i]); + } + } + + logger.apply(logger, values); + }; + + /** + Execute the `debugger` statement in the current context. + + ```handlebars + {{debugger}} + ``` + + Before invoking the `debugger` statement, there + are a few helpful variables defined in the + body of this helper that you can inspect while + debugging that describe how and where this + helper was invoked: + + - templateContext: this is most likely a controller + from which this template looks up / displays properties + - typeOfTemplateContext: a string description of + what the templateContext is + + For example, if you're wondering why a value `{{foo}}` + isn't rendering as expected within a template, you + could place a `{{debugger}}` statement, and when + the `debugger;` breakpoint is hit, you can inspect + `templateContext`, determine if it's the object you + expect, and/or evaluate expressions in the console + to perform property lookups on the `templateContext`: + + ``` + > templateContext.get('foo') // -> "" + ``` + + @method debugger + @for Ember.Handlebars.helpers + @param {String} property + */ + function debuggerHelper(options) { + + // These are helpful values you can inspect while debugging. + var templateContext = this; + var typeOfTemplateContext = inspect(templateContext); + + debugger; + } + + __exports__.logHelper = logHelper; + __exports__.debuggerHelper = debuggerHelper; + }); +define("ember-handlebars/helpers/each", + ["ember-metal/core","ember-handlebars-compiler","ember-runtime/system/string","ember-metal/property_get","ember-metal/property_set","ember-handlebars/views/metamorph_view","ember-views/views/collection_view","ember-metal/binding","ember-runtime/controllers/controller","ember-runtime/controllers/array_controller","ember-runtime/mixins/array","ember-runtime/copy","ember-metal/run_loop","ember-metal/observer","ember-metal/events","ember-handlebars/ext","ember-metal/computed","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __dependency11__, __dependency12__, __dependency13__, __dependency14__, __dependency15__, __dependency16__, __dependency17__, __exports__) { + "use strict"; + + /** + @module ember + @submodule ember-handlebars + */ + var Ember = __dependency1__["default"]; + // Ember.assert;, Ember.K + // var emberAssert = Ember.assert, + var K = Ember.K; + + var EmberHandlebars = __dependency2__["default"]; + var helpers = EmberHandlebars.helpers; + + var fmt = __dependency3__.fmt; + var get = __dependency4__.get; + var set = __dependency5__.set; + var _Metamorph = __dependency6__._Metamorph; + var _MetamorphView = __dependency6__._MetamorphView; + var CollectionView = __dependency7__["default"]; + var Binding = __dependency8__.Binding; + var ControllerMixin = __dependency9__.ControllerMixin; + var ArrayController = __dependency10__["default"]; + var EmberArray = __dependency11__["default"]; + var copy = __dependency12__["default"]; + var run = __dependency13__["default"]; + var addObserver = __dependency14__.addObserver; + var removeObserver = __dependency14__.removeObserver; + var addBeforeObserver = __dependency14__.addBeforeObserver; + var removeBeforeObserver = __dependency14__.removeBeforeObserver; + var on = __dependency15__.on; + var handlebarsGet = __dependency16__.handlebarsGet; + var computed = __dependency17__.computed; + + var handlebarsGet = __dependency16__.handlebarsGet; + + var EachView = CollectionView.extend(_Metamorph, { + + init: function() { + var itemController = get(this, 'itemController'); + var binding; + + if (itemController) { + var controller = get(this, 'controller.container').lookupFactory('controller:array').create({ + _isVirtual: true, + parentController: get(this, 'controller'), + itemController: itemController, + target: get(this, 'controller'), + _eachView: this + }); + + this.disableContentObservers(function() { + set(this, 'content', controller); + binding = new Binding('content', '_eachView.dataSource').oneWay(); + binding.connect(controller); + }); + + set(this, '_arrayController', controller); + } else { + this.disableContentObservers(function() { + binding = new Binding('content', 'dataSource').oneWay(); + binding.connect(this); + }); + } + + return this._super(); + }, + + _assertArrayLike: function(content) { + Ember.assert(fmt("The value that #each loops over must be an Array. You " + + "passed %@, but it should have been an ArrayController", + [content.constructor]), + !ControllerMixin.detect(content) || + (content && content.isGenerated) || + content instanceof ArrayController); + Ember.assert(fmt("The value that #each loops over must be an Array. You passed %@", [(ControllerMixin.detect(content) && content.get('model') !== undefined) ? fmt("'%@' (wrapped in %@)", [content.get('model'), content]) : content]), EmberArray.detect(content)); + }, + + disableContentObservers: function(callback) { + removeBeforeObserver(this, 'content', null, '_contentWillChange'); + removeObserver(this, 'content', null, '_contentDidChange'); + + callback.call(this); + + addBeforeObserver(this, 'content', null, '_contentWillChange'); + addObserver(this, 'content', null, '_contentDidChange'); + }, + + itemViewClass: _MetamorphView, + emptyViewClass: _MetamorphView, + + createChildView: function(view, attrs) { + view = this._super(view, attrs); + + // At the moment, if a container view subclass wants + // to insert keywords, it is responsible for cloning + // the keywords hash. This will be fixed momentarily. + var keyword = get(this, 'keyword'); + var content = get(view, 'content'); + + if (keyword) { + var data = get(view, 'templateData'); + + data = copy(data); + data.keywords = view.cloneKeywords(); + set(view, 'templateData', data); + + // In this case, we do not bind, because the `content` of + // a #each item cannot change. + data.keywords[keyword] = content; + } + + // If {{#each}} is looping over an array of controllers, + // point each child view at their respective controller. + if (content && content.isController) { + set(view, 'controller', content); + } + + return view; + }, + + destroy: function() { + if (!this._super()) { return; } + + var arrayController = get(this, '_arrayController'); + + if (arrayController) { + arrayController.destroy(); + } + + return this; + } + }); + + // Defeatureify doesn't seem to like nested functions that need to be removed + function _addMetamorphCheck() { + EachView.reopen({ + _checkMetamorph: on('didInsertElement', function() { + Ember.assert("The metamorph tags, " + + this.morph.start + " and " + this.morph.end + + ", have different parents.\nThe browser has fixed your template to output valid HTML (for example, check that you have properly closed all tags and have used a TBODY tag when creating a table with '{{#each}}')", + document.getElementById( this.morph.start ).parentNode === + document.getElementById( this.morph.end ).parentNode + ); + }) + }); + } + + // until ember-debug is es6ed + var runInDebug = function(f){f()}; + runInDebug( function() { + _addMetamorphCheck(); + }); + + var GroupedEach = EmberHandlebars.GroupedEach = function(context, path, options) { + var self = this, + normalized = EmberHandlebars.normalizePath(context, path, options.data); + + this.context = context; + this.path = path; + this.options = options; + this.template = options.fn; + this.containingView = options.data.view; + this.normalizedRoot = normalized.root; + this.normalizedPath = normalized.path; + this.content = this.lookupContent(); + + this.addContentObservers(); + this.addArrayObservers(); + + this.containingView.on('willClearRender', function() { + self.destroy(); + }); + }; + + GroupedEach.prototype = { + contentWillChange: function() { + this.removeArrayObservers(); + }, + + contentDidChange: function() { + this.content = this.lookupContent(); + this.addArrayObservers(); + this.rerenderContainingView(); + }, + + contentArrayWillChange: K, + + contentArrayDidChange: function() { + this.rerenderContainingView(); + }, + + lookupContent: function() { + return handlebarsGet(this.normalizedRoot, this.normalizedPath, this.options); + }, + + addArrayObservers: function() { + if (!this.content) { return; } + + this.content.addArrayObserver(this, { + willChange: 'contentArrayWillChange', + didChange: 'contentArrayDidChange' + }); + }, + + removeArrayObservers: function() { + if (!this.content) { return; } + + this.content.removeArrayObserver(this, { + willChange: 'contentArrayWillChange', + didChange: 'contentArrayDidChange' + }); + }, + + addContentObservers: function() { + addBeforeObserver(this.normalizedRoot, this.normalizedPath, this, this.contentWillChange); + addObserver(this.normalizedRoot, this.normalizedPath, this, this.contentDidChange); + }, + + removeContentObservers: function() { + removeBeforeObserver(this.normalizedRoot, this.normalizedPath, this.contentWillChange); + removeObserver(this.normalizedRoot, this.normalizedPath, this.contentDidChange); + }, + + render: function() { + if (!this.content) { return; } + + var content = this.content, + contentLength = get(content, 'length'), + data = this.options.data, + template = this.template; + + data.insideEach = true; + for (var i = 0; i < contentLength; i++) { + template(content.objectAt(i), { data: data }); + } + }, + + rerenderContainingView: function() { + var self = this; + run.scheduleOnce('render', this, function() { + // It's possible it's been destroyed after we enqueued a re-render call. + if (!self.destroyed) { + self.containingView.rerender(); + } + }); + }, + + destroy: function() { + this.removeContentObservers(); + if (this.content) { + this.removeArrayObservers(); + } + this.destroyed = true; + } + }; + + /** + The `{{#each}}` helper loops over elements in a collection, rendering its + block once for each item. It is an extension of the base Handlebars `{{#each}}` + helper: + + ```javascript + Developers = [{name: 'Yehuda'},{name: 'Tom'}, {name: 'Paul'}]; + ``` + + ```handlebars + {{#each Developers}} + {{name}} + {{/each}} + ``` + + `{{each}}` supports an alternative syntax with element naming: + + ```handlebars + {{#each person in Developers}} + {{person.name}} + {{/each}} + ``` + + When looping over objects that do not have properties, `{{this}}` can be used + to render the object: + + ```javascript + DeveloperNames = ['Yehuda', 'Tom', 'Paul'] + ``` + + ```handlebars + {{#each DeveloperNames}} + {{this}} + {{/each}} + ``` + ### {{else}} condition + `{{#each}}` can have a matching `{{else}}`. The contents of this block will render + if the collection is empty. + + ``` + {{#each person in Developers}} + {{person.name}} + {{else}} +

    Sorry, nobody is available for this task.

    + {{/each}} + ``` + ### Specifying a View class for items + If you provide an `itemViewClass` option that references a view class + with its own `template` you can omit the block. + + The following template: + + ```handlebars + {{#view App.MyView }} + {{each view.items itemViewClass="App.AnItemView"}} + {{/view}} + ``` + + And application code + + ```javascript + App = Ember.Application.create({ + MyView: Ember.View.extend({ + items: [ + Ember.Object.create({name: 'Dave'}), + Ember.Object.create({name: 'Mary'}), + Ember.Object.create({name: 'Sara'}) + ] + }) + }); + + App.AnItemView = Ember.View.extend({ + template: Ember.Handlebars.compile("Greetings {{name}}") + }); + ``` + + Will result in the HTML structure below + + ```html +
    +
    Greetings Dave
    +
    Greetings Mary
    +
    Greetings Sara
    +
    + ``` + + If an `itemViewClass` is defined on the helper, and therefore the helper is not + being used as a block, an `emptyViewClass` can also be provided optionally. + The `emptyViewClass` will match the behavior of the `{{else}}` condition + described above. That is, the `emptyViewClass` will render if the collection + is empty. + + ### Representing each item with a Controller. + By default the controller lookup within an `{{#each}}` block will be + the controller of the template where the `{{#each}}` was used. If each + item needs to be presented by a custom controller you can provide a + `itemController` option which references a controller by lookup name. + Each item in the loop will be wrapped in an instance of this controller + and the item itself will be set to the `content` property of that controller. + + This is useful in cases where properties of model objects need transformation + or synthesis for display: + + ```javascript + App.DeveloperController = Ember.ObjectController.extend({ + isAvailableForHire: function() { + return !this.get('content.isEmployed') && this.get('content.isSeekingWork'); + }.property('isEmployed', 'isSeekingWork') + }) + ``` + + ```handlebars + {{#each person in developers itemController="developer"}} + {{person.name}} {{#if person.isAvailableForHire}}Hire me!{{/if}} + {{/each}} + ``` + + Each itemController will receive a reference to the current controller as + a `parentController` property. + + ### (Experimental) Grouped Each + + When used in conjunction with the experimental [group helper](https://github.com/emberjs/group-helper), + you can inform Handlebars to re-render an entire group of items instead of + re-rendering them one at a time (in the event that they are changed en masse + or an item is added/removed). + + ```handlebars + {{#group}} + {{#each people}} + {{firstName}} {{lastName}} + {{/each}} + {{/group}} + ``` + + This can be faster than the normal way that Handlebars re-renders items + in some cases. + + If for some reason you have a group with more than one `#each`, you can make + one of the collections be updated in normal (non-grouped) fashion by setting + the option `groupedRows=true` (counter-intuitive, I know). + + For example, + + ```handlebars + {{dealershipName}} + + {{#group}} + {{#each dealers}} + {{firstName}} {{lastName}} + {{/each}} + + {{#each car in cars groupedRows=true}} + {{car.make}} {{car.model}} {{car.color}} + {{/each}} + {{/group}} + ``` + Any change to `dealershipName` or the `dealers` collection will cause the + entire group to be re-rendered. However, changes to the `cars` collection + will be re-rendered individually (as normal). + + Note that `group` behavior is also disabled by specifying an `itemViewClass`. + + @method each + @for Ember.Handlebars.helpers + @param [name] {String} name for item (used with `in`) + @param [path] {String} path + @param [options] {Object} Handlebars key/value pairs of options + @param [options.itemViewClass] {String} a path to a view class used for each item + @param [options.itemController] {String} name of a controller to be created for each item + @param [options.groupedRows] {boolean} enable normal item-by-item rendering when inside a `#group` helper + */ + function eachHelper(path, options) { + var ctx, helperName = 'each'; + + if (arguments.length === 4) { + Ember.assert("If you pass more than one argument to the each helper, it must be in the form #each foo in bar", arguments[1] === "in"); + + var keywordName = arguments[0]; + + + options = arguments[3]; + path = arguments[2]; + + helperName += ' ' + keywordName + ' in ' + path; + + if (path === '') { path = "this"; } + + options.hash.keyword = keywordName; + + } else if (arguments.length === 1) { + options = path; + path = 'this'; + } else { + helperName += ' ' + path; + } + + options.hash.dataSourceBinding = path; + // Set up emptyView as a metamorph with no tag + //options.hash.emptyViewClass = Ember._MetamorphView; + + // can't rely on this default behavior when use strict + ctx = this || window; + + options.helperName = options.helperName || helperName; + + if (options.data.insideGroup && !options.hash.groupedRows && !options.hash.itemViewClass) { + new GroupedEach(ctx, path, options).render(); + } else { + // ES6TODO: figure out how to do this without global lookup. + return helpers.collection.call(ctx, 'Ember.Handlebars.EachView', options); + } + } + + __exports__.EachView = EachView; + __exports__.GroupedEach = GroupedEach; + __exports__.eachHelper = eachHelper; + }); +define("ember-handlebars/helpers/loc", + ["ember-runtime/system/string","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var loc = __dependency1__.loc; + + /** + @module ember + @submodule ember-handlebars + */ + + // ES6TODO: + // Pretty sure this can be expressed as + // var locHelper EmberStringUtils.loc ? + + /** + Calls [Ember.String.loc](/api/classes/Ember.String.html#method_loc) with the + provided string. + + This is a convenient way to localize text. For example: + + ```html + + ``` + + Take note that `"welcome"` is a string and not an object + reference. + + See [Ember.String.loc](/api/classes/Ember.String.html#method_loc) for how to + set up localized string references. + + @method loc + @for Ember.Handlebars.helpers + @param {String} str The string to format + @see {Ember.String#loc} + */ + function locHelper(str) { + return loc(str); + } + + __exports__["default"] = locHelper; + }); +define("ember-handlebars/helpers/partial", + ["ember-metal/core","ember-metal/is_none","ember-handlebars/ext","ember-handlebars/helpers/binding","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __exports__) { + "use strict"; + var Ember = __dependency1__["default"]; + // Ember.assert + // var emberAssert = Ember.assert; + + var isNone = __dependency2__.isNone; + var handlebarsGet = __dependency3__.handlebarsGet; + var bind = __dependency4__.bind; + + /** + @module ember + @submodule ember-handlebars + */ + + /** + The `partial` helper renders another template without + changing the template context: + + ```handlebars + {{foo}} + {{partial "nav"}} + ``` + + The above example template will render a template named + "_nav", which has the same context as the parent template + it's rendered into, so if the "_nav" template also referenced + `{{foo}}`, it would print the same thing as the `{{foo}}` + in the above example. + + If a "_nav" template isn't found, the `partial` helper will + fall back to a template named "nav". + + ## Bound template names + + The parameter supplied to `partial` can also be a path + to a property containing a template name, e.g.: + + ```handlebars + {{partial someTemplateName}} + ``` + + The above example will look up the value of `someTemplateName` + on the template context (e.g. a controller) and use that + value as the name of the template to render. If the resolved + value is falsy, nothing will be rendered. If `someTemplateName` + changes, the partial will be re-rendered using the new template + name. + + ## Setting the partial's context with `with` + + The `partial` helper can be used in conjunction with the `with` + helper to set a context that will be used by the partial: + + ```handlebars + {{#with currentUser}} + {{partial "user_info"}} + {{/with}} + ``` + + @method partial + @for Ember.Handlebars.helpers + @param {String} partialName the name of the template to render minus the leading underscore + */ + + function partialHelper(name, options) { + + var context = (options.contexts && options.contexts.length) ? options.contexts[0] : this; + + options.helperName = options.helperName || 'partial'; + + if (options.types[0] === "ID") { + // Helper was passed a property path; we need to + // create a binding that will re-render whenever + // this property changes. + options.fn = function(context, fnOptions) { + var partialName = handlebarsGet(context, name, fnOptions); + renderPartial(context, partialName, fnOptions); + }; + + return bind.call(context, name, options, true, exists); + } else { + // Render the partial right into parent template. + renderPartial(context, name, options); + } + } + + function exists(value) { + return !isNone(value); + } + + function renderPartial(context, name, options) { + var nameParts = name.split("/"); + var lastPart = nameParts[nameParts.length - 1]; + + nameParts[nameParts.length - 1] = "_" + lastPart; + + var view = options.data.view; + var underscoredName = nameParts.join("/"); + var template = view.templateForName(underscoredName); + var deprecatedTemplate = !template && view.templateForName(name); + + Ember.assert("Unable to find partial with name '"+name+"'.", template || deprecatedTemplate); + + template = template || deprecatedTemplate; + + template(context, { data: options.data }); + } + + __exports__["default"] = partialHelper; + }); +define("ember-handlebars/helpers/shared", + ["ember-handlebars/ext","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var handlebarsGet = __dependency1__.handlebarsGet; + + function resolvePaths(options) { + var ret = [], + contexts = options.contexts, + roots = options.roots, + data = options.data; + + for (var i=0, l=contexts.length; i + {{#with loggedInUser}} + Last Login: {{lastLogin}} + User Info: {{template "user_info"}} + {{/with}} + + ``` + + ```html + + ``` + + ```handlebars + {{#if isUser}} + {{template "user_info"}} + {{else}} + {{template "unlogged_user_info"}} + {{/if}} + ``` + + This helper looks for templates in the global `Ember.TEMPLATES` hash. If you + add `"; + return testEl.firstChild.innerHTML === ''; + })(); + + // IE 8 (and likely earlier) likes to move whitespace preceeding + // a script tag to appear after it. This means that we can + // accidentally remove whitespace when updating a morph. + var movesWhitespace = typeof document !== 'undefined' && (function() { + var testEl = document.createElement('div'); + testEl.innerHTML = "Test: Value"; + return testEl.childNodes[0].nodeValue === 'Test:' && + testEl.childNodes[2].nodeValue === ' Value'; + })(); + + // Use this to find children by ID instead of using jQuery + var findChildById = function(element, id) { + if (element.getAttribute('id') === id) { return element; } + + var len = element.childNodes.length, idx, node, found; + for (idx=0; idx 0) { + var len = matches.length, idx; + for (idx=0; idxTest'); + canSet = el.options.length === 1; + } + + innerHTMLTags[tagName] = canSet; + + return canSet; + }; + + var setInnerHTML = function(element, html) { + var tagName = element.tagName; + + if (canSetInnerHTML(tagName)) { + setInnerHTMLWithoutFix(element, html); + } else { + // Firefox versions < 11 do not have support for element.outerHTML. + var outerHTML = element.outerHTML || new XMLSerializer().serializeToString(element); + + var startTag = outerHTML.match(new RegExp("<"+tagName+"([^>]*)>", 'i'))[0], + endTag = ''; + + var wrapper = document.createElement('div'); + setInnerHTMLWithoutFix(wrapper, startTag + html + endTag); + element = wrapper.firstChild; + while (element.tagName !== tagName) { + element = element.nextSibling; + } + } + + return element; + }; + + function isSimpleClick(event) { + var modifier = event.shiftKey || event.metaKey || event.altKey || event.ctrlKey, + secondaryClick = event.which > 1; // IE9 may return undefined + + return !modifier && !secondaryClick; + } + + __exports__.setInnerHTML = setInnerHTML; + __exports__.isSimpleClick = isSimpleClick; + }); +define("ember-views/views/collection_view", + ["ember-metal/core","ember-metal/platform","ember-metal/binding","ember-metal/merge","ember-metal/property_get","ember-metal/property_set","ember-runtime/system/string","ember-views/views/container_view","ember-views/views/view","ember-metal/mixin","ember-runtime/mixins/array","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __dependency11__, __exports__) { + "use strict"; + + /** + @module ember + @submodule ember-views + */ + + var Ember = __dependency1__["default"]; + // Ember.assert + var create = __dependency2__.create; + var isGlobalPath = __dependency3__.isGlobalPath; + var merge = __dependency4__["default"]; + var get = __dependency5__.get; + var set = __dependency6__.set; + var fmt = __dependency7__.fmt; + var ContainerView = __dependency8__["default"]; + var CoreView = __dependency9__.CoreView; + var View = __dependency9__.View; + var observer = __dependency10__.observer; + var beforeObserver = __dependency10__.beforeObserver; + var EmberArray = __dependency11__["default"]; + + /** + `Ember.CollectionView` is an `Ember.View` descendent responsible for managing + a collection (an array or array-like object) by maintaining a child view object + and associated DOM representation for each item in the array and ensuring + that child views and their associated rendered HTML are updated when items in + the array are added, removed, or replaced. + + ## Setting content + + The managed collection of objects is referenced as the `Ember.CollectionView` + instance's `content` property. + + ```javascript + someItemsView = Ember.CollectionView.create({ + content: ['A', 'B','C'] + }) + ``` + + The view for each item in the collection will have its `content` property set + to the item. + + ## Specifying itemViewClass + + By default the view class for each item in the managed collection will be an + instance of `Ember.View`. You can supply a different class by setting the + `CollectionView`'s `itemViewClass` property. + + Given an empty `` and the following code: + + ```javascript + someItemsView = Ember.CollectionView.create({ + classNames: ['a-collection'], + content: ['A','B','C'], + itemViewClass: Ember.View.extend({ + template: Ember.Handlebars.compile("the letter: {{view.content}}") + }) + }); + + someItemsView.appendTo('body'); + ``` + + Will result in the following HTML structure + + ```html +
    +
    the letter: A
    +
    the letter: B
    +
    the letter: C
    +
    + ``` + + ## Automatic matching of parent/child tagNames + + Setting the `tagName` property of a `CollectionView` to any of + "ul", "ol", "table", "thead", "tbody", "tfoot", "tr", or "select" will result + in the item views receiving an appropriately matched `tagName` property. + + Given an empty `` and the following code: + + ```javascript + anUnorderedListView = Ember.CollectionView.create({ + tagName: 'ul', + content: ['A','B','C'], + itemViewClass: Ember.View.extend({ + template: Ember.Handlebars.compile("the letter: {{view.content}}") + }) + }); + + anUnorderedListView.appendTo('body'); + ``` + + Will result in the following HTML structure + + ```html +
      +
    • the letter: A
    • +
    • the letter: B
    • +
    • the letter: C
    • +
    + ``` + + Additional `tagName` pairs can be provided by adding to + `Ember.CollectionView.CONTAINER_MAP ` + + ```javascript + Ember.CollectionView.CONTAINER_MAP['article'] = 'section' + ``` + + ## Programmatic creation of child views + + For cases where additional customization beyond the use of a single + `itemViewClass` or `tagName` matching is required CollectionView's + `createChildView` method can be overidden: + + ```javascript + CustomCollectionView = Ember.CollectionView.extend({ + createChildView: function(viewClass, attrs) { + if (attrs.content.kind == 'album') { + viewClass = App.AlbumView; + } else { + viewClass = App.SongView; + } + return this._super(viewClass, attrs); + } + }); + ``` + + ## Empty View + + You can provide an `Ember.View` subclass to the `Ember.CollectionView` + instance as its `emptyView` property. If the `content` property of a + `CollectionView` is set to `null` or an empty array, an instance of this view + will be the `CollectionView`s only child. + + ```javascript + aListWithNothing = Ember.CollectionView.create({ + classNames: ['nothing'] + content: null, + emptyView: Ember.View.extend({ + template: Ember.Handlebars.compile("The collection is empty") + }) + }); + + aListWithNothing.appendTo('body'); + ``` + + Will result in the following HTML structure + + ```html +
    +
    + The collection is empty +
    +
    + ``` + + ## Adding and Removing items + + The `childViews` property of a `CollectionView` should not be directly + manipulated. Instead, add, remove, replace items from its `content` property. + This will trigger appropriate changes to its rendered HTML. + + + @class CollectionView + @namespace Ember + @extends Ember.ContainerView + @since Ember 0.9 + */ + var CollectionView = ContainerView.extend({ + + /** + A list of items to be displayed by the `Ember.CollectionView`. + + @property content + @type Ember.Array + @default null + */ + content: null, + + /** + This provides metadata about what kind of empty view class this + collection would like if it is being instantiated from another + system (like Handlebars) + + @private + @property emptyViewClass + */ + emptyViewClass: View, + + /** + An optional view to display if content is set to an empty array. + + @property emptyView + @type Ember.View + @default null + */ + emptyView: null, + + /** + @property itemViewClass + @type Ember.View + @default Ember.View + */ + itemViewClass: View, + + /** + Setup a CollectionView + + @method init + */ + init: function() { + var ret = this._super(); + this._contentDidChange(); + return ret; + }, + + /** + Invoked when the content property is about to change. Notifies observers that the + entire array content will change. + + @private + @method _contentWillChange + */ + _contentWillChange: beforeObserver('content', function() { + var content = this.get('content'); + + if (content) { content.removeArrayObserver(this); } + var len = content ? get(content, 'length') : 0; + this.arrayWillChange(content, 0, len); + }), + + /** + Check to make sure that the content has changed, and if so, + update the children directly. This is always scheduled + asynchronously, to allow the element to be created before + bindings have synchronized and vice versa. + + @private + @method _contentDidChange + */ + _contentDidChange: observer('content', function() { + var content = get(this, 'content'); + + if (content) { + this._assertArrayLike(content); + content.addArrayObserver(this); + } + + var len = content ? get(content, 'length') : 0; + this.arrayDidChange(content, 0, null, len); + }), + + /** + Ensure that the content implements Ember.Array + + @private + @method _assertArrayLike + */ + _assertArrayLike: function(content) { + }, + + /** + Removes the content and content observers. + + @method destroy + */ + destroy: function() { + if (!this._super()) { return; } + + var content = get(this, 'content'); + if (content) { content.removeArrayObserver(this); } + + if (this._createdEmptyView) { + this._createdEmptyView.destroy(); + } + + return this; + }, + + /** + Called when a mutation to the underlying content array will occur. + + This method will remove any views that are no longer in the underlying + content array. + + Invokes whenever the content array itself will change. + + @method arrayWillChange + @param {Array} content the managed collection of objects + @param {Number} start the index at which the changes will occurr + @param {Number} removed number of object to be removed from content + */ + arrayWillChange: function(content, start, removedCount) { + // If the contents were empty before and this template collection has an + // empty view remove it now. + var emptyView = get(this, 'emptyView'); + if (emptyView && emptyView instanceof View) { + emptyView.removeFromParent(); + } + + // Loop through child views that correspond with the removed items. + // Note that we loop from the end of the array to the beginning because + // we are mutating it as we go. + var childViews = this._childViews, childView, idx, len; + + len = this._childViews.length; + + var removingAll = removedCount === len; + + if (removingAll) { + this.currentState.empty(this); + this.invokeRecursively(function(view) { + view.removedFromDOM = true; + }, false); + } + + for (idx = start + removedCount - 1; idx >= start; idx--) { + childView = childViews[idx]; + childView.destroy(); + } + }, + + /** + Called when a mutation to the underlying content array occurs. + + This method will replay that mutation against the views that compose the + `Ember.CollectionView`, ensuring that the view reflects the model. + + This array observer is added in `contentDidChange`. + + @method arrayDidChange + @param {Array} content the managed collection of objects + @param {Number} start the index at which the changes occurred + @param {Number} removed number of object removed from content + @param {Number} added number of object added to content + */ + arrayDidChange: function(content, start, removed, added) { + var addedViews = [], view, item, idx, len, itemViewClass, + emptyView; + + len = content ? get(content, 'length') : 0; + + if (len) { + itemViewClass = get(this, 'itemViewClass'); + + if ('string' === typeof itemViewClass && isGlobalPath(itemViewClass)) { + itemViewClass = get(itemViewClass) || itemViewClass; + } + + + for (idx = start; idx < start+added; idx++) { + item = content.objectAt(idx); + + view = this.createChildView(itemViewClass, { + content: item, + contentIndex: idx + }); + + addedViews.push(view); + } + } else { + emptyView = get(this, 'emptyView'); + + if (!emptyView) { return; } + + if ('string' === typeof emptyView && isGlobalPath(emptyView)) { + emptyView = get(emptyView) || emptyView; + } + + emptyView = this.createChildView(emptyView); + addedViews.push(emptyView); + set(this, 'emptyView', emptyView); + + if (CoreView.detect(emptyView)) { + this._createdEmptyView = emptyView; + } + } + + this.replace(start, 0, addedViews); + }, + + /** + Instantiates a view to be added to the childViews array during view + initialization. You generally will not call this method directly unless + you are overriding `createChildViews()`. Note that this method will + automatically configure the correct settings on the new view instance to + act as a child of the parent. + + The tag name for the view will be set to the tagName of the viewClass + passed in. + + @method createChildView + @param {Class} viewClass + @param {Hash} [attrs] Attributes to add + @return {Ember.View} new instance + */ + createChildView: function(view, attrs) { + view = this._super(view, attrs); + + var itemTagName = get(view, 'tagName'); + + if (itemTagName === null || itemTagName === undefined) { + itemTagName = CollectionView.CONTAINER_MAP[get(this, 'tagName')]; + set(view, 'tagName', itemTagName); + } + + return view; + } + }); + + /** + A map of parent tags to their default child tags. You can add + additional parent tags if you want collection views that use + a particular parent tag to default to a child tag. + + @property CONTAINER_MAP + @type Hash + @static + @final + */ + CollectionView.CONTAINER_MAP = { + ul: 'li', + ol: 'li', + table: 'tr', + thead: 'tr', + tbody: 'tr', + tfoot: 'tr', + tr: 'td', + select: 'option' + }; + + __exports__["default"] = CollectionView; + }); +define("ember-views/views/component", + ["ember-metal/core","ember-views/mixins/component_template_deprecation","ember-runtime/mixins/target_action_support","ember-views/views/view","ember-metal/property_get","ember-metal/property_set","ember-metal/is_none","ember-metal/computed","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __exports__) { + "use strict"; + var Ember = __dependency1__["default"]; + // Ember.assert, Ember.Handlebars + + var ComponentTemplateDeprecation = __dependency2__["default"]; + var TargetActionSupport = __dependency3__["default"]; + var View = __dependency4__.View;var get = __dependency5__.get; + var set = __dependency6__.set; + var isNone = __dependency7__.isNone; + + var computed = __dependency8__.computed; + + var a_slice = Array.prototype.slice; + + /** + @module ember + @submodule ember-views + */ + + /** + An `Ember.Component` is a view that is completely + isolated. Property access in its templates go + to the view object and actions are targeted at + the view object. There is no access to the + surrounding context or outer controller; all + contextual information must be passed in. + + The easiest way to create an `Ember.Component` is via + a template. If you name a template + `components/my-foo`, you will be able to use + `{{my-foo}}` in other templates, which will make + an instance of the isolated component. + + ```handlebars + {{app-profile person=currentUser}} + ``` + + ```handlebars + +

    {{person.title}}

    + +

    {{person.signature}}

    + ``` + + You can use `yield` inside a template to + include the **contents** of any block attached to + the component. The block will be executed in the + context of the surrounding context or outer controller: + + ```handlebars + {{#app-profile person=currentUser}} +

    Admin mode

    + {{! Executed in the controller's context. }} + {{/app-profile}} + ``` + + ```handlebars + +

    {{person.title}}

    + {{! Executed in the components context. }} + {{yield}} {{! block contents }} + ``` + + If you want to customize the component, in order to + handle events or actions, you implement a subclass + of `Ember.Component` named after the name of the + component. Note that `Component` needs to be appended to the name of + your subclass like `AppProfileComponent`. + + For example, you could implement the action + `hello` for the `app-profile` component: + + ```javascript + App.AppProfileComponent = Ember.Component.extend({ + actions: { + hello: function(name) { + console.log("Hello", name); + } + } + }); + ``` + + And then use it in the component's template: + + ```handlebars + + +

    {{person.title}}

    + {{yield}} + + + ``` + + Components must have a `-` in their name to avoid + conflicts with built-in controls that wrap HTML + elements. This is consistent with the same + requirement in web components. + + @class Component + @namespace Ember + @extends Ember.View + */ + var Component = View.extend(TargetActionSupport, ComponentTemplateDeprecation, { + instrumentName: 'component', + instrumentDisplay: computed(function() { + if (this._debugContainerKey) { + return '{{' + this._debugContainerKey.split(':')[1] + '}}'; + } + }), + + init: function() { + this._super(); + set(this, 'context', this); + set(this, 'controller', this); + }, + + defaultLayout: function(context, options){ + Ember.Handlebars.helpers['yield'].call(context, options); + }, + + /** + A components template property is set by passing a block + during its invocation. It is executed within the parent context. + + Example: + + ```handlebars + {{#my-component}} + // something that is run in the context + // of the parent context + {{/my-component}} + ``` + + Specifying a template directly to a component is deprecated without + also specifying the layout property. + + @deprecated + @property template + */ + template: computed(function(key, value) { + if (value !== undefined) { return value; } + + var templateName = get(this, 'templateName'), + template = this.templateForName(templateName, 'template'); + + + return template || get(this, 'defaultTemplate'); + }).property('templateName'), + + /** + Specifying a components `templateName` is deprecated without also + providing the `layout` or `layoutName` properties. + + @deprecated + @property templateName + */ + templateName: null, + + // during render, isolate keywords + cloneKeywords: function() { + return { + view: this, + controller: this + }; + }, + + _yield: function(context, options) { + var view = options.data.view, + parentView = this._parentView, + template = get(this, 'template'); + + if (template) { + + view.appendChild(View, { + isVirtual: true, + tagName: '', + _contextView: parentView, + template: template, + context: get(parentView, 'context'), + controller: get(parentView, 'controller'), + templateData: { keywords: parentView.cloneKeywords() } + }); + } + }, + + /** + If the component is currently inserted into the DOM of a parent view, this + property will point to the controller of the parent view. + + @property targetObject + @type Ember.Controller + @default null + */ + targetObject: computed(function(key) { + var parentView = get(this, '_parentView'); + return parentView ? get(parentView, 'controller') : null; + }).property('_parentView'), + + /** + Triggers a named action on the controller context where the component is used if + this controller has registered for notifications of the action. + + For example a component for playing or pausing music may translate click events + into action notifications of "play" or "stop" depending on some internal state + of the component: + + + ```javascript + App.PlayButtonComponent = Ember.Component.extend({ + click: function(){ + if (this.get('isPlaying')) { + this.sendAction('play'); + } else { + this.sendAction('stop'); + } + } + }); + ``` + + When used inside a template these component actions are configured to + trigger actions in the outer application context: + + ```handlebars + {{! application.hbs }} + {{play-button play="musicStarted" stop="musicStopped"}} + ``` + + When the component receives a browser `click` event it translate this + interaction into application-specific semantics ("play" or "stop") and + triggers the specified action name on the controller for the template + where the component is used: + + + ```javascript + App.ApplicationController = Ember.Controller.extend({ + actions: { + musicStarted: function(){ + // called when the play button is clicked + // and the music started playing + }, + musicStopped: function(){ + // called when the play button is clicked + // and the music stopped playing + } + } + }); + ``` + + If no action name is passed to `sendAction` a default name of "action" + is assumed. + + ```javascript + App.NextButtonComponent = Ember.Component.extend({ + click: function(){ + this.sendAction(); + } + }); + ``` + + ```handlebars + {{! application.hbs }} + {{next-button action="playNextSongInAlbum"}} + ``` + + ```javascript + App.ApplicationController = Ember.Controller.extend({ + actions: { + playNextSongInAlbum: function(){ + ... + } + } + }); + ``` + + @method sendAction + @param [action] {String} the action to trigger + @param [context] {*} a context to send with the action + */ + sendAction: function(action) { + var actionName, + contexts = a_slice.call(arguments, 1); + + // Send the default action + if (action === undefined) { + actionName = get(this, 'action'); + } else { + actionName = get(this, action); + } + + // If no action name for that action could be found, just abort. + if (actionName === undefined) { return; } + + this.triggerAction({ + action: actionName, + actionContext: contexts + }); + } + }); + + __exports__["default"] = Component; + }); +define("ember-views/views/container_view", + ["ember-metal/core","ember-metal/merge","ember-runtime/mixins/mutable_array","ember-metal/property_get","ember-metal/property_set","ember-views/views/view","ember-views/views/states","ember-metal/error","ember-metal/enumerable_utils","ember-metal/computed","ember-metal/run_loop","ember-metal/properties","ember-views/system/render_buffer","ember-metal/mixin","ember-runtime/system/native_array","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __dependency11__, __dependency12__, __dependency13__, __dependency14__, __dependency15__, __exports__) { + "use strict"; + var Ember = __dependency1__["default"]; + // Ember.assert, Ember.K + + var merge = __dependency2__["default"]; + var MutableArray = __dependency3__["default"]; + var get = __dependency4__.get; + var set = __dependency5__.set; + + var View = __dependency6__.View; + var ViewCollection = __dependency6__.ViewCollection; + var cloneStates = __dependency7__.cloneStates; + var EmberViewStates = __dependency7__.states; + + var EmberError = __dependency8__["default"]; + + // ES6TODO: functions on EnumerableUtils should get their own export + var EnumerableUtils = __dependency9__["default"]; + var forEach = EnumerableUtils.forEach; + + var computed = __dependency10__.computed; + var run = __dependency11__["default"]; + var defineProperty = __dependency12__.defineProperty; + var RenderBuffer = __dependency13__["default"]; + var observer = __dependency14__.observer; + var beforeObserver = __dependency14__.beforeObserver; + var A = __dependency15__.A; + + /** + @module ember + @submodule ember-views + */ + + var states = cloneStates(EmberViewStates); + + /** + A `ContainerView` is an `Ember.View` subclass that implements `Ember.MutableArray` + allowing programmatic management of its child views. + + ## Setting Initial Child Views + + The initial array of child views can be set in one of two ways. You can + provide a `childViews` property at creation time that contains instance of + `Ember.View`: + + ```javascript + aContainer = Ember.ContainerView.create({ + childViews: [Ember.View.create(), Ember.View.create()] + }); + ``` + + You can also provide a list of property names whose values are instances of + `Ember.View`: + + ```javascript + aContainer = Ember.ContainerView.create({ + childViews: ['aView', 'bView', 'cView'], + aView: Ember.View.create(), + bView: Ember.View.create(), + cView: Ember.View.create() + }); + ``` + + The two strategies can be combined: + + ```javascript + aContainer = Ember.ContainerView.create({ + childViews: ['aView', Ember.View.create()], + aView: Ember.View.create() + }); + ``` + + Each child view's rendering will be inserted into the container's rendered + HTML in the same order as its position in the `childViews` property. + + ## Adding and Removing Child Views + + The container view implements `Ember.MutableArray` allowing programmatic management of its child views. + + To remove a view, pass that view into a `removeObject` call on the container view. + + Given an empty `` the following code + + ```javascript + aContainer = Ember.ContainerView.create({ + classNames: ['the-container'], + childViews: ['aView', 'bView'], + aView: Ember.View.create({ + template: Ember.Handlebars.compile("A") + }), + bView: Ember.View.create({ + template: Ember.Handlebars.compile("B") + }) + }); + + aContainer.appendTo('body'); + ``` + + Results in the HTML + + ```html +
    +
    A
    +
    B
    +
    + ``` + + Removing a view + + ```javascript + aContainer.toArray(); // [aContainer.aView, aContainer.bView] + aContainer.removeObject(aContainer.get('bView')); + aContainer.toArray(); // [aContainer.aView] + ``` + + Will result in the following HTML + + ```html +
    +
    A
    +
    + ``` + + Similarly, adding a child view is accomplished by adding `Ember.View` instances to the + container view. + + Given an empty `` the following code + + ```javascript + aContainer = Ember.ContainerView.create({ + classNames: ['the-container'], + childViews: ['aView', 'bView'], + aView: Ember.View.create({ + template: Ember.Handlebars.compile("A") + }), + bView: Ember.View.create({ + template: Ember.Handlebars.compile("B") + }) + }); + + aContainer.appendTo('body'); + ``` + + Results in the HTML + + ```html +
    +
    A
    +
    B
    +
    + ``` + + Adding a view + + ```javascript + AnotherViewClass = Ember.View.extend({ + template: Ember.Handlebars.compile("Another view") + }); + + aContainer.toArray(); // [aContainer.aView, aContainer.bView] + aContainer.pushObject(AnotherViewClass.create()); + aContainer.toArray(); // [aContainer.aView, aContainer.bView, ] + ``` + + Will result in the following HTML + + ```html +
    +
    A
    +
    B
    +
    Another view
    +
    + ``` + + ## Templates and Layout + + A `template`, `templateName`, `defaultTemplate`, `layout`, `layoutName` or + `defaultLayout` property on a container view will not result in the template + or layout being rendered. The HTML contents of a `Ember.ContainerView`'s DOM + representation will only be the rendered HTML of its child views. + + @class ContainerView + @namespace Ember + @extends Ember.View + */ + var ContainerView = View.extend(MutableArray, { + states: states, + + init: function() { + this._super(); + + var childViews = get(this, 'childViews'); + + // redefine view's childViews property that was obliterated + defineProperty(this, 'childViews', View.childViewsProperty); + + var _childViews = this._childViews; + + forEach(childViews, function(viewName, idx) { + var view; + + if ('string' === typeof viewName) { + view = get(this, viewName); + view = this.createChildView(view); + set(this, viewName, view); + } else { + view = this.createChildView(viewName); + } + + _childViews[idx] = view; + }, this); + + var currentView = get(this, 'currentView'); + if (currentView) { + if (!_childViews.length) { _childViews = this._childViews = this._childViews.slice(); } + _childViews.push(this.createChildView(currentView)); + } + }, + + replace: function(idx, removedCount, addedViews) { + var addedCount = addedViews ? get(addedViews, 'length') : 0; + var self = this; + + this.arrayContentWillChange(idx, removedCount, addedCount); + this.childViewsWillChange(this._childViews, idx, removedCount); + + if (addedCount === 0) { + this._childViews.splice(idx, removedCount) ; + } else { + var args = [idx, removedCount].concat(addedViews); + if (addedViews.length && !this._childViews.length) { this._childViews = this._childViews.slice(); } + this._childViews.splice.apply(this._childViews, args); + } + + this.arrayContentDidChange(idx, removedCount, addedCount); + this.childViewsDidChange(this._childViews, idx, removedCount, addedCount); + + return this; + }, + + objectAt: function(idx) { + return this._childViews[idx]; + }, + + length: computed(function () { + return this._childViews.length; + }).volatile(), + + /** + Instructs each child view to render to the passed render buffer. + + @private + @method render + @param {Ember.RenderBuffer} buffer the buffer to render to + */ + render: function(buffer) { + this.forEachChildView(function(view) { + view.renderToBuffer(buffer); + }); + }, + + instrumentName: 'container', + + /** + When a child view is removed, destroy its element so that + it is removed from the DOM. + + The array observer that triggers this action is set up in the + `renderToBuffer` method. + + @private + @method childViewsWillChange + @param {Ember.Array} views the child views array before mutation + @param {Number} start the start position of the mutation + @param {Number} removed the number of child views removed + **/ + childViewsWillChange: function(views, start, removed) { + this.propertyWillChange('childViews'); + + if (removed > 0) { + var changedViews = views.slice(start, start+removed); + // transition to preRender before clearing parentView + this.currentState.childViewsWillChange(this, views, start, removed); + this.initializeViews(changedViews, null, null); + } + }, + + removeChild: function(child) { + this.removeObject(child); + return this; + }, + + /** + When a child view is added, make sure the DOM gets updated appropriately. + + If the view has already rendered an element, we tell the child view to + create an element and insert it into the DOM. If the enclosing container + view has already written to a buffer, but not yet converted that buffer + into an element, we insert the string representation of the child into the + appropriate place in the buffer. + + @private + @method childViewsDidChange + @param {Ember.Array} views the array of child views after the mutation has occurred + @param {Number} start the start position of the mutation + @param {Number} removed the number of child views removed + @param {Number} added the number of child views added + */ + childViewsDidChange: function(views, start, removed, added) { + if (added > 0) { + var changedViews = views.slice(start, start+added); + this.initializeViews(changedViews, this, get(this, 'templateData')); + this.currentState.childViewsDidChange(this, views, start, added); + } + this.propertyDidChange('childViews'); + }, + + initializeViews: function(views, parentView, templateData) { + forEach(views, function(view) { + set(view, '_parentView', parentView); + + if (!view.container && parentView) { + set(view, 'container', parentView.container); + } + + if (!get(view, 'templateData')) { + set(view, 'templateData', templateData); + } + }); + }, + + currentView: null, + + _currentViewWillChange: beforeObserver('currentView', function() { + var currentView = get(this, 'currentView'); + if (currentView) { + currentView.destroy(); + } + }), + + _currentViewDidChange: observer('currentView', function() { + var currentView = get(this, 'currentView'); + if (currentView) { + this.pushObject(currentView); + } + }), + + _ensureChildrenAreInDOM: function () { + this.currentState.ensureChildrenAreInDOM(this); + } + }); + + merge(states._default, { + childViewsWillChange: Ember.K, + childViewsDidChange: Ember.K, + ensureChildrenAreInDOM: Ember.K + }); + + merge(states.inBuffer, { + childViewsDidChange: function(parentView, views, start, added) { + throw new EmberError('You cannot modify child views while in the inBuffer state'); + } + }); + + merge(states.hasElement, { + childViewsWillChange: function(view, views, start, removed) { + for (var i=start; i + ``` + + ## HTML `class` Attribute + + The HTML `class` attribute of a view's tag can be set by providing a + `classNames` property that is set to an array of strings: + + ```javascript + MyView = Ember.View.extend({ + classNames: ['my-class', 'my-other-class'] + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    + ``` + + `class` attribute values can also be set by providing a `classNameBindings` + property set to an array of properties names for the view. The return value + of these properties will be added as part of the value for the view's `class` + attribute. These properties can be computed properties: + + ```javascript + MyView = Ember.View.extend({ + classNameBindings: ['propertyA', 'propertyB'], + propertyA: 'from-a', + propertyB: function() { + if (someLogic) { return 'from-b'; } + }.property() + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    + ``` + + If the value of a class name binding returns a boolean the property name + itself will be used as the class name if the property is true. The class name + will not be added if the value is `false` or `undefined`. + + ```javascript + MyView = Ember.View.extend({ + classNameBindings: ['hovered'], + hovered: true + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    + ``` + + When using boolean class name bindings you can supply a string value other + than the property name for use as the `class` HTML attribute by appending the + preferred value after a ":" character when defining the binding: + + ```javascript + MyView = Ember.View.extend({ + classNameBindings: ['awesome:so-very-cool'], + awesome: true + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    + ``` + + Boolean value class name bindings whose property names are in a + camelCase-style format will be converted to a dasherized format: + + ```javascript + MyView = Ember.View.extend({ + classNameBindings: ['isUrgent'], + isUrgent: true + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    + ``` + + Class name bindings can also refer to object values that are found by + traversing a path relative to the view itself: + + ```javascript + MyView = Ember.View.extend({ + classNameBindings: ['messages.empty'] + messages: Ember.Object.create({ + empty: true + }) + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    + ``` + + If you want to add a class name for a property which evaluates to true and + and a different class name if it evaluates to false, you can pass a binding + like this: + + ```javascript + // Applies 'enabled' class when isEnabled is true and 'disabled' when isEnabled is false + Ember.View.extend({ + classNameBindings: ['isEnabled:enabled:disabled'] + isEnabled: true + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    + ``` + + When isEnabled is `false`, the resulting HTML reprensentation looks like + this: + + ```html +
    + ``` + + This syntax offers the convenience to add a class if a property is `false`: + + ```javascript + // Applies no class when isEnabled is true and class 'disabled' when isEnabled is false + Ember.View.extend({ + classNameBindings: ['isEnabled::disabled'] + isEnabled: true + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    + ``` + + When the `isEnabled` property on the view is set to `false`, it will result + in view instances with an HTML representation of: + + ```html +
    + ``` + + Updates to the the value of a class name binding will result in automatic + update of the HTML `class` attribute in the view's rendered HTML + representation. If the value becomes `false` or `undefined` the class name + will be removed. + + Both `classNames` and `classNameBindings` are concatenated properties. See + [Ember.Object](/api/classes/Ember.Object.html) documentation for more + information about concatenated properties. + + ## HTML Attributes + + The HTML attribute section of a view's tag can be set by providing an + `attributeBindings` property set to an array of property names on the view. + The return value of these properties will be used as the value of the view's + HTML associated attribute: + + ```javascript + AnchorView = Ember.View.extend({ + tagName: 'a', + attributeBindings: ['href'], + href: 'http://google.com' + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html + + ``` + + One property can be mapped on to another by placing a ":" between + the source property and the destination property: + + ```javascript + AnchorView = Ember.View.extend({ + tagName: 'a', + attributeBindings: ['url:href'], + url: 'http://google.com' + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html + + ``` + + If the return value of an `attributeBindings` monitored property is a boolean + the property will follow HTML's pattern of repeating the attribute's name as + its value: + + ```javascript + MyTextInput = Ember.View.extend({ + tagName: 'input', + attributeBindings: ['disabled'], + disabled: true + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html + + ``` + + `attributeBindings` can refer to computed properties: + + ```javascript + MyTextInput = Ember.View.extend({ + tagName: 'input', + attributeBindings: ['disabled'], + disabled: function() { + if (someLogic) { + return true; + } else { + return false; + } + }.property() + }); + ``` + + Updates to the the property of an attribute binding will result in automatic + update of the HTML attribute in the view's rendered HTML representation. + + `attributeBindings` is a concatenated property. See [Ember.Object](/api/classes/Ember.Object.html) + documentation for more information about concatenated properties. + + ## Templates + + The HTML contents of a view's rendered representation are determined by its + template. Templates can be any function that accepts an optional context + parameter and returns a string of HTML that will be inserted within the + view's tag. Most typically in Ember this function will be a compiled + `Ember.Handlebars` template. + + ```javascript + AView = Ember.View.extend({ + template: Ember.Handlebars.compile('I am the template') + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    I am the template
    + ``` + + Within an Ember application is more common to define a Handlebars templates as + part of a page: + + ```html + + ``` + + And associate it by name using a view's `templateName` property: + + ```javascript + AView = Ember.View.extend({ + templateName: 'some-template' + }); + ``` + + If you have nested resources, your Handlebars template will look like this: + + ```html + + ``` + + And `templateName` property: + + ```javascript + AView = Ember.View.extend({ + templateName: 'posts/new' + }); + ``` + + Using a value for `templateName` that does not have a Handlebars template + with a matching `data-template-name` attribute will throw an error. + + For views classes that may have a template later defined (e.g. as the block + portion of a `{{view}}` Handlebars helper call in another template or in + a subclass), you can provide a `defaultTemplate` property set to compiled + template function. If a template is not later provided for the view instance + the `defaultTemplate` value will be used: + + ```javascript + AView = Ember.View.extend({ + defaultTemplate: Ember.Handlebars.compile('I was the default'), + template: null, + templateName: null + }); + ``` + + Will result in instances with an HTML representation of: + + ```html +
    I was the default
    + ``` + + If a `template` or `templateName` is provided it will take precedence over + `defaultTemplate`: + + ```javascript + AView = Ember.View.extend({ + defaultTemplate: Ember.Handlebars.compile('I was the default') + }); + + aView = AView.create({ + template: Ember.Handlebars.compile('I was the template, not default') + }); + ``` + + Will result in the following HTML representation when rendered: + + ```html +
    I was the template, not default
    + ``` + + ## View Context + + The default context of the compiled template is the view's controller: + + ```javascript + AView = Ember.View.extend({ + template: Ember.Handlebars.compile('Hello {{excitedGreeting}}') + }); + + aController = Ember.Object.create({ + firstName: 'Barry', + excitedGreeting: function() { + return this.get("content.firstName") + "!!!" + }.property() + }); + + aView = AView.create({ + controller: aController, + }); + ``` + + Will result in an HTML representation of: + + ```html +
    Hello Barry!!!
    + ``` + + A context can also be explicitly supplied through the view's `context` + property. If the view has neither `context` nor `controller` properties, the + `parentView`'s context will be used. + + ## Layouts + + Views can have a secondary template that wraps their main template. Like + primary templates, layouts can be any function that accepts an optional + context parameter and returns a string of HTML that will be inserted inside + view's tag. Views whose HTML element is self closing (e.g. ``) + cannot have a layout and this property will be ignored. + + Most typically in Ember a layout will be a compiled `Ember.Handlebars` + template. + + A view's layout can be set directly with the `layout` property or reference + an existing Handlebars template by name with the `layoutName` property. + + A template used as a layout must contain a single use of the Handlebars + `{{yield}}` helper. The HTML contents of a view's rendered `template` will be + inserted at this location: + + ```javascript + AViewWithLayout = Ember.View.extend({ + layout: Ember.Handlebars.compile("
    {{yield}}
    ") + template: Ember.Handlebars.compile("I got wrapped"), + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    +
    + I got wrapped +
    +
    + ``` + + See [Ember.Handlebars.helpers.yield](/api/classes/Ember.Handlebars.helpers.html#method_yield) + for more information. + + ## Responding to Browser Events + + Views can respond to user-initiated events in one of three ways: method + implementation, through an event manager, and through `{{action}}` helper use + in their template or layout. + + ### Method Implementation + + Views can respond to user-initiated events by implementing a method that + matches the event name. A `jQuery.Event` object will be passed as the + argument to this method. + + ```javascript + AView = Ember.View.extend({ + click: function(event) { + // will be called when when an instance's + // rendered element is clicked + } + }); + ``` + + ### Event Managers + + Views can define an object as their `eventManager` property. This object can + then implement methods that match the desired event names. Matching events + that occur on the view's rendered HTML or the rendered HTML of any of its DOM + descendants will trigger this method. A `jQuery.Event` object will be passed + as the first argument to the method and an `Ember.View` object as the + second. The `Ember.View` will be the view whose rendered HTML was interacted + with. This may be the view with the `eventManager` property or one of its + descendent views. + + ```javascript + AView = Ember.View.extend({ + eventManager: Ember.Object.create({ + doubleClick: function(event, view) { + // will be called when when an instance's + // rendered element or any rendering + // of this views's descendent + // elements is clicked + } + }) + }); + ``` + + An event defined for an event manager takes precedence over events of the + same name handled through methods on the view. + + ```javascript + AView = Ember.View.extend({ + mouseEnter: function(event) { + // will never trigger. + }, + eventManager: Ember.Object.create({ + mouseEnter: function(event, view) { + // takes precedence over AView#mouseEnter + } + }) + }); + ``` + + Similarly a view's event manager will take precedence for events of any views + rendered as a descendent. A method name that matches an event name will not + be called if the view instance was rendered inside the HTML representation of + a view that has an `eventManager` property defined that handles events of the + name. Events not handled by the event manager will still trigger method calls + on the descendent. + + ```javascript + OuterView = Ember.View.extend({ + template: Ember.Handlebars.compile("outer {{#view InnerView}}inner{{/view}} outer"), + eventManager: Ember.Object.create({ + mouseEnter: function(event, view) { + // view might be instance of either + // OuterView or InnerView depending on + // where on the page the user interaction occured + } + }) + }); + + InnerView = Ember.View.extend({ + click: function(event) { + // will be called if rendered inside + // an OuterView because OuterView's + // eventManager doesn't handle click events + }, + mouseEnter: function(event) { + // will never be called if rendered inside + // an OuterView. + } + }); + ``` + + ### Handlebars `{{action}}` Helper + + See [Handlebars.helpers.action](/api/classes/Ember.Handlebars.helpers.html#method_action). + + ### Event Names + + All of the event handling approaches described above respond to the same set + of events. The names of the built-in events are listed below. (The hash of + built-in events exists in `Ember.EventDispatcher`.) Additional, custom events + can be registered by using `Ember.Application.customEvents`. + + Touch events: + + * `touchStart` + * `touchMove` + * `touchEnd` + * `touchCancel` + + Keyboard events + + * `keyDown` + * `keyUp` + * `keyPress` + + Mouse events + + * `mouseDown` + * `mouseUp` + * `contextMenu` + * `click` + * `doubleClick` + * `mouseMove` + * `focusIn` + * `focusOut` + * `mouseEnter` + * `mouseLeave` + + Form events: + + * `submit` + * `change` + * `focusIn` + * `focusOut` + * `input` + + HTML5 drag and drop events: + + * `dragStart` + * `drag` + * `dragEnter` + * `dragLeave` + * `dragOver` + * `dragEnd` + * `drop` + + ## Handlebars `{{view}}` Helper + + Other `Ember.View` instances can be included as part of a view's template by + using the `{{view}}` Handlebars helper. See [Ember.Handlebars.helpers.view](/api/classes/Ember.Handlebars.helpers.html#method_view) + for additional information. + + @class View + @namespace Ember + @extends Ember.CoreView + */ + var View = CoreView.extend({ + + concatenatedProperties: ['classNames', 'classNameBindings', 'attributeBindings'], + + /** + @property isView + @type Boolean + @default true + @static + */ + isView: true, + + // .......................................................... + // TEMPLATE SUPPORT + // + + /** + The name of the template to lookup if no template is provided. + + By default `Ember.View` will lookup a template with this name in + `Ember.TEMPLATES` (a shared global object). + + @property templateName + @type String + @default null + */ + templateName: null, + + /** + The name of the layout to lookup if no layout is provided. + + By default `Ember.View` will lookup a template with this name in + `Ember.TEMPLATES` (a shared global object). + + @property layoutName + @type String + @default null + */ + layoutName: null, + + /** + Used to identify this view during debugging + + @property instrumentDisplay + @type String + */ + instrumentDisplay: computed(function() { + if (this.helperName) { + return '{{' + this.helperName + '}}'; + } + }), + + /** + The template used to render the view. This should be a function that + accepts an optional context parameter and returns a string of HTML that + will be inserted into the DOM relative to its parent view. + + In general, you should set the `templateName` property instead of setting + the template yourself. + + @property template + @type Function + */ + template: computed('templateName', function(key, value) { + if (value !== undefined) { return value; } + + var templateName = get(this, 'templateName'), + template = this.templateForName(templateName, 'template'); + + + return template || get(this, 'defaultTemplate'); + }), + + /** + The controller managing this view. If this property is set, it will be + made available for use by the template. + + @property controller + @type Object + */ + controller: computed('_parentView', function(key) { + var parentView = get(this, '_parentView'); + return parentView ? get(parentView, 'controller') : null; + }), + + /** + A view may contain a layout. A layout is a regular template but + supersedes the `template` property during rendering. It is the + responsibility of the layout template to retrieve the `template` + property from the view (or alternatively, call `Handlebars.helpers.yield`, + `{{yield}}`) to render it in the correct location. + + This is useful for a view that has a shared wrapper, but which delegates + the rendering of the contents of the wrapper to the `template` property + on a subclass. + + @property layout + @type Function + */ + layout: computed(function(key) { + var layoutName = get(this, 'layoutName'), + layout = this.templateForName(layoutName, 'layout'); + + + return layout || get(this, 'defaultLayout'); + }).property('layoutName'), + + _yield: function(context, options) { + var template = get(this, 'template'); + if (template) { template(context, options); } + }, + + templateForName: function(name, type) { + if (!name) { return; } + + // the defaultContainer is deprecated + var container = this.container || (Container && Container.defaultContainer); + return container && container.lookup('template:' + name); + }, + + /** + The object from which templates should access properties. + + This object will be passed to the template function each time the render + method is called, but it is up to the individual function to decide what + to do with it. + + By default, this will be the view's controller. + + @property context + @type Object + */ + context: computed(function(key, value) { + if (arguments.length === 2) { + set(this, '_context', value); + return value; + } else { + return get(this, '_context'); + } + }).volatile(), + + /** + Private copy of the view's template context. This can be set directly + by Handlebars without triggering the observer that causes the view + to be re-rendered. + + The context of a view is looked up as follows: + + 1. Supplied context (usually by Handlebars) + 2. Specified controller + 3. `parentView`'s context (for a child of a ContainerView) + + The code in Handlebars that overrides the `_context` property first + checks to see whether the view has a specified controller. This is + something of a hack and should be revisited. + + @property _context + @private + */ + _context: computed(function(key) { + var parentView, controller; + + if (controller = get(this, 'controller')) { + return controller; + } + + parentView = this._parentView; + if (parentView) { + return get(parentView, '_context'); + } + + return null; + }), + + /** + If a value that affects template rendering changes, the view should be + re-rendered to reflect the new value. + + @method _contextDidChange + @private + */ + _contextDidChange: observer('context', function() { + this.rerender(); + }), + + /** + If `false`, the view will appear hidden in DOM. + + @property isVisible + @type Boolean + @default null + */ + isVisible: true, + + /** + Array of child views. You should never edit this array directly. + Instead, use `appendChild` and `removeFromParent`. + + @property childViews + @type Array + @default [] + @private + */ + childViews: childViewsProperty, + + _childViews: EMPTY_ARRAY, + + // When it's a virtual view, we need to notify the parent that their + // childViews will change. + _childViewsWillChange: beforeObserver('childViews', function() { + if (this.isVirtual) { + var parentView = get(this, 'parentView'); + if (parentView) { propertyWillChange(parentView, 'childViews'); } + } + }), + + // When it's a virtual view, we need to notify the parent that their + // childViews did change. + _childViewsDidChange: observer('childViews', function() { + if (this.isVirtual) { + var parentView = get(this, 'parentView'); + if (parentView) { propertyDidChange(parentView, 'childViews'); } + } + }), + + /** + Return the nearest ancestor that is an instance of the provided + class. + + @method nearestInstanceOf + @param {Class} klass Subclass of Ember.View (or Ember.View itself) + @return Ember.View + @deprecated + */ + nearestInstanceOf: function(klass) { + var view = get(this, 'parentView'); + + while (view) { + if (view instanceof klass) { return view; } + view = get(view, 'parentView'); + } + }, + + /** + Return the nearest ancestor that is an instance of the provided + class or mixin. + + @method nearestOfType + @param {Class,Mixin} klass Subclass of Ember.View (or Ember.View itself), + or an instance of Ember.Mixin. + @return Ember.View + */ + nearestOfType: function(klass) { + var view = get(this, 'parentView'), + isOfType = klass instanceof Mixin ? + function(view) { return klass.detect(view); } : + function(view) { return klass.detect(view.constructor); }; + + while (view) { + if (isOfType(view)) { return view; } + view = get(view, 'parentView'); + } + }, + + /** + Return the nearest ancestor that has a given property. + + @method nearestWithProperty + @param {String} property A property name + @return Ember.View + */ + nearestWithProperty: function(property) { + var view = get(this, 'parentView'); + + while (view) { + if (property in view) { return view; } + view = get(view, 'parentView'); + } + }, + + /** + Return the nearest ancestor whose parent is an instance of + `klass`. + + @method nearestChildOf + @param {Class} klass Subclass of Ember.View (or Ember.View itself) + @return Ember.View + */ + nearestChildOf: function(klass) { + var view = get(this, 'parentView'); + + while (view) { + if (get(view, 'parentView') instanceof klass) { return view; } + view = get(view, 'parentView'); + } + }, + + /** + When the parent view changes, recursively invalidate `controller` + + @method _parentViewDidChange + @private + */ + _parentViewDidChange: observer('_parentView', function() { + if (this.isDestroying) { return; } + + this.trigger('parentViewDidChange'); + + if (get(this, 'parentView.controller') && !get(this, 'controller')) { + this.notifyPropertyChange('controller'); + } + }), + + _controllerDidChange: observer('controller', function() { + if (this.isDestroying) { return; } + + this.rerender(); + + this.forEachChildView(function(view) { + view.propertyDidChange('controller'); + }); + }), + + cloneKeywords: function() { + var templateData = get(this, 'templateData'); + + var keywords = templateData ? copy(templateData.keywords) : {}; + set(keywords, 'view', get(this, 'concreteView')); + set(keywords, '_view', this); + set(keywords, 'controller', get(this, 'controller')); + + return keywords; + }, + + /** + Called on your view when it should push strings of HTML into a + `Ember.RenderBuffer`. Most users will want to override the `template` + or `templateName` properties instead of this method. + + By default, `Ember.View` will look for a function in the `template` + property and invoke it with the value of `context`. The value of + `context` will be the view's controller unless you override it. + + @method render + @param {Ember.RenderBuffer} buffer The render buffer + */ + render: function(buffer) { + // If this view has a layout, it is the responsibility of the + // the layout to render the view's template. Otherwise, render the template + // directly. + var template = get(this, 'layout') || get(this, 'template'); + + if (template) { + var context = get(this, 'context'); + var keywords = this.cloneKeywords(); + var output; + + var data = { + view: this, + buffer: buffer, + isRenderData: true, + keywords: keywords, + insideGroup: get(this, 'templateData.insideGroup') + }; + + // Invoke the template with the provided template context, which + // is the view's controller by default. A hash of data is also passed that provides + // the template with access to the view and render buffer. + + // The template should write directly to the render buffer instead + // of returning a string. + output = template(context, { data: data }); + + // If the template returned a string instead of writing to the buffer, + // push the string onto the buffer. + if (output !== undefined) { buffer.push(output); } + } + }, + + /** + Renders the view again. This will work regardless of whether the + view is already in the DOM or not. If the view is in the DOM, the + rendering process will be deferred to give bindings a chance + to synchronize. + + If children were added during the rendering process using `appendChild`, + `rerender` will remove them, because they will be added again + if needed by the next `render`. + + In general, if the display of your view changes, you should modify + the DOM element directly instead of manually calling `rerender`, which can + be slow. + + @method rerender + */ + rerender: function() { + return this.currentState.rerender(this); + }, + + clearRenderedChildren: function() { + var lengthBefore = this.lengthBeforeRender, + lengthAfter = this.lengthAfterRender; + + // If there were child views created during the last call to render(), + // remove them under the assumption that they will be re-created when + // we re-render. + + // VIEW-TODO: Unit test this path. + var childViews = this._childViews; + for (var i=lengthAfter-1; i>=lengthBefore; i--) { + if (childViews[i]) { childViews[i].destroy(); } + } + }, + + /** + Iterates over the view's `classNameBindings` array, inserts the value + of the specified property into the `classNames` array, then creates an + observer to update the view's element if the bound property ever changes + in the future. + + @method _applyClassNameBindings + @private + */ + _applyClassNameBindings: function(classBindings) { + var classNames = this.classNames, + elem, newClass, dasherizedClass; + + // Loop through all of the configured bindings. These will be either + // property names ('isUrgent') or property paths relative to the view + // ('content.isUrgent') + a_forEach(classBindings, function(binding) { + + + // Variable in which the old class value is saved. The observer function + // closes over this variable, so it knows which string to remove when + // the property changes. + var oldClass; + // Extract just the property name from bindings like 'foo:bar' + var parsedPath = View._parsePropertyPath(binding); + + // Set up an observer on the context. If the property changes, toggle the + // class name. + var observer = function() { + // Get the current value of the property + newClass = this._classStringForProperty(binding); + elem = this.$(); + + // If we had previously added a class to the element, remove it. + if (oldClass) { + elem.removeClass(oldClass); + // Also remove from classNames so that if the view gets rerendered, + // the class doesn't get added back to the DOM. + classNames.removeObject(oldClass); + } + + // If necessary, add a new class. Make sure we keep track of it so + // it can be removed in the future. + if (newClass) { + elem.addClass(newClass); + oldClass = newClass; + } else { + oldClass = null; + } + }; + + // Get the class name for the property at its current value + dasherizedClass = this._classStringForProperty(binding); + + if (dasherizedClass) { + // Ensure that it gets into the classNames array + // so it is displayed when we render. + a_addObject(classNames, dasherizedClass); + + // Save a reference to the class name so we can remove it + // if the observer fires. Remember that this variable has + // been closed over by the observer. + oldClass = dasherizedClass; + } + + this.registerObserver(this, parsedPath.path, observer); + // Remove className so when the view is rerendered, + // the className is added based on binding reevaluation + this.one('willClearRender', function() { + if (oldClass) { + classNames.removeObject(oldClass); + oldClass = null; + } + }); + + }, this); + }, + + _unspecifiedAttributeBindings: null, + + /** + Iterates through the view's attribute bindings, sets up observers for each, + then applies the current value of the attributes to the passed render buffer. + + @method _applyAttributeBindings + @param {Ember.RenderBuffer} buffer + @private + */ + _applyAttributeBindings: function(buffer, attributeBindings) { + var attributeValue, + unspecifiedAttributeBindings = this._unspecifiedAttributeBindings = this._unspecifiedAttributeBindings || {}; + + a_forEach(attributeBindings, function(binding) { + var split = binding.split(':'), + property = split[0], + attributeName = split[1] || property; + + if (property in this) { + this._setupAttributeBindingObservation(property, attributeName); + + // Determine the current value and add it to the render buffer + // if necessary. + attributeValue = get(this, property); + View.applyAttributeBindings(buffer, attributeName, attributeValue); + } else { + unspecifiedAttributeBindings[property] = attributeName; + } + }, this); + + // Lazily setup setUnknownProperty after attributeBindings are initially applied + this.setUnknownProperty = this._setUnknownProperty; + }, + + _setupAttributeBindingObservation: function(property, attributeName) { + var attributeValue, elem; + + // Create an observer to add/remove/change the attribute if the + // JavaScript property changes. + var observer = function() { + elem = this.$(); + + attributeValue = get(this, property); + + View.applyAttributeBindings(elem, attributeName, attributeValue); + }; + + this.registerObserver(this, property, observer); + }, + + /** + We're using setUnknownProperty as a hook to setup attributeBinding observers for + properties that aren't defined on a view at initialization time. + + Note: setUnknownProperty will only be called once for each property. + + @method setUnknownProperty + @param key + @param value + @private + */ + setUnknownProperty: null, // Gets defined after initialization by _applyAttributeBindings + + _setUnknownProperty: function(key, value) { + var attributeName = this._unspecifiedAttributeBindings && this._unspecifiedAttributeBindings[key]; + if (attributeName) { + this._setupAttributeBindingObservation(key, attributeName); + } + + defineProperty(this, key); + return set(this, key, value); + }, + + /** + Given a property name, returns a dasherized version of that + property name if the property evaluates to a non-falsy value. + + For example, if the view has property `isUrgent` that evaluates to true, + passing `isUrgent` to this method will return `"is-urgent"`. + + @method _classStringForProperty + @param property + @private + */ + _classStringForProperty: function(property) { + var parsedPath = View._parsePropertyPath(property); + var path = parsedPath.path; + + var val = get(this, path); + if (val === undefined && isGlobalPath(path)) { + val = get(Ember.lookup, path); + } + + return View._classStringForValue(path, val, parsedPath.className, parsedPath.falsyClassName); + }, + + // .......................................................... + // ELEMENT SUPPORT + // + + /** + Returns the current DOM element for the view. + + @property element + @type DOMElement + */ + element: computed('_parentView', function(key, value) { + if (value !== undefined) { + return this.currentState.setElement(this, value); + } else { + return this.currentState.getElement(this); + } + }), + + /** + Returns a jQuery object for this view's element. If you pass in a selector + string, this method will return a jQuery object, using the current element + as its buffer. + + For example, calling `view.$('li')` will return a jQuery object containing + all of the `li` elements inside the DOM element of this view. + + @method $ + @param {String} [selector] a jQuery-compatible selector string + @return {jQuery} the jQuery object for the DOM node + */ + $: function(sel) { + return this.currentState.$(this, sel); + }, + + mutateChildViews: function(callback) { + var childViews = this._childViews, + idx = childViews.length, + view; + + while(--idx >= 0) { + view = childViews[idx]; + callback(this, view, idx); + } + + return this; + }, + + forEachChildView: function(callback) { + var childViews = this._childViews; + + if (!childViews) { return this; } + + var len = childViews.length, + view, idx; + + for (idx = 0; idx < len; idx++) { + view = childViews[idx]; + callback(view); + } + + return this; + }, + + /** + Appends the view's element to the specified parent element. + + If the view does not have an HTML representation yet, `createElement()` + will be called automatically. + + Note that this method just schedules the view to be appended; the DOM + element will not be appended to the given element until all bindings have + finished synchronizing. + + This is not typically a function that you will need to call directly when + building your application. You might consider using `Ember.ContainerView` + instead. If you do need to use `appendTo`, be sure that the target element + you are providing is associated with an `Ember.Application` and does not + have an ancestor element that is associated with an Ember view. + + @method appendTo + @param {String|DOMElement|jQuery} A selector, element, HTML string, or jQuery object + @return {Ember.View} receiver + */ + appendTo: function(target) { + // Schedule the DOM element to be created and appended to the given + // element after bindings have synchronized. + this._insertElementLater(function() { + this.$().appendTo(target); + }); + + return this; + }, + + /** + Replaces the content of the specified parent element with this view's + element. If the view does not have an HTML representation yet, + `createElement()` will be called automatically. + + Note that this method just schedules the view to be appended; the DOM + element will not be appended to the given element until all bindings have + finished synchronizing + + @method replaceIn + @param {String|DOMElement|jQuery} target A selector, element, HTML string, or jQuery object + @return {Ember.View} received + */ + replaceIn: function(target) { + + this._insertElementLater(function() { + jQuery(target).empty(); + this.$().appendTo(target); + }); + + return this; + }, + + /** + Schedules a DOM operation to occur during the next render phase. This + ensures that all bindings have finished synchronizing before the view is + rendered. + + To use, pass a function that performs a DOM operation. + + Before your function is called, this view and all child views will receive + the `willInsertElement` event. After your function is invoked, this view + and all of its child views will receive the `didInsertElement` event. + + ```javascript + view._insertElementLater(function() { + this.createElement(); + this.$().appendTo('body'); + }); + ``` + + @method _insertElementLater + @param {Function} fn the function that inserts the element into the DOM + @private + */ + _insertElementLater: function(fn) { + this._scheduledInsert = run.scheduleOnce('render', this, '_insertElement', fn); + }, + + _insertElement: function (fn) { + this._scheduledInsert = null; + this.currentState.insertElement(this, fn); + }, + + /** + Appends the view's element to the document body. If the view does + not have an HTML representation yet, `createElement()` will be called + automatically. + + If your application uses the `rootElement` property, you must append + the view within that element. Rendering views outside of the `rootElement` + is not supported. + + Note that this method just schedules the view to be appended; the DOM + element will not be appended to the document body until all bindings have + finished synchronizing. + + @method append + @return {Ember.View} receiver + */ + append: function() { + return this.appendTo(document.body); + }, + + /** + Removes the view's element from the element to which it is attached. + + @method remove + @return {Ember.View} receiver + */ + remove: function() { + // What we should really do here is wait until the end of the run loop + // to determine if the element has been re-appended to a different + // element. + // In the interim, we will just re-render if that happens. It is more + // important than elements get garbage collected. + if (!this.removedFromDOM) { this.destroyElement(); } + this.invokeRecursively(function(view) { + if (view.clearRenderedChildren) { view.clearRenderedChildren(); } + }); + }, + + elementId: null, + + /** + Attempts to discover the element in the parent element. The default + implementation looks for an element with an ID of `elementId` (or the + view's guid if `elementId` is null). You can override this method to + provide your own form of lookup. For example, if you want to discover your + element using a CSS class name instead of an ID. + + @method findElementInParentElement + @param {DOMElement} parentElement The parent's DOM element + @return {DOMElement} The discovered element + */ + findElementInParentElement: function(parentElem) { + var id = "#" + this.elementId; + return jQuery(id)[0] || jQuery(id, parentElem)[0]; + }, + + /** + Creates a DOM representation of the view and all of its + child views by recursively calling the `render()` method. + + After the element has been created, `didInsertElement` will + be called on this view and all of its child views. + + @method createElement + @return {Ember.View} receiver + */ + createElement: function() { + if (get(this, 'element')) { return this; } + + var buffer = this.renderToBuffer(); + set(this, 'element', buffer.element()); + + return this; + }, + + /** + Called when a view is going to insert an element into the DOM. + + @event willInsertElement + */ + willInsertElement: Ember.K, + + /** + Called when the element of the view has been inserted into the DOM + or after the view was re-rendered. Override this function to do any + set up that requires an element in the document body. + + @event didInsertElement + */ + didInsertElement: Ember.K, + + /** + Called when the view is about to rerender, but before anything has + been torn down. This is a good opportunity to tear down any manual + observers you have installed based on the DOM state + + @event willClearRender + */ + willClearRender: Ember.K, + + /** + Run this callback on the current view (unless includeSelf is false) and recursively on child views. + + @method invokeRecursively + @param fn {Function} + @param includeSelf {Boolean} Includes itself if true. + @private + */ + invokeRecursively: function(fn, includeSelf) { + var childViews = (includeSelf === false) ? this._childViews : [this]; + var currentViews, view, currentChildViews; + + while (childViews.length) { + currentViews = childViews.slice(); + childViews = []; + + for (var i=0, l=currentViews.length; i` tag for views. + + @property tagName + @type String + @default null + */ + + // We leave this null by default so we can tell the difference between + // the default case and a user-specified tag. + tagName: null, + + /** + The WAI-ARIA role of the control represented by this view. For example, a + button may have a role of type 'button', or a pane may have a role of + type 'alertdialog'. This property is used by assistive software to help + visually challenged users navigate rich web applications. + + The full list of valid WAI-ARIA roles is available at: + [http://www.w3.org/TR/wai-aria/roles#roles_categorization](http://www.w3.org/TR/wai-aria/roles#roles_categorization) + + @property ariaRole + @type String + @default null + */ + ariaRole: null, + + /** + Standard CSS class names to apply to the view's outer element. This + property automatically inherits any class names defined by the view's + superclasses as well. + + @property classNames + @type Array + @default ['ember-view'] + */ + classNames: ['ember-view'], + + /** + A list of properties of the view to apply as class names. If the property + is a string value, the value of that string will be applied as a class + name. + + ```javascript + // Applies the 'high' class to the view element + Ember.View.extend({ + classNameBindings: ['priority'] + priority: 'high' + }); + ``` + + If the value of the property is a Boolean, the name of that property is + added as a dasherized class name. + + ```javascript + // Applies the 'is-urgent' class to the view element + Ember.View.extend({ + classNameBindings: ['isUrgent'] + isUrgent: true + }); + ``` + + If you would prefer to use a custom value instead of the dasherized + property name, you can pass a binding like this: + + ```javascript + // Applies the 'urgent' class to the view element + Ember.View.extend({ + classNameBindings: ['isUrgent:urgent'] + isUrgent: true + }); + ``` + + This list of properties is inherited from the view's superclasses as well. + + @property classNameBindings + @type Array + @default [] + */ + classNameBindings: EMPTY_ARRAY, + + /** + A list of properties of the view to apply as attributes. If the property is + a string value, the value of that string will be applied as the attribute. + + ```javascript + // Applies the type attribute to the element + // with the value "button", like
    + Ember.View.extend({ + attributeBindings: ['type'], + type: 'button' + }); + ``` + + If the value of the property is a Boolean, the name of that property is + added as an attribute. + + ```javascript + // Renders something like
    + Ember.View.extend({ + attributeBindings: ['enabled'], + enabled: true + }); + ``` + + @property attributeBindings + */ + attributeBindings: EMPTY_ARRAY, + + // ....................................................... + // CORE DISPLAY METHODS + // + + /** + Setup a view, but do not finish waking it up. + + * configure `childViews` + * register the view with the global views hash, which is used for event + dispatch + + @method init + @private + */ + init: function() { + this.elementId = this.elementId || guidFor(this); + + this._super(); + + // setup child views. be sure to clone the child views array first + this._childViews = this._childViews.slice(); + + this.classNameBindings = A(this.classNameBindings.slice()); + + this.classNames = A(this.classNames.slice()); + }, + + appendChild: function(view, options) { + return this.currentState.appendChild(this, view, options); + }, + + /** + Removes the child view from the parent view. + + @method removeChild + @param {Ember.View} view + @return {Ember.View} receiver + */ + removeChild: function(view) { + // If we're destroying, the entire subtree will be + // freed, and the DOM will be handled separately, + // so no need to mess with childViews. + if (this.isDestroying) { return; } + + // update parent node + set(view, '_parentView', null); + + // remove view from childViews array. + var childViews = this._childViews; + + a_removeObject(childViews, view); + + this.propertyDidChange('childViews'); // HUH?! what happened to will change? + + return this; + }, + + /** + Removes all children from the `parentView`. + + @method removeAllChildren + @return {Ember.View} receiver + */ + removeAllChildren: function() { + return this.mutateChildViews(function(parentView, view) { + parentView.removeChild(view); + }); + }, + + destroyAllChildren: function() { + return this.mutateChildViews(function(parentView, view) { + view.destroy(); + }); + }, + + /** + Removes the view from its `parentView`, if one is found. Otherwise + does nothing. + + @method removeFromParent + @return {Ember.View} receiver + */ + removeFromParent: function() { + var parent = this._parentView; + + // Remove DOM element from parent + this.remove(); + + if (parent) { parent.removeChild(this); } + return this; + }, + + /** + You must call `destroy` on a view to destroy the view (and all of its + child views). This will remove the view from any parent node, then make + sure that the DOM element managed by the view can be released by the + memory manager. + + @method destroy + */ + destroy: function() { + var childViews = this._childViews, + // get parentView before calling super because it'll be destroyed + nonVirtualParentView = get(this, 'parentView'), + viewName = this.viewName, + childLen, i; + + if (!this._super()) { return; } + + childLen = childViews.length; + for (i=childLen-1; i>=0; i--) { + childViews[i].removedFromDOM = true; + } + + // remove from non-virtual parent view if viewName was specified + if (viewName && nonVirtualParentView) { + nonVirtualParentView.set(viewName, null); + } + + childLen = childViews.length; + for (i=childLen-1; i>=0; i--) { + childViews[i].destroy(); + } + + return this; + }, + + /** + Instantiates a view to be added to the childViews array during view + initialization. You generally will not call this method directly unless + you are overriding `createChildViews()`. Note that this method will + automatically configure the correct settings on the new view instance to + act as a child of the parent. + + @method createChildView + @param {Class|String} viewClass + @param {Hash} [attrs] Attributes to add + @return {Ember.View} new instance + */ + createChildView: function(view, attrs) { + if (!view) { + throw new TypeError("createChildViews first argument must exist"); + } + + if (view.isView && view._parentView === this && view.container === this.container) { + return view; + } + + attrs = attrs || {}; + attrs._parentView = this; + + if (CoreView.detect(view)) { + attrs.templateData = attrs.templateData || get(this, 'templateData'); + + attrs.container = this.container; + view = view.create(attrs); + + // don't set the property on a virtual view, as they are invisible to + // consumers of the view API + if (view.viewName) { + set(get(this, 'concreteView'), view.viewName, view); + } + } else if ('string' === typeof view) { + var fullName = 'view:' + view; + var ViewKlass = this.container.lookupFactory(fullName); + + + attrs.templateData = get(this, 'templateData'); + view = ViewKlass.create(attrs); + } else { + attrs.container = this.container; + + if (!get(view, 'templateData')) { + attrs.templateData = get(this, 'templateData'); + } + + setProperties(view, attrs); + + } + + return view; + }, + + becameVisible: Ember.K, + becameHidden: Ember.K, + + /** + When the view's `isVisible` property changes, toggle the visibility + element of the actual DOM element. + + @method _isVisibleDidChange + @private + */ + _isVisibleDidChange: observer('isVisible', function() { + if (this._isVisible === get(this, 'isVisible')) { return ; } + run.scheduleOnce('render', this, this._toggleVisibility); + }), + + _toggleVisibility: function() { + var $el = this.$(); + if (!$el) { return; } + + var isVisible = get(this, 'isVisible'); + + if (this._isVisible === isVisible) { return ; } + + $el.toggle(isVisible); + + this._isVisible = isVisible; + + if (this._isAncestorHidden()) { return; } + + if (isVisible) { + this._notifyBecameVisible(); + } else { + this._notifyBecameHidden(); + } + }, + + _notifyBecameVisible: function() { + this.trigger('becameVisible'); + + this.forEachChildView(function(view) { + var isVisible = get(view, 'isVisible'); + + if (isVisible || isVisible === null) { + view._notifyBecameVisible(); + } + }); + }, + + _notifyBecameHidden: function() { + this.trigger('becameHidden'); + this.forEachChildView(function(view) { + var isVisible = get(view, 'isVisible'); + + if (isVisible || isVisible === null) { + view._notifyBecameHidden(); + } + }); + }, + + _isAncestorHidden: function() { + var parent = get(this, 'parentView'); + + while (parent) { + if (get(parent, 'isVisible') === false) { return true; } + + parent = get(parent, 'parentView'); + } + + return false; + }, + + clearBuffer: function() { + this.invokeRecursively(nullViewsBuffer); + }, + + transitionTo: function(state, children) { + var priorState = this.currentState, + currentState = this.currentState = this.states[state]; + this.state = state; + + if (priorState && priorState.exit) { priorState.exit(this); } + if (currentState.enter) { currentState.enter(this); } + if (state === 'inDOM') { meta(this).cache.element = undefined; } + + if (children !== false) { + this.forEachChildView(function(view) { + view.transitionTo(state); + }); + } + }, + + // ....................................................... + // EVENT HANDLING + // + + /** + Handle events from `Ember.EventDispatcher` + + @method handleEvent + @param eventName {String} + @param evt {Event} + @private + */ + handleEvent: function(eventName, evt) { + return this.currentState.handleEvent(this, eventName, evt); + }, + + registerObserver: function(root, path, target, observer) { + if (!observer && 'function' === typeof target) { + observer = target; + target = null; + } + + if (!root || typeof root !== 'object') { + return; + } + + var view = this, + stateCheckedObserver = function() { + view.currentState.invokeObserver(this, observer); + }, + scheduledObserver = function() { + run.scheduleOnce('render', this, stateCheckedObserver); + }; + + addObserver(root, path, target, scheduledObserver); + + this.one('willClearRender', function() { + removeObserver(root, path, target, scheduledObserver); + }); + } + + }); + + /* + Describe how the specified actions should behave in the various + states that a view can exist in. Possible states: + + * preRender: when a view is first instantiated, and after its + element was destroyed, it is in the preRender state + * inBuffer: once a view has been rendered, but before it has + been inserted into the DOM, it is in the inBuffer state + * hasElement: the DOM representation of the view is created, + and is ready to be inserted + * inDOM: once a view has been inserted into the DOM it is in + the inDOM state. A view spends the vast majority of its + existence in this state. + * destroyed: once a view has been destroyed (using the destroy + method), it is in this state. No further actions can be invoked + on a destroyed view. + */ + + // in the destroyed state, everything is illegal + + // before rendering has begun, all legal manipulations are noops. + + // inside the buffer, legal manipulations are done on the buffer + + // once the view has been inserted into the DOM, legal manipulations + // are done on the DOM element. + + function notifyMutationListeners() { + run.once(View, 'notifyMutationListeners'); + } + + var DOMManager = { + prepend: function(view, html) { + view.$().prepend(html); + notifyMutationListeners(); + }, + + after: function(view, html) { + view.$().after(html); + notifyMutationListeners(); + }, + + html: function(view, html) { + view.$().html(html); + notifyMutationListeners(); + }, + + replace: function(view) { + var element = get(view, 'element'); + + set(view, 'element', null); + + view._insertElementLater(function() { + jQuery(element).replaceWith(get(view, 'element')); + notifyMutationListeners(); + }); + }, + + remove: function(view) { + view.$().remove(); + notifyMutationListeners(); + }, + + empty: function(view) { + view.$().empty(); + notifyMutationListeners(); + } + }; + + View.reopen({ + domManager: DOMManager + }); + + View.reopenClass({ + + /** + Parse a path and return an object which holds the parsed properties. + + For example a path like "content.isEnabled:enabled:disabled" will return the + following object: + + ```javascript + { + path: "content.isEnabled", + className: "enabled", + falsyClassName: "disabled", + classNames: ":enabled:disabled" + } + ``` + + @method _parsePropertyPath + @static + @private + */ + _parsePropertyPath: function(path) { + var split = path.split(':'), + propertyPath = split[0], + classNames = "", + className, + falsyClassName; + + // check if the property is defined as prop:class or prop:trueClass:falseClass + if (split.length > 1) { + className = split[1]; + if (split.length === 3) { falsyClassName = split[2]; } + + classNames = ':' + className; + if (falsyClassName) { classNames += ":" + falsyClassName; } + } + + return { + path: propertyPath, + classNames: classNames, + className: (className === '') ? undefined : className, + falsyClassName: falsyClassName + }; + }, + + /** + Get the class name for a given value, based on the path, optional + `className` and optional `falsyClassName`. + + - if a `className` or `falsyClassName` has been specified: + - if the value is truthy and `className` has been specified, + `className` is returned + - if the value is falsy and `falsyClassName` has been specified, + `falsyClassName` is returned + - otherwise `null` is returned + - if the value is `true`, the dasherized last part of the supplied path + is returned + - if the value is not `false`, `undefined` or `null`, the `value` + is returned + - if none of the above rules apply, `null` is returned + + @method _classStringForValue + @param path + @param val + @param className + @param falsyClassName + @static + @private + */ + _classStringForValue: function(path, val, className, falsyClassName) { + if(isArray(val)) { + val = get(val, 'length') !== 0; + } + + // When using the colon syntax, evaluate the truthiness or falsiness + // of the value to determine which className to return + if (className || falsyClassName) { + if (className && !!val) { + return className; + + } else if (falsyClassName && !val) { + return falsyClassName; + + } else { + return null; + } + + // If value is a Boolean and true, return the dasherized property + // name. + } else if (val === true) { + // Normalize property path to be suitable for use + // as a class name. For exaple, content.foo.barBaz + // becomes bar-baz. + var parts = path.split('.'); + return dasherize(parts[parts.length-1]); + + // If the value is not false, undefined, or null, return the current + // value of the property. + } else if (val !== false && val != null) { + return val; + + // Nothing to display. Return null so that the old class is removed + // but no new class is added. + } else { + return null; + } + } + }); + + var mutation = EmberObject.extend(Evented).create(); + + View.addMutationListener = function(callback) { + mutation.on('change', callback); + }; + + View.removeMutationListener = function(callback) { + mutation.off('change', callback); + }; + + View.notifyMutationListeners = function() { + mutation.trigger('change'); + }; + + /** + Global views hash + + @property views + @static + @type Hash + */ + View.views = {}; + + // If someone overrides the child views computed property when + // defining their class, we want to be able to process the user's + // supplied childViews and then restore the original computed property + // at view initialization time. This happens in Ember.ContainerView's init + // method. + View.childViewsProperty = childViewsProperty; + + View.applyAttributeBindings = function(elem, name, value) { + var type = typeOf(value); + + // if this changes, also change the logic in ember-handlebars/lib/helpers/binding.js + if (name !== 'value' && (type === 'string' || (type === 'number' && !isNaN(value)))) { + if (value !== elem.attr(name)) { + elem.attr(name, value); + } + } else if (name === 'value' || type === 'boolean') { + if (isNone(value) || value === false) { + // `null`, `undefined` or `false` should remove attribute + elem.removeAttr(name); + elem.prop(name, ''); + } else if (value !== elem.prop(name)) { + // value should always be properties + elem.prop(name, value); + } + } else if (!value) { + elem.removeAttr(name); + } + }; + + __exports__.CoreView = CoreView; + __exports__.View = View; + __exports__.ViewCollection = ViewCollection; + }); +})(); + +(function() { +define("metamorph", + [], + function() { + "use strict"; + // ========================================================================== + // Project: metamorph + // Copyright: ©2014 Tilde, Inc. All rights reserved. + // ========================================================================== + + var K = function() {}, + guid = 0, + disableRange = (function(){ + if ('undefined' !== typeof MetamorphENV) { + return MetamorphENV.DISABLE_RANGE_API; + } else if ('undefined' !== ENV) { + return ENV.DISABLE_RANGE_API; + } else { + return false; + } + })(), + + // Feature-detect the W3C range API, the extended check is for IE9 which only partially supports ranges + supportsRange = (!disableRange) && typeof document !== 'undefined' && ('createRange' in document) && (typeof Range !== 'undefined') && Range.prototype.createContextualFragment, + + // Internet Explorer prior to 9 does not allow setting innerHTML if the first element + // is a "zero-scope" element. This problem can be worked around by making + // the first node an invisible text node. We, like Modernizr, use ­ + needsShy = typeof document !== 'undefined' && (function() { + var testEl = document.createElement('div'); + testEl.innerHTML = "
    "; + testEl.firstChild.innerHTML = ""; + return testEl.firstChild.innerHTML === ''; + })(), + + + // IE 8 (and likely earlier) likes to move whitespace preceeding + // a script tag to appear after it. This means that we can + // accidentally remove whitespace when updating a morph. + movesWhitespace = document && (function() { + var testEl = document.createElement('div'); + testEl.innerHTML = "Test: Value"; + return testEl.childNodes[0].nodeValue === 'Test:' && + testEl.childNodes[2].nodeValue === ' Value'; + })(); + + // Constructor that supports either Metamorph('foo') or new + // Metamorph('foo'); + // + // Takes a string of HTML as the argument. + + var Metamorph = function(html) { + var self; + + if (this instanceof Metamorph) { + self = this; + } else { + self = new K(); + } + + self.innerHTML = html; + var myGuid = 'metamorph-'+(guid++); + self.start = myGuid + '-start'; + self.end = myGuid + '-end'; + + return self; + }; + + K.prototype = Metamorph.prototype; + + var rangeFor, htmlFunc, removeFunc, outerHTMLFunc, appendToFunc, afterFunc, prependFunc, startTagFunc, endTagFunc; + + outerHTMLFunc = function() { + return this.startTag() + this.innerHTML + this.endTag(); + }; + + startTagFunc = function() { + /* + * We replace chevron by its hex code in order to prevent escaping problems. + * Check this thread for more explaination: + * http://stackoverflow.com/questions/8231048/why-use-x3c-instead-of-when-generating-html-from-javascript + */ + return "hi"; + * div.firstChild.firstChild.tagName //=> "" + * + * If our script markers are inside such a node, we need to find that + * node and use *it* as the marker. + */ + var realNode = function(start) { + while (start.parentNode.tagName === "") { + start = start.parentNode; + } + + return start; + }; + + /* + * When automatically adding a tbody, Internet Explorer inserts the + * tbody immediately before the first . Other browsers create it + * before the first node, no matter what. + * + * This means the the following code: + * + * div = document.createElement("div"); + * div.innerHTML = "
    hi
    + * + * Generates the following DOM in IE: + * + * + div + * + table + * - script id='first' + * + tbody + * + tr + * + td + * - "hi" + * - script id='last' + * + * Which means that the two script tags, even though they were + * inserted at the same point in the hierarchy in the original + * HTML, now have different parents. + * + * This code reparents the first script tag by making it the tbody's + * first child. + * + */ + var fixParentage = function(start, end) { + if (start.parentNode !== end.parentNode) { + end.parentNode.insertBefore(start, end.parentNode.firstChild); + } + }; + + htmlFunc = function(html, outerToo) { + // get the real starting node. see realNode for details. + var start = realNode(document.getElementById(this.start)); + var end = document.getElementById(this.end); + var parentNode = end.parentNode; + var node, nextSibling, last; + + // make sure that the start and end nodes share the same + // parent. If not, fix it. + fixParentage(start, end); + + // remove all of the nodes after the starting placeholder and + // before the ending placeholder. + node = start.nextSibling; + while (node) { + nextSibling = node.nextSibling; + last = node === end; + + // if this is the last node, and we want to remove it as well, + // set the `end` node to the next sibling. This is because + // for the rest of the function, we insert the new nodes + // before the end (note that insertBefore(node, null) is + // the same as appendChild(node)). + // + // if we do not want to remove it, just break. + if (last) { + if (outerToo) { end = node.nextSibling; } else { break; } + } + + node.parentNode.removeChild(node); + + // if this is the last node and we didn't break before + // (because we wanted to remove the outer nodes), break + // now. + if (last) { break; } + + node = nextSibling; + } + + // get the first node for the HTML string, even in cases like + // tables and lists where a simple innerHTML on a div would + // swallow some of the content. + node = firstNodeFor(start.parentNode, html); + + if (outerToo) { + start.parentNode.removeChild(start); + } + + // copy the nodes for the HTML between the starting and ending + // placeholder. + while (node) { + nextSibling = node.nextSibling; + parentNode.insertBefore(node, end); + node = nextSibling; + } + }; + + // remove the nodes in the DOM representing this metamorph. + // + // this includes the starting and ending placeholders. + removeFunc = function() { + var start = realNode(document.getElementById(this.start)); + var end = document.getElementById(this.end); + + this.html(''); + start.parentNode.removeChild(start); + end.parentNode.removeChild(end); + }; + + appendToFunc = function(parentNode) { + var node = firstNodeFor(parentNode, this.outerHTML()); + var nextSibling; + + while (node) { + nextSibling = node.nextSibling; + parentNode.appendChild(node); + node = nextSibling; + } + }; + + afterFunc = function(html) { + // get the real starting node. see realNode for details. + var end = document.getElementById(this.end); + var insertBefore = end.nextSibling; + var parentNode = end.parentNode; + var nextSibling; + var node; + + // get the first node for the HTML string, even in cases like + // tables and lists where a simple innerHTML on a div would + // swallow some of the content. + node = firstNodeFor(parentNode, html); + + // copy the nodes for the HTML between the starting and ending + // placeholder. + while (node) { + nextSibling = node.nextSibling; + parentNode.insertBefore(node, insertBefore); + node = nextSibling; + } + }; + + prependFunc = function(html) { + var start = document.getElementById(this.start); + var parentNode = start.parentNode; + var nextSibling; + var node; + + node = firstNodeFor(parentNode, html); + var insertBefore = start.nextSibling; + + while (node) { + nextSibling = node.nextSibling; + parentNode.insertBefore(node, insertBefore); + node = nextSibling; + } + }; + } + + Metamorph.prototype.html = function(html) { + this.checkRemoved(); + if (html === undefined) { return this.innerHTML; } + + htmlFunc.call(this, html); + + this.innerHTML = html; + }; + + Metamorph.prototype.replaceWith = function(html) { + this.checkRemoved(); + htmlFunc.call(this, html, true); + }; + + Metamorph.prototype.remove = removeFunc; + Metamorph.prototype.outerHTML = outerHTMLFunc; + Metamorph.prototype.appendTo = appendToFunc; + Metamorph.prototype.after = afterFunc; + Metamorph.prototype.prepend = prependFunc; + Metamorph.prototype.startTag = startTagFunc; + Metamorph.prototype.endTag = endTagFunc; + + Metamorph.prototype.isRemoved = function() { + var before = document.getElementById(this.start); + var after = document.getElementById(this.end); + + return !before || !after; + }; + + Metamorph.prototype.checkRemoved = function() { + if (this.isRemoved()) { + throw new Error("Cannot perform operations on a Metamorph that is not in the DOM."); + } + }; + + return Metamorph; + }); + +})(); + +(function() { +define("ember-handlebars-compiler", + ["ember-metal/core","exports"], + function(__dependency1__, __exports__) { + "use strict"; + /** + @module ember + @submodule ember-handlebars-compiler + */ + + var Ember = __dependency1__["default"]; + + // ES6Todo: you'll need to import debugger once debugger is es6'd. + if (typeof Ember.assert === 'undefined') { Ember.assert = function(){}; }; + if (typeof Ember.FEATURES === 'undefined') { Ember.FEATURES = { isEnabled: function(){} }; }; + + var objectCreate = Object.create || function(parent) { + function F() {} + F.prototype = parent; + return new F(); + }; + + // set up for circular references later + var View, Component; + + // ES6Todo: when ember-debug is es6'ed import this. + // var emberAssert = Ember.assert; + var Handlebars = (Ember.imports && Ember.imports.Handlebars) || (this && this.Handlebars); + if (!Handlebars && typeof require === 'function') { + Handlebars = require('handlebars'); + } + + + + /** + Prepares the Handlebars templating library for use inside Ember's view + system. + + The `Ember.Handlebars` object is the standard Handlebars library, extended to + use Ember's `get()` method instead of direct property access, which allows + computed properties to be used inside templates. + + To create an `Ember.Handlebars` template, call `Ember.Handlebars.compile()`. + This will return a function that can be used by `Ember.View` for rendering. + + @class Handlebars + @namespace Ember + */ + var EmberHandlebars = Ember.Handlebars = objectCreate(Handlebars); + + /** + Register a bound helper or custom view helper. + + ## Simple bound helper example + + ```javascript + Ember.Handlebars.helper('capitalize', function(value) { + return value.toUpperCase(); + }); + ``` + + The above bound helper can be used inside of templates as follows: + + ```handlebars + {{capitalize name}} + ``` + + In this case, when the `name` property of the template's context changes, + the rendered value of the helper will update to reflect this change. + + For more examples of bound helpers, see documentation for + `Ember.Handlebars.registerBoundHelper`. + + ## Custom view helper example + + Assuming a view subclass named `App.CalendarView` were defined, a helper + for rendering instances of this view could be registered as follows: + + ```javascript + Ember.Handlebars.helper('calendar', App.CalendarView): + ``` + + The above bound helper can be used inside of templates as follows: + + ```handlebars + {{calendar}} + ``` + + Which is functionally equivalent to: + + ```handlebars + {{view App.CalendarView}} + ``` + + Options in the helper will be passed to the view in exactly the same + manner as with the `view` helper. + + @method helper + @for Ember.Handlebars + @param {String} name + @param {Function|Ember.View} function or view class constructor + @param {String} dependentKeys* + */ + EmberHandlebars.helper = function(name, value) { + if (!View) { View = requireModule('ember-views/views/view')['View']; } // ES6TODO: stupid circular dep + if (!Component) { Component = requireModule('ember-views/views/component')['default']; } // ES6TODO: stupid circular dep + + + if (View.detect(value)) { + EmberHandlebars.registerHelper(name, EmberHandlebars.makeViewHelper(value)); + } else { + EmberHandlebars.registerBoundHelper.apply(null, arguments); + } + }; + + /** + Returns a helper function that renders the provided ViewClass. + + Used internally by Ember.Handlebars.helper and other methods + involving helper/component registration. + + @private + @method makeViewHelper + @for Ember.Handlebars + @param {Function} ViewClass view class constructor + @since 1.2.0 + */ + EmberHandlebars.makeViewHelper = function(ViewClass) { + return function(options) { + return EmberHandlebars.helpers.view.call(this, ViewClass, options); + }; + }; + + /** + @class helpers + @namespace Ember.Handlebars + */ + EmberHandlebars.helpers = objectCreate(Handlebars.helpers); + + /** + Override the the opcode compiler and JavaScript compiler for Handlebars. + + @class Compiler + @namespace Ember.Handlebars + @private + @constructor + */ + EmberHandlebars.Compiler = function() {}; + + // Handlebars.Compiler doesn't exist in runtime-only + if (Handlebars.Compiler) { + EmberHandlebars.Compiler.prototype = objectCreate(Handlebars.Compiler.prototype); + } + + EmberHandlebars.Compiler.prototype.compiler = EmberHandlebars.Compiler; + + /** + @class JavaScriptCompiler + @namespace Ember.Handlebars + @private + @constructor + */ + EmberHandlebars.JavaScriptCompiler = function() {}; + + // Handlebars.JavaScriptCompiler doesn't exist in runtime-only + if (Handlebars.JavaScriptCompiler) { + EmberHandlebars.JavaScriptCompiler.prototype = objectCreate(Handlebars.JavaScriptCompiler.prototype); + EmberHandlebars.JavaScriptCompiler.prototype.compiler = EmberHandlebars.JavaScriptCompiler; + } + + + EmberHandlebars.JavaScriptCompiler.prototype.namespace = "Ember.Handlebars"; + + EmberHandlebars.JavaScriptCompiler.prototype.initializeBuffer = function() { + return "''"; + }; + + /** + Override the default buffer for Ember Handlebars. By default, Handlebars + creates an empty String at the beginning of each invocation and appends to + it. Ember's Handlebars overrides this to append to a single shared buffer. + + @private + @method appendToBuffer + @param string {String} + */ + EmberHandlebars.JavaScriptCompiler.prototype.appendToBuffer = function(string) { + return "data.buffer.push("+string+");"; + }; + + // Hacks ahead: + // Handlebars presently has a bug where the `blockHelperMissing` hook + // doesn't get passed the name of the missing helper name, but rather + // gets passed the value of that missing helper evaluated on the current + // context, which is most likely `undefined` and totally useless. + // + // So we alter the compiled template function to pass the name of the helper + // instead, as expected. + // + // This can go away once the following is closed: + // https://github.com/wycats/handlebars.js/issues/634 + + var DOT_LOOKUP_REGEX = /helpers\.(.*?)\)/, + BRACKET_STRING_LOOKUP_REGEX = /helpers\['(.*?)'/, + INVOCATION_SPLITTING_REGEX = /(.*blockHelperMissing\.call\(.*)(stack[0-9]+)(,.*)/; + + EmberHandlebars.JavaScriptCompiler.stringifyLastBlockHelperMissingInvocation = function(source) { + var helperInvocation = source[source.length - 1], + helperName = (DOT_LOOKUP_REGEX.exec(helperInvocation) || BRACKET_STRING_LOOKUP_REGEX.exec(helperInvocation))[1], + matches = INVOCATION_SPLITTING_REGEX.exec(helperInvocation); + + source[source.length - 1] = matches[1] + "'" + helperName + "'" + matches[3]; + }; + + var stringifyBlockHelperMissing = EmberHandlebars.JavaScriptCompiler.stringifyLastBlockHelperMissingInvocation; + + var originalBlockValue = EmberHandlebars.JavaScriptCompiler.prototype.blockValue; + EmberHandlebars.JavaScriptCompiler.prototype.blockValue = function() { + originalBlockValue.apply(this, arguments); + stringifyBlockHelperMissing(this.source); + }; + + var originalAmbiguousBlockValue = EmberHandlebars.JavaScriptCompiler.prototype.ambiguousBlockValue; + EmberHandlebars.JavaScriptCompiler.prototype.ambiguousBlockValue = function() { + originalAmbiguousBlockValue.apply(this, arguments); + stringifyBlockHelperMissing(this.source); + }; + + /** + Rewrite simple mustaches from `{{foo}}` to `{{bind "foo"}}`. This means that + all simple mustaches in Ember's Handlebars will also set up an observer to + keep the DOM up to date when the underlying property changes. + + @private + @method mustache + @for Ember.Handlebars.Compiler + @param mustache + */ + EmberHandlebars.Compiler.prototype.mustache = function(mustache) { + if (!(mustache.params.length || mustache.hash)) { + var id = new Handlebars.AST.IdNode([{ part: '_triageMustache' }]); + + // Update the mustache node to include a hash value indicating whether the original node + // was escaped. This will allow us to properly escape values when the underlying value + // changes and we need to re-render the value. + if (!mustache.escaped) { + mustache.hash = mustache.hash || new Handlebars.AST.HashNode([]); + mustache.hash.pairs.push(["unescaped", new Handlebars.AST.StringNode("true")]); + } + mustache = new Handlebars.AST.MustacheNode([id].concat([mustache.id]), mustache.hash, !mustache.escaped); + } + + return Handlebars.Compiler.prototype.mustache.call(this, mustache); + }; + + /** + Used for precompilation of Ember Handlebars templates. This will not be used + during normal app execution. + + @method precompile + @for Ember.Handlebars + @static + @param {String} string The template to precompile + @param {Boolean} asObject optional parameter, defaulting to true, of whether or not the + compiled template should be returned as an Object or a String + */ + EmberHandlebars.precompile = function(string, asObject) { + var ast = Handlebars.parse(string); + + var options = { + knownHelpers: { + action: true, + unbound: true, + 'bind-attr': true, + template: true, + view: true, + _triageMustache: true + }, + data: true, + stringParams: true + }; + + asObject = asObject === undefined ? true : asObject; + + var environment = new EmberHandlebars.Compiler().compile(ast, options); + return new EmberHandlebars.JavaScriptCompiler().compile(environment, options, undefined, asObject); + }; + + // We don't support this for Handlebars runtime-only + if (Handlebars.compile) { + /** + The entry point for Ember Handlebars. This replaces the default + `Handlebars.compile` and turns on template-local data and String + parameters. + + @method compile + @for Ember.Handlebars + @static + @param {String} string The template to compile + @return {Function} + */ + EmberHandlebars.compile = function(string) { + var ast = Handlebars.parse(string); + var options = { data: true, stringParams: true }; + var environment = new EmberHandlebars.Compiler().compile(ast, options); + var templateSpec = new EmberHandlebars.JavaScriptCompiler().compile(environment, options, undefined, true); + + var template = EmberHandlebars.template(templateSpec); + template.isMethod = false; //Make sure we don't wrap templates with ._super + + return template; + }; + } + + __exports__["default"] = EmberHandlebars; + }); +})(); + +(function() { +define("ember-handlebars/component_lookup", + ["ember-runtime/system/object","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var EmberObject = __dependency1__["default"]; + + var ComponentLookup = EmberObject.extend({ + lookupFactory: function(name, container) { + + container = container || this.container; + + var fullName = 'component:' + name, + templateFullName = 'template:components/' + name, + templateRegistered = container && container.has(templateFullName); + + if (templateRegistered) { + container.injection(fullName, 'layout', templateFullName); + } + + var Component = container.lookupFactory(fullName); + + // Only treat as a component if either the component + // or a template has been registered. + if (templateRegistered || Component) { + if (!Component) { + container.register(fullName, Ember.Component); + Component = container.lookupFactory(fullName); + } + return Component; + } + } + }); + + __exports__["default"] = ComponentLookup; + }); +define("ember-handlebars/controls", + ["ember-handlebars/controls/checkbox","ember-handlebars/controls/text_field","ember-handlebars/controls/text_area","ember-metal/core","ember-handlebars-compiler","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __exports__) { + "use strict"; + var Checkbox = __dependency1__["default"]; + var TextField = __dependency2__["default"]; + var TextArea = __dependency3__["default"]; + + var Ember = __dependency4__["default"]; + // Ember.assert + // var emberAssert = Ember.assert; + + var EmberHandlebars = __dependency5__["default"]; + var helpers = EmberHandlebars.helpers; + /** + @module ember + @submodule ember-handlebars-compiler + */ + + /** + + The `{{input}}` helper inserts an HTML `` tag into the template, + with a `type` value of either `text` or `checkbox`. If no `type` is provided, + `text` will be the default value applied. The attributes of `{{input}}` + match those of the native HTML tag as closely as possible for these two types. + + ## Use as text field + An `{{input}}` with no `type` or a `type` of `text` will render an HTML text input. + The following HTML attributes can be set via the helper: + + + + + + + + + + + + +
    `readonly``required``autofocus`
    `value``placeholder``disabled`
    `size``tabindex``maxlength`
    `name``min``max`
    `pattern``accept``autocomplete`
    `autosave``formaction``formenctype`
    `formmethod``formnovalidate``formtarget`
    `height``inputmode``multiple`
    `step``width``form`
    `selectionDirection``spellcheck` 
    + + + When set to a quoted string, these values will be directly applied to the HTML + element. When left unquoted, these values will be bound to a property on the + template's current rendering context (most typically a controller instance). + + ## Unbound: + + ```handlebars + {{input value="http://www.facebook.com"}} + ``` + + + ```html + + ``` + + ## Bound: + + ```javascript + App.ApplicationController = Ember.Controller.extend({ + firstName: "Stanley", + entryNotAllowed: true + }); + ``` + + + ```handlebars + {{input type="text" value=firstName disabled=entryNotAllowed size="50"}} + ``` + + + ```html + + ``` + + ## Extension + + Internally, `{{input type="text"}}` creates an instance of `Ember.TextField`, passing + arguments from the helper to `Ember.TextField`'s `create` method. You can extend the + capabilities of text inputs in your applications by reopening this class. For example, + if you are building a Bootstrap project where `data-*` attributes are used, you + can add one to the `TextField`'s `attributeBindings` property: + + + ```javascript + Ember.TextField.reopen({ + attributeBindings: ['data-error'] + }); + ``` + + Keep in mind when writing `Ember.TextField` subclasses that `Ember.TextField` + itself extends `Ember.Component`, meaning that it does NOT inherit + the `controller` of the parent view. + + See more about [Ember components](api/classes/Ember.Component.html) + + + ## Use as checkbox + + An `{{input}}` with a `type` of `checkbox` will render an HTML checkbox input. + The following HTML attributes can be set via the helper: + + * `checked` + * `disabled` + * `tabindex` + * `indeterminate` + * `name` + * `autofocus` + * `form` + + + When set to a quoted string, these values will be directly applied to the HTML + element. When left unquoted, these values will be bound to a property on the + template's current rendering context (most typically a controller instance). + + ## Unbound: + + ```handlebars + {{input type="checkbox" name="isAdmin"}} + ``` + + ```html + + ``` + + ## Bound: + + ```javascript + App.ApplicationController = Ember.Controller.extend({ + isAdmin: true + }); + ``` + + + ```handlebars + {{input type="checkbox" checked=isAdmin }} + ``` + + + ```html + + ``` + + ## Extension + + Internally, `{{input type="checkbox"}}` creates an instance of `Ember.Checkbox`, passing + arguments from the helper to `Ember.Checkbox`'s `create` method. You can extend the + capablilties of checkbox inputs in your applications by reopening this class. For example, + if you wanted to add a css class to all checkboxes in your application: + + + ```javascript + Ember.Checkbox.reopen({ + classNames: ['my-app-checkbox'] + }); + ``` + + + @method input + @for Ember.Handlebars.helpers + @param {Hash} options + */ + function inputHelper(options) { + + var hash = options.hash, + types = options.hashTypes, + inputType = hash.type, + onEvent = hash.on; + + delete hash.type; + delete hash.on; + + if (inputType === 'checkbox') { + return helpers.view.call(this, Checkbox, options); + } else { + if (inputType) { hash.type = inputType; } + hash.onEvent = onEvent || 'enter'; + return helpers.view.call(this, TextField, options); + } + } + + /** + `{{textarea}}` inserts a new instance of ` + ``` + + Bound: + + In the following example, the `writtenWords` property on `App.ApplicationController` + will be updated live as the user types 'Lots of text that IS bound' into + the text area of their browser's window. + + ```javascript + App.ApplicationController = Ember.Controller.extend({ + writtenWords: "Lots of text that IS bound" + }); + ``` + + ```handlebars + {{textarea value=writtenWords}} + ``` + + Would result in the following HTML: + + ```html + + ``` + + If you wanted a one way binding between the text area and a div tag + somewhere else on your screen, you could use `Ember.computed.oneWay`: + + ```javascript + App.ApplicationController = Ember.Controller.extend({ + writtenWords: "Lots of text that IS bound", + outputWrittenWords: Ember.computed.oneWay("writtenWords") + }); + ``` + + ```handlebars + {{textarea value=writtenWords}} + +
    + {{outputWrittenWords}} +
    + ``` + + Would result in the following HTML: + + ```html + + + <-- the following div will be updated in real time as you type --> + +
    + Lots of text that IS bound +
    + ``` + + Finally, this example really shows the power and ease of Ember when two + properties are bound to eachother via `Ember.computed.alias`. Type into + either text area box and they'll both stay in sync. Note that + `Ember.computed.alias` costs more in terms of performance, so only use it when + your really binding in both directions: + + ```javascript + App.ApplicationController = Ember.Controller.extend({ + writtenWords: "Lots of text that IS bound", + twoWayWrittenWords: Ember.computed.alias("writtenWords") + }); + ``` + + ```handlebars + {{textarea value=writtenWords}} + {{textarea value=twoWayWrittenWords}} + ``` + + ```html + + + <-- both updated in real time --> + + + ``` + + ## Extension + + Internally, `{{textarea}}` creates an instance of `Ember.TextArea`, passing + arguments from the helper to `Ember.TextArea`'s `create` method. You can + extend the capabilities of text areas in your application by reopening this + class. For example, if you are building a Bootstrap project where `data-*` + attributes are used, you can globally add support for a `data-*` attribute + on all `{{textarea}}`s' in your app by reopening `Ember.TextArea` or + `Ember.TextSupport` and adding it to the `attributeBindings` concatenated + property: + + ```javascript + Ember.TextArea.reopen({ + attributeBindings: ['data-error'] + }); + ``` + + Keep in mind when writing `Ember.TextArea` subclasses that `Ember.TextArea` + itself extends `Ember.Component`, meaning that it does NOT inherit + the `controller` of the parent view. + + See more about [Ember components](api/classes/Ember.Component.html) + + @method textarea + @for Ember.Handlebars.helpers + @param {Hash} options + */ + function textareaHelper(options) { + + var hash = options.hash, + types = options.hashTypes; + + return helpers.view.call(this, TextArea, options); + } + + __exports__.inputHelper = inputHelper; + __exports__.textareaHelper = textareaHelper; + }); +define("ember-handlebars/controls/checkbox", + ["ember-metal/property_get","ember-metal/property_set","ember-views/views/view","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __exports__) { + "use strict"; + var get = __dependency1__.get; + var set = __dependency2__.set; + var View = __dependency3__.View; + + /** + @module ember + @submodule ember-handlebars + */ + + /** + The internal class used to create text inputs when the `{{input}}` + helper is used with `type` of `checkbox`. + + See [handlebars.helpers.input](/api/classes/Ember.Handlebars.helpers.html#method_input) for usage details. + + ## Direct manipulation of `checked` + + The `checked` attribute of an `Ember.Checkbox` object should always be set + through the Ember object or by interacting with its rendered element + representation via the mouse, keyboard, or touch. Updating the value of the + checkbox via jQuery will result in the checked value of the object and its + element losing synchronization. + + ## Layout and LayoutName properties + + Because HTML `input` elements are self closing `layout` and `layoutName` + properties will not be applied. See [Ember.View](/api/classes/Ember.View.html)'s + layout section for more information. + + @class Checkbox + @namespace Ember + @extends Ember.View + */ + var Checkbox = View.extend({ + instrumentDisplay: '{{input type="checkbox"}}', + + classNames: ['ember-checkbox'], + + tagName: 'input', + + attributeBindings: ['type', 'checked', 'indeterminate', 'disabled', 'tabindex', 'name', + 'autofocus', 'required', 'form'], + + type: "checkbox", + checked: false, + disabled: false, + indeterminate: false, + + init: function() { + this._super(); + this.on("change", this, this._updateElementValue); + }, + + didInsertElement: function() { + this._super(); + get(this, 'element').indeterminate = !!get(this, 'indeterminate'); + }, + + _updateElementValue: function() { + set(this, 'checked', this.$().prop('checked')); + } + }); + + __exports__["default"] = Checkbox; + }); +define("ember-handlebars/controls/select", + ["ember-handlebars-compiler","ember-metal/enumerable_utils","ember-metal/property_get","ember-metal/property_set","ember-views/views/view","ember-views/views/collection_view","ember-metal/utils","ember-metal/is_none","ember-metal/computed","ember-runtime/system/native_array","ember-metal/mixin","ember-metal/properties","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __dependency11__, __dependency12__, __exports__) { + "use strict"; + /*jshint eqeqeq:false newcap:false */ + + /** + @module ember + @submodule ember-handlebars + */ + + var EmberHandlebars = __dependency1__["default"]; + var EnumerableUtils = __dependency2__["default"]; + var get = __dependency3__.get; + var set = __dependency4__.set; + var View = __dependency5__.View; + var CollectionView = __dependency6__["default"]; + var isArray = __dependency7__.isArray; + var isNone = __dependency8__["default"]; + var computed = __dependency9__.computed; + var A = __dependency10__.A; + var observer = __dependency11__.observer; + var defineProperty = __dependency12__.defineProperty; + + var indexOf = EnumerableUtils.indexOf, + indexesOf = EnumerableUtils.indexesOf, + forEach = EnumerableUtils.forEach, + replace = EnumerableUtils.replace, + precompileTemplate = EmberHandlebars.compile; + + var SelectOption = View.extend({ + instrumentDisplay: 'Ember.SelectOption', + + tagName: 'option', + attributeBindings: ['value', 'selected'], + + defaultTemplate: function(context, options) { + options = { data: options.data, hash: {} }; + EmberHandlebars.helpers.bind.call(context, "view.label", options); + }, + + init: function() { + this.labelPathDidChange(); + this.valuePathDidChange(); + + this._super(); + }, + + selected: computed(function() { + var content = get(this, 'content'), + selection = get(this, 'parentView.selection'); + if (get(this, 'parentView.multiple')) { + return selection && indexOf(selection, content.valueOf()) > -1; + } else { + // Primitives get passed through bindings as objects... since + // `new Number(4) !== 4`, we use `==` below + return content == selection; + } + }).property('content', 'parentView.selection'), + + labelPathDidChange: observer('parentView.optionLabelPath', function() { + var labelPath = get(this, 'parentView.optionLabelPath'); + + if (!labelPath) { return; } + + defineProperty(this, 'label', computed(function() { + return get(this, labelPath); + }).property(labelPath)); + }), + + valuePathDidChange: observer('parentView.optionValuePath', function() { + var valuePath = get(this, 'parentView.optionValuePath'); + + if (!valuePath) { return; } + + defineProperty(this, 'value', computed(function() { + return get(this, valuePath); + }).property(valuePath)); + }) + }); + + var SelectOptgroup = CollectionView.extend({ + instrumentDisplay: 'Ember.SelectOptgroup', + + tagName: 'optgroup', + attributeBindings: ['label'], + + selectionBinding: 'parentView.selection', + multipleBinding: 'parentView.multiple', + optionLabelPathBinding: 'parentView.optionLabelPath', + optionValuePathBinding: 'parentView.optionValuePath', + + itemViewClassBinding: 'parentView.optionView' + }); + + /** + The `Ember.Select` view class renders a + [select](https://developer.mozilla.org/en/HTML/Element/select) HTML element, + allowing the user to choose from a list of options. + + The text and `value` property of each ` + + + ``` + + The `value` attribute of the selected `"); + return buffer; + } + +function program3(depth0,data) { + + var stack1; + stack1 = helpers.each.call(depth0, "view.groupedContent", {hash:{},hashTypes:{},hashContexts:{},inverse:self.noop,fn:self.program(4, program4, data),contexts:[depth0],types:["ID"],data:data}); + if(stack1 || stack1 === 0) { data.buffer.push(stack1); } + else { data.buffer.push(''); } + } +function program4(depth0,data) { + + + data.buffer.push(escapeExpression(helpers.view.call(depth0, "view.groupView", {hash:{ + 'content': ("content"), + 'label': ("label") + },hashTypes:{'content': "ID",'label': "ID"},hashContexts:{'content': depth0,'label': depth0},contexts:[depth0],types:["ID"],data:data}))); + } + +function program6(depth0,data) { + + var stack1; + stack1 = helpers.each.call(depth0, "view.content", {hash:{},hashTypes:{},hashContexts:{},inverse:self.noop,fn:self.program(7, program7, data),contexts:[depth0],types:["ID"],data:data}); + if(stack1 || stack1 === 0) { data.buffer.push(stack1); } + else { data.buffer.push(''); } + } +function program7(depth0,data) { + + + data.buffer.push(escapeExpression(helpers.view.call(depth0, "view.optionView", {hash:{ + 'content': ("") + },hashTypes:{'content': "ID"},hashContexts:{'content': depth0},contexts:[depth0],types:["ID"],data:data}))); + } + + stack1 = helpers['if'].call(depth0, "view.prompt", {hash:{},hashTypes:{},hashContexts:{},inverse:self.noop,fn:self.program(1, program1, data),contexts:[depth0],types:["ID"],data:data}); + if(stack1 || stack1 === 0) { data.buffer.push(stack1); } + stack1 = helpers['if'].call(depth0, "view.optionGroupPath", {hash:{},hashTypes:{},hashContexts:{},inverse:self.program(6, program6, data),fn:self.program(3, program3, data),contexts:[depth0],types:["ID"],data:data}); + if(stack1 || stack1 === 0) { data.buffer.push(stack1); } + return buffer; + +}), + attributeBindings: ['multiple', 'disabled', 'tabindex', 'name', 'required', 'autofocus', + 'form', 'size'], + + /** + The `multiple` attribute of the select element. Indicates whether multiple + options can be selected. + + @property multiple + @type Boolean + @default false + */ + multiple: false, + + /** + The `disabled` attribute of the select element. Indicates whether + the element is disabled from interactions. + + @property disabled + @type Boolean + @default false + */ + disabled: false, + + /** + The `required` attribute of the select element. Indicates whether + a selected option is required for form validation. + + @property required + @type Boolean + @default false + @since 1.5.0 + */ + required: false, + + /** + The list of options. + + If `optionLabelPath` and `optionValuePath` are not overridden, this should + be a list of strings, which will serve simultaneously as labels and values. + + Otherwise, this should be a list of objects. For instance: + + ```javascript + Ember.Select.create({ + content: A([ + { id: 1, firstName: 'Yehuda' }, + { id: 2, firstName: 'Tom' } + ]), + optionLabelPath: 'content.firstName', + optionValuePath: 'content.id' + }); + ``` + + @property content + @type Array + @default null + */ + content: null, + + /** + When `multiple` is `false`, the element of `content` that is currently + selected, if any. + + When `multiple` is `true`, an array of such elements. + + @property selection + @type Object or Array + @default null + */ + selection: null, + + /** + In single selection mode (when `multiple` is `false`), value can be used to + get the current selection's value or set the selection by it's value. + + It is not currently supported in multiple selection mode. + + @property value + @type String + @default null + */ + value: computed(function(key, value) { + if (arguments.length === 2) { return value; } + var valuePath = get(this, 'optionValuePath').replace(/^content\.?/, ''); + return valuePath ? get(this, 'selection.' + valuePath) : get(this, 'selection'); + }).property('selection'), + + /** + If given, a top-most dummy option will be rendered to serve as a user + prompt. + + @property prompt + @type String + @default null + */ + prompt: null, + + /** + The path of the option labels. See [content](/api/classes/Ember.Select.html#property_content). + + @property optionLabelPath + @type String + @default 'content' + */ + optionLabelPath: 'content', + + /** + The path of the option values. See [content](/api/classes/Ember.Select.html#property_content). + + @property optionValuePath + @type String + @default 'content' + */ + optionValuePath: 'content', + + /** + The path of the option group. + When this property is used, `content` should be sorted by `optionGroupPath`. + + @property optionGroupPath + @type String + @default null + */ + optionGroupPath: null, + + /** + The view class for optgroup. + + @property groupView + @type Ember.View + @default Ember.SelectOptgroup + */ + groupView: SelectOptgroup, + + groupedContent: computed(function() { + var groupPath = get(this, 'optionGroupPath'); + var groupedContent = A(); + var content = get(this, 'content') || []; + + forEach(content, function(item) { + var label = get(item, groupPath); + + if (get(groupedContent, 'lastObject.label') !== label) { + groupedContent.pushObject({ + label: label, + content: A() + }); + } + + get(groupedContent, 'lastObject.content').push(item); + }); + + return groupedContent; + }).property('optionGroupPath', 'content.@each'), + + /** + The view class for option. + + @property optionView + @type Ember.View + @default Ember.SelectOption + */ + optionView: SelectOption, + + _change: function() { + if (get(this, 'multiple')) { + this._changeMultiple(); + } else { + this._changeSingle(); + } + }, + + selectionDidChange: observer('selection.@each', function() { + var selection = get(this, 'selection'); + if (get(this, 'multiple')) { + if (!isArray(selection)) { + set(this, 'selection', A([selection])); + return; + } + this._selectionDidChangeMultiple(); + } else { + this._selectionDidChangeSingle(); + } + }), + + valueDidChange: observer('value', function() { + var content = get(this, 'content'), + value = get(this, 'value'), + valuePath = get(this, 'optionValuePath').replace(/^content\.?/, ''), + selectedValue = (valuePath ? get(this, 'selection.' + valuePath) : get(this, 'selection')), + selection; + + if (value !== selectedValue) { + selection = content ? content.find(function(obj) { + return value === (valuePath ? get(obj, valuePath) : obj); + }) : null; + + this.set('selection', selection); + } + }), + + + _triggerChange: function() { + var selection = get(this, 'selection'); + var value = get(this, 'value'); + + if (!isNone(selection)) { this.selectionDidChange(); } + if (!isNone(value)) { this.valueDidChange(); } + + this._change(); + }, + + _changeSingle: function() { + var selectedIndex = this.$()[0].selectedIndex, + content = get(this, 'content'), + prompt = get(this, 'prompt'); + + if (!content || !get(content, 'length')) { return; } + if (prompt && selectedIndex === 0) { set(this, 'selection', null); return; } + + if (prompt) { selectedIndex -= 1; } + set(this, 'selection', content.objectAt(selectedIndex)); + }, + + + _changeMultiple: function() { + var options = this.$('option:selected'), + prompt = get(this, 'prompt'), + offset = prompt ? 1 : 0, + content = get(this, 'content'), + selection = get(this, 'selection'); + + if (!content) { return; } + if (options) { + var selectedIndexes = options.map(function() { + return this.index - offset; + }).toArray(); + var newSelection = content.objectsAt(selectedIndexes); + + if (isArray(selection)) { + replace(selection, 0, get(selection, 'length'), newSelection); + } else { + set(this, 'selection', newSelection); + } + } + }, + + _selectionDidChangeSingle: function() { + var el = this.get('element'); + if (!el) { return; } + + var content = get(this, 'content'), + selection = get(this, 'selection'), + selectionIndex = content ? indexOf(content, selection) : -1, + prompt = get(this, 'prompt'); + + if (prompt) { selectionIndex += 1; } + if (el) { el.selectedIndex = selectionIndex; } + }, + + _selectionDidChangeMultiple: function() { + var content = get(this, 'content'), + selection = get(this, 'selection'), + selectedIndexes = content ? indexesOf(content, selection) : [-1], + prompt = get(this, 'prompt'), + offset = prompt ? 1 : 0, + options = this.$('option'), + adjusted; + + if (options) { + options.each(function() { + adjusted = this.index > -1 ? this.index - offset : -1; + this.selected = indexOf(selectedIndexes, adjusted) > -1; + }); + } + }, + + init: function() { + this._super(); + this.on("didInsertElement", this, this._triggerChange); + this.on("change", this, this._change); + } + }); + + __exports__["default"] = Select + __exports__.Select = Select; + __exports__.SelectOption = SelectOption; + __exports__.SelectOptgroup = SelectOptgroup; + }); +define("ember-handlebars/controls/text_area", + ["ember-metal/property_get","ember-views/views/component","ember-handlebars/controls/text_support","ember-metal/mixin","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __exports__) { + "use strict"; + + /** + @module ember + @submodule ember-handlebars + */ + var get = __dependency1__.get; + var Component = __dependency2__["default"]; + var TextSupport = __dependency3__["default"]; + var observer = __dependency4__.observer; + + /** + The internal class used to create textarea element when the `{{textarea}}` + helper is used. + + See [handlebars.helpers.textarea](/api/classes/Ember.Handlebars.helpers.html#method_textarea) for usage details. + + ## Layout and LayoutName properties + + Because HTML `textarea` elements do not contain inner HTML the `layout` and + `layoutName` properties will not be applied. See [Ember.View](/api/classes/Ember.View.html)'s + layout section for more information. + + @class TextArea + @namespace Ember + @extends Ember.Component + @uses Ember.TextSupport + */ + var TextArea = Component.extend(TextSupport, { + instrumentDisplay: '{{textarea}}', + + classNames: ['ember-text-area'], + + tagName: "textarea", + attributeBindings: ['rows', 'cols', 'name', 'selectionEnd', 'selectionStart', 'wrap'], + rows: null, + cols: null, + + _updateElementValue: observer('value', function() { + // We do this check so cursor position doesn't get affected in IE + var value = get(this, 'value'), + $el = this.$(); + if ($el && value !== $el.val()) { + $el.val(value); + } + }), + + init: function() { + this._super(); + this.on("didInsertElement", this, this._updateElementValue); + } + + }); + + __exports__["default"] = TextArea; + }); +define("ember-handlebars/controls/text_field", + ["ember-metal/property_get","ember-metal/property_set","ember-views/views/component","ember-handlebars/controls/text_support","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __exports__) { + "use strict"; + /** + @module ember + @submodule ember-handlebars + */ + + var get = __dependency1__.get; + var set = __dependency2__.set; + var Component = __dependency3__["default"]; + var TextSupport = __dependency4__["default"]; + + /** + + The internal class used to create text inputs when the `{{input}}` + helper is used with `type` of `text`. + + See [Handlebars.helpers.input](/api/classes/Ember.Handlebars.helpers.html#method_input) for usage details. + + ## Layout and LayoutName properties + + Because HTML `input` elements are self closing `layout` and `layoutName` + properties will not be applied. See [Ember.View](/api/classes/Ember.View.html)'s + layout section for more information. + + @class TextField + @namespace Ember + @extends Ember.Component + @uses Ember.TextSupport + */ + var TextField = Component.extend(TextSupport, { + instrumentDisplay: '{{input type="text"}}', + + classNames: ['ember-text-field'], + tagName: "input", + attributeBindings: ['type', 'value', 'size', 'pattern', 'name', 'min', 'max', + 'accept', 'autocomplete', 'autosave', 'formaction', + 'formenctype', 'formmethod', 'formnovalidate', 'formtarget', + 'height', 'inputmode', 'list', 'multiple', 'pattern', 'step', + 'width'], + + /** + The `value` attribute of the input element. As the user inputs text, this + property is updated live. + + @property value + @type String + @default "" + */ + value: "", + + /** + The `type` attribute of the input element. + + @property type + @type String + @default "text" + */ + type: "text", + + /** + The `size` of the text field in characters. + + @property size + @type String + @default null + */ + size: null, + + /** + The `pattern` attribute of input element. + + @property pattern + @type String + @default null + */ + pattern: null, + + /** + The `min` attribute of input element used with `type="number"` or `type="range"`. + + @property min + @type String + @default null + @since 1.4.0 + */ + min: null, + + /** + The `max` attribute of input element used with `type="number"` or `type="range"`. + + @property max + @type String + @default null + @since 1.4.0 + */ + max: null + }); + + __exports__["default"] = TextField; + }); +define("ember-handlebars/controls/text_support", + ["ember-metal/property_get","ember-metal/property_set","ember-metal/mixin","ember-runtime/mixins/target_action_support","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __exports__) { + "use strict"; + /** + @module ember + @submodule ember-handlebars + */ + + var get = __dependency1__.get; + var set = __dependency2__.set; + var Mixin = __dependency3__.Mixin; + var TargetActionSupport = __dependency4__["default"]; + + /** + Shared mixin used by `Ember.TextField` and `Ember.TextArea`. + + @class TextSupport + @namespace Ember + @uses Ember.TargetActionSupport + @extends Ember.Mixin + @private + */ + var TextSupport = Mixin.create(TargetActionSupport, { + value: "", + + attributeBindings: ['placeholder', 'disabled', 'maxlength', 'tabindex', 'readonly', + 'autofocus', 'form', 'selectionDirection', 'spellcheck', 'required', + 'title', 'autocapitalize', 'autocorrect'], + placeholder: null, + disabled: false, + maxlength: null, + + init: function() { + this._super(); + this.on("focusOut", this, this._elementValueDidChange); + this.on("change", this, this._elementValueDidChange); + this.on("paste", this, this._elementValueDidChange); + this.on("cut", this, this._elementValueDidChange); + this.on("input", this, this._elementValueDidChange); + this.on("keyUp", this, this.interpretKeyEvents); + }, + + /** + The action to be sent when the user presses the return key. + + This is similar to the `{{action}}` helper, but is fired when + the user presses the return key when editing a text field, and sends + the value of the field as the context. + + @property action + @type String + @default null + */ + action: null, + + /** + The event that should send the action. + + Options are: + + * `enter`: the user pressed enter + * `keyPress`: the user pressed a key + + @property onEvent + @type String + @default enter + */ + onEvent: 'enter', + + /** + Whether they `keyUp` event that triggers an `action` to be sent continues + propagating to other views. + + By default, when the user presses the return key on their keyboard and + the text field has an `action` set, the action will be sent to the view's + controller and the key event will stop propagating. + + If you would like parent views to receive the `keyUp` event even after an + action has been dispatched, set `bubbles` to true. + + @property bubbles + @type Boolean + @default false + */ + bubbles: false, + + interpretKeyEvents: function(event) { + var map = TextSupport.KEY_EVENTS; + var method = map[event.keyCode]; + + this._elementValueDidChange(); + if (method) { return this[method](event); } + }, + + _elementValueDidChange: function() { + set(this, 'value', this.$().val()); + }, + + /** + The action to be sent when the user inserts a new line. + + Called by the `Ember.TextSupport` mixin on keyUp if keycode matches 13. + Uses sendAction to send the `enter` action to the controller. + + @method insertNewline + @param {Event} event + */ + insertNewline: function(event) { + sendAction('enter', this, event); + sendAction('insert-newline', this, event); + }, + + /** + Called when the user hits escape. + + Called by the `Ember.TextSupport` mixin on keyUp if keycode matches 27. + Uses sendAction to send the `escape-press` action to the controller. + + @method cancel + @param {Event} event + */ + cancel: function(event) { + sendAction('escape-press', this, event); + }, + + /** + Called when the text area is focused. + + @method focusIn + @param {Event} event + */ + focusIn: function(event) { + sendAction('focus-in', this, event); + }, + + /** + Called when the text area is blurred. + + @method focusOut + @param {Event} event + */ + focusOut: function(event) { + sendAction('focus-out', this, event); + }, + + /** + The action to be sent when the user presses a key. Enabled by setting + the `onEvent` property to `keyPress`. + + Uses sendAction to send the `keyPress` action to the controller. + + @method keyPress + @param {Event} event + */ + keyPress: function(event) { + sendAction('key-press', this, event); + } + + }); + + TextSupport.KEY_EVENTS = { + 13: 'insertNewline', + 27: 'cancel' + }; + + // In principle, this shouldn't be necessary, but the legacy + // sendAction semantics for TextField are different from + // the component semantics so this method normalizes them. + function sendAction(eventName, view, event) { + var action = get(view, eventName), + on = get(view, 'onEvent'), + value = get(view, 'value'); + + // back-compat support for keyPress as an event name even though + // it's also a method name that consumes the event (and therefore + // incompatible with sendAction semantics). + if (on === eventName || (on === 'keyPress' && eventName === 'key-press')) { + view.sendAction('action', value); + } + + view.sendAction(eventName, value); + + if (action || on === eventName) { + if(!get(view, 'bubbles')) { + event.stopPropagation(); + } + } + } + + __exports__["default"] = TextSupport; + }); +define("ember-handlebars/ext", + ["ember-metal/core","ember-runtime/system/string","ember-handlebars-compiler","ember-metal/property_get","ember-metal/binding","ember-metal/error","ember-metal/mixin","ember-metal/is_empty","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __exports__) { + "use strict"; + var Ember = __dependency1__["default"]; + // Ember.FEATURES, Ember.assert, Ember.Handlebars, Ember.lookup + // var emberAssert = Ember.assert; + + var fmt = __dependency2__.fmt; + + var EmberHandlebars = __dependency3__["default"]; + var helpers = EmberHandlebars.helpers; + + var get = __dependency4__.get; + var isGlobalPath = __dependency5__.isGlobalPath; + var EmberError = __dependency6__["default"]; + var IS_BINDING = __dependency7__.IS_BINDING; + + // late bound via requireModule because of circular dependencies. + var resolveHelper, + SimpleHandlebarsView; + + var isEmpty = __dependency8__["default"]; + + var slice = [].slice, originalTemplate = EmberHandlebars.template; + + /** + If a path starts with a reserved keyword, returns the root + that should be used. + + @private + @method normalizePath + @for Ember + @param root {Object} + @param path {String} + @param data {Hash} + */ + function normalizePath(root, path, data) { + var keywords = (data && data.keywords) || {}, + keyword, isKeyword; + + // Get the first segment of the path. For example, if the + // path is "foo.bar.baz", returns "foo". + keyword = path.split('.', 1)[0]; + + // Test to see if the first path is a keyword that has been + // passed along in the view's data hash. If so, we will treat + // that object as the new root. + if (keywords.hasOwnProperty(keyword)) { + // Look up the value in the template's data hash. + root = keywords[keyword]; + isKeyword = true; + + // Handle cases where the entire path is the reserved + // word. In that case, return the object itself. + if (path === keyword) { + path = ''; + } else { + // Strip the keyword from the path and look up + // the remainder from the newly found root. + path = path.substr(keyword.length+1); + } + } + + return { root: root, path: path, isKeyword: isKeyword }; + }; + + + /** + Lookup both on root and on window. If the path starts with + a keyword, the corresponding object will be looked up in the + template's data hash and used to resolve the path. + + @method get + @for Ember.Handlebars + @param {Object} root The object to look up the property on + @param {String} path The path to be lookedup + @param {Object} options The template's option hash + */ + function handlebarsGet(root, path, options) { + var data = options && options.data, + normalizedPath = normalizePath(root, path, data), + value; + + + root = normalizedPath.root; + path = normalizedPath.path; + + value = get(root, path); + + if (value === undefined && root !== Ember.lookup && isGlobalPath(path)) { + value = get(Ember.lookup, path); + } + + + return value; + } + + /** + This method uses `Ember.Handlebars.get` to lookup a value, then ensures + that the value is escaped properly. + + If `unescaped` is a truthy value then the escaping will not be performed. + + @method getEscaped + @for Ember.Handlebars + @param {Object} root The object to look up the property on + @param {String} path The path to be lookedup + @param {Object} options The template's option hash + @since 1.4.0 + */ + function getEscaped(root, path, options) { + var result = handlebarsGet(root, path, options); + + if (result === null || result === undefined) { + result = ""; + } else if (!(result instanceof Handlebars.SafeString)) { + result = String(result); + } + if (!options.hash.unescaped){ + result = Handlebars.Utils.escapeExpression(result); + } + + return result; + }; + + function resolveParams(context, params, options) { + var resolvedParams = [], types = options.types, param, type; + + for (var i=0, l=params.length; i{{user.name}} + +
    +
    {{user.role.label}}
    + {{user.role.id}} + +

    {{user.role.description}}

    +
    + ``` + + `{{with}}` can be our best friend in these cases, + instead of writing `user.role.*` over and over, we use `{{#with user.role}}`. + Now the context within the `{{#with}} .. {{/with}}` block is `user.role` so you can do the following: + + ```handlebars +
    {{user.name}}
    + +
    + {{#with user.role}} +
    {{label}}
    + {{id}} + +

    {{description}}

    + {{/with}} +
    + ``` + + ### `as` operator + + This operator aliases the scope to a new name. It's helpful for semantic clarity and to retain + default scope or to reference from another `{{with}}` block. + + ```handlebars + // posts might not be + {{#with user.posts as blogPosts}} +
    + There are {{blogPosts.length}} blog posts written by {{user.name}}. +
    + + {{#each post in blogPosts}} +
  • {{post.title}}
  • + {{/each}} + {{/with}} + ``` + + Without the `as` operator, it would be impossible to reference `user.name` in the example above. + + NOTE: The alias should not reuse a name from the bound property path. + For example: `{{#with foo.bar as foo}}` is not supported because it attempts to alias using + the first part of the property path, `foo`. Instead, use `{{#with foo.bar as baz}}`. + + ### `controller` option + + Adding `controller='something'` instructs the `{{with}}` helper to create and use an instance of + the specified controller with the new context as its content. + + This is very similar to using an `itemController` option with the `{{each}}` helper. + + ```handlebars + {{#with users.posts controller='userBlogPosts'}} + {{!- The current context is wrapped in our controller instance }} + {{/with}} + ``` + + In the above example, the template provided to the `{{with}}` block is now wrapped in the + `userBlogPost` controller, which provides a very elegant way to decorate the context with custom + functions/properties. + + @method with + @for Ember.Handlebars.helpers + @param {Function} context + @param {Hash} options + @return {String} HTML string + */ + function withHelper(context, options) { + var bindContext, preserveContext, controller, helperName = 'with'; + + if (arguments.length === 4) { + var keywordName, path, rootPath, normalized, contextPath; + + options = arguments[3]; + keywordName = arguments[2]; + path = arguments[0]; + + if (path) { + helperName += ' ' + path + ' as ' + keywordName; + } + + + var localizedOptions = o_create(options); + localizedOptions.data = o_create(options.data); + localizedOptions.data.keywords = o_create(options.data.keywords || {}); + + if (isGlobalPath(path)) { + contextPath = path; + } else { + normalized = normalizePath(this, path, options.data); + path = normalized.path; + rootPath = normalized.root; + + // This is a workaround for the fact that you cannot bind separate objects + // together. When we implement that functionality, we should use it here. + var contextKey = jQuery.expando + guidFor(rootPath); + localizedOptions.data.keywords[contextKey] = rootPath; + // if the path is '' ("this"), just bind directly to the current context + contextPath = path ? contextKey + '.' + path : contextKey; + } + + localizedOptions.hash.keywordName = keywordName; + localizedOptions.hash.keywordPath = contextPath; + + bindContext = this; + context = path; + options = localizedOptions; + preserveContext = true; + } else { + + helperName += ' ' + context; + bindContext = options.contexts[0]; + preserveContext = false; + } + + options.helperName = helperName; + options.isWithHelper = true; + + return bind.call(bindContext, context, options, preserveContext, exists); + } + /** + See [boundIf](/api/classes/Ember.Handlebars.helpers.html#method_boundIf) + and [unboundIf](/api/classes/Ember.Handlebars.helpers.html#method_unboundIf) + + @method if + @for Ember.Handlebars.helpers + @param {Function} context + @param {Hash} options + @return {String} HTML string + */ + function ifHelper(context, options) { + + options.helperName = options.helperName || ('if ' + context); + + if (options.data.isUnbound) { + return helpers.unboundIf.call(options.contexts[0], context, options); + } else { + return helpers.boundIf.call(options.contexts[0], context, options); + } + } + + /** + @method unless + @for Ember.Handlebars.helpers + @param {Function} context + @param {Hash} options + @return {String} HTML string + */ + function unlessHelper(context, options) { + + var fn = options.fn, inverse = options.inverse, helperName = 'unless'; + + if (context) { + helperName += ' ' + context; + } + + options.fn = inverse; + options.inverse = fn; + + options.helperName = options.helperName || helperName; + + if (options.data.isUnbound) { + return helpers.unboundIf.call(options.contexts[0], context, options); + } else { + return helpers.boundIf.call(options.contexts[0], context, options); + } + } + + /** + `bind-attr` allows you to create a binding between DOM element attributes and + Ember objects. For example: + + ```handlebars + imageTitle + ``` + + The above handlebars template will fill the ``'s `src` attribute will + the value of the property referenced with `"imageUrl"` and its `alt` + attribute with the value of the property referenced with `"imageTitle"`. + + If the rendering context of this template is the following object: + + ```javascript + { + imageUrl: 'http://lolcats.info/haz-a-funny', + imageTitle: 'A humorous image of a cat' + } + ``` + + The resulting HTML output will be: + + ```html + A humorous image of a cat + ``` + + `bind-attr` cannot redeclare existing DOM element attributes. The use of `src` + in the following `bind-attr` example will be ignored and the hard coded value + of `src="/failwhale.gif"` will take precedence: + + ```handlebars + imageTitle + ``` + + ### `bind-attr` and the `class` attribute + + `bind-attr` supports a special syntax for handling a number of cases unique + to the `class` DOM element attribute. The `class` attribute combines + multiple discrete values into a single attribute as a space-delimited + list of strings. Each string can be: + + * a string return value of an object's property. + * a boolean return value of an object's property + * a hard-coded value + + A string return value works identically to other uses of `bind-attr`. The + return value of the property will become the value of the attribute. For + example, the following view and template: + + ```javascript + AView = View.extend({ + someProperty: function() { + return "aValue"; + }.property() + }) + ``` + + ```handlebars + + ``` + + A boolean return value will insert a specified class name if the property + returns `true` and remove the class name if the property returns `false`. + + A class name is provided via the syntax + `somePropertyName:class-name-if-true`. + + ```javascript + AView = View.extend({ + someBool: true + }) + ``` + + ```handlebars + + ``` + + Result in the following rendered output: + + ```html + + ``` + + An additional section of the binding can be provided if you want to + replace the existing class instead of removing it when the boolean + value changes: + + ```handlebars + + ``` + + A hard-coded value can be used by prepending `:` to the desired + class name: `:class-name-to-always-apply`. + + ```handlebars + + ``` + + Results in the following rendered output: + + ```html + + ``` + + All three strategies - string return value, boolean return value, and + hard-coded value – can be combined in a single declaration: + + ```handlebars + + ``` + + @method bind-attr + @for Ember.Handlebars.helpers + @param {Hash} options + @return {String} HTML string + */ + function bindAttrHelper(options) { + var attrs = options.hash; + + + var view = options.data.view; + var ret = []; + + // we relied on the behavior of calling without + // context to mean this === window, but when running + // "use strict", it's possible for this to === undefined; + var ctx = this || window; + + // Generate a unique id for this element. This will be added as a + // data attribute to the element so it can be looked up when + // the bound property changes. + var dataId = ++Ember.uuid; + + // Handle classes differently, as we can bind multiple classes + var classBindings = attrs['class']; + if (classBindings != null) { + var classResults = bindClasses(ctx, classBindings, view, dataId, options); + + ret.push('class="' + Handlebars.Utils.escapeExpression(classResults.join(' ')) + '"'); + delete attrs['class']; + } + + var attrKeys = keys(attrs); + + // For each attribute passed, create an observer and emit the + // current value of the property as an attribute. + forEach.call(attrKeys, function(attr) { + var path = attrs[attr], + normalized; + + + normalized = normalizePath(ctx, path, options.data); + + var value = (path === 'this') ? normalized.root : handlebarsGet(ctx, path, options), + type = typeOf(value); + + + var observer, invoker; + + observer = function observer() { + var result = handlebarsGet(ctx, path, options); + + + var elem = view.$("[data-bindattr-" + dataId + "='" + dataId + "']"); + + // If we aren't able to find the element, it means the element + // to which we were bound has been removed from the view. + // In that case, we can assume the template has been re-rendered + // and we need to clean up the observer. + if (!elem || elem.length === 0) { + removeObserver(normalized.root, normalized.path, invoker); + return; + } + + View.applyAttributeBindings(elem, attr, result); + }; + + // Add an observer to the view for when the property changes. + // When the observer fires, find the element using the + // unique data id and update the attribute to the new value. + // Note: don't add observer when path is 'this' or path + // is whole keyword e.g. {{#each x in list}} ... {{bind-attr attr="x"}} + if (path !== 'this' && !(normalized.isKeyword && normalized.path === '' )) { + view.registerObserver(normalized.root, normalized.path, observer); + } + + // if this changes, also change the logic in ember-views/lib/views/view.js + if ((type === 'string' || (type === 'number' && !isNaN(value)))) { + ret.push(attr + '="' + Handlebars.Utils.escapeExpression(value) + '"'); + } else if (value && type === 'boolean') { + // The developer controls the attr name, so it should always be safe + ret.push(attr + '="' + attr + '"'); + } + }, this); + + // Add the unique identifier + // NOTE: We use all lower-case since Firefox has problems with mixed case in SVG + ret.push('data-bindattr-' + dataId + '="' + dataId + '"'); + return new SafeString(ret.join(' ')); + } + + /** + See `bind-attr` + + @method bindAttr + @for Ember.Handlebars.helpers + @deprecated + @param {Function} context + @param {Hash} options + @return {String} HTML string + */ + function bindAttrHelperDeprecated() { + return helpers['bind-attr'].apply(this, arguments); + } + + /** + Helper that, given a space-separated string of property paths and a context, + returns an array of class names. Calling this method also has the side + effect of setting up observers at those property paths, such that if they + change, the correct class name will be reapplied to the DOM element. + + For example, if you pass the string "fooBar", it will first look up the + "fooBar" value of the context. If that value is true, it will add the + "foo-bar" class to the current element (i.e., the dasherized form of + "fooBar"). If the value is a string, it will add that string as the class. + Otherwise, it will not add any new class name. + + @private + @method bindClasses + @for Ember.Handlebars + @param {Ember.Object} context The context from which to lookup properties + @param {String} classBindings A string, space-separated, of class bindings + to use + @param {View} view The view in which observers should look for the + element to update + @param {Srting} bindAttrId Optional bindAttr id used to lookup elements + @return {Array} An array of class names to add + */ + function bindClasses(context, classBindings, view, bindAttrId, options) { + var ret = [], newClass, value, elem; + + // Helper method to retrieve the property from the context and + // determine which class string to return, based on whether it is + // a Boolean or not. + var classStringForPath = function(root, parsedPath, options) { + var val, + path = parsedPath.path; + + if (path === 'this') { + val = root; + } else if (path === '') { + val = true; + } else { + val = handlebarsGet(root, path, options); + } + + return View._classStringForValue(path, val, parsedPath.className, parsedPath.falsyClassName); + }; + + // For each property passed, loop through and setup + // an observer. + forEach.call(classBindings.split(' '), function(binding) { + + // Variable in which the old class value is saved. The observer function + // closes over this variable, so it knows which string to remove when + // the property changes. + var oldClass; + + var observer, invoker; + + var parsedPath = View._parsePropertyPath(binding), + path = parsedPath.path, + pathRoot = context, + normalized; + + if (path !== '' && path !== 'this') { + normalized = normalizePath(context, path, options.data); + + pathRoot = normalized.root; + path = normalized.path; + } + + // Set up an observer on the context. If the property changes, toggle the + // class name. + observer = function() { + // Get the current value of the property + newClass = classStringForPath(context, parsedPath, options); + elem = bindAttrId ? view.$("[data-bindattr-" + bindAttrId + "='" + bindAttrId + "']") : view.$(); + + // If we can't find the element anymore, a parent template has been + // re-rendered and we've been nuked. Remove the observer. + if (!elem || elem.length === 0) { + removeObserver(pathRoot, path, invoker); + } else { + // If we had previously added a class to the element, remove it. + if (oldClass) { + elem.removeClass(oldClass); + } + + // If necessary, add a new class. Make sure we keep track of it so + // it can be removed in the future. + if (newClass) { + elem.addClass(newClass); + oldClass = newClass; + } else { + oldClass = null; + } + } + }; + + if (path !== '' && path !== 'this') { + view.registerObserver(pathRoot, path, observer); + } + + // We've already setup the observer; now we just need to figure out the + // correct behavior right now on the first pass through. + value = classStringForPath(context, parsedPath, options); + + if (value) { + ret.push(value); + + // Make sure we save the current value so that it can be removed if the + // observer fires. + oldClass = value; + } + }); + + return ret; + }; + + __exports__.bind = bind; + __exports__._triageMustacheHelper = _triageMustacheHelper; + __exports__.resolveHelper = resolveHelper; + __exports__.bindHelper = bindHelper; + __exports__.boundIfHelper = boundIfHelper; + __exports__.unboundIfHelper = unboundIfHelper; + __exports__.withHelper = withHelper; + __exports__.ifHelper = ifHelper; + __exports__.unlessHelper = unlessHelper; + __exports__.bindAttrHelper = bindAttrHelper; + __exports__.bindAttrHelperDeprecated = bindAttrHelperDeprecated; + __exports__.bindClasses = bindClasses; + }); +define("ember-handlebars/helpers/collection", + ["ember-metal/core","ember-metal/utils","ember-handlebars-compiler","ember-runtime/system/string","ember-metal/property_get","ember-handlebars/ext","ember-handlebars/helpers/view","ember-metal/computed","ember-views/views/collection_view","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __exports__) { + "use strict"; + /** + @module ember + @submodule ember-handlebars + */ + + var Ember = __dependency1__["default"]; + // Ember.assert, Ember.deprecate + var inspect = __dependency2__.inspect; + + // var emberAssert = Ember.assert; + // emberDeprecate = Ember.deprecate; + + var EmberHandlebars = __dependency3__["default"]; + var helpers = EmberHandlebars.helpers; + + var fmt = __dependency4__.fmt; + var get = __dependency5__.get; + var handlebarsGet = __dependency6__.handlebarsGet; + var ViewHelper = __dependency7__.ViewHelper; + var computed = __dependency8__.computed; + var CollectionView = __dependency9__["default"]; + + var alias = computed.alias; + /** + `{{collection}}` is a `Ember.Handlebars` helper for adding instances of + `Ember.CollectionView` to a template. See [Ember.CollectionView](/api/classes/Ember.CollectionView.html) + for additional information on how a `CollectionView` functions. + + `{{collection}}`'s primary use is as a block helper with a `contentBinding` + option pointing towards an `Ember.Array`-compatible object. An `Ember.View` + instance will be created for each item in its `content` property. Each view + will have its own `content` property set to the appropriate item in the + collection. + + The provided block will be applied as the template for each item's view. + + Given an empty `` the following template: + + ```handlebars + {{#collection contentBinding="App.items"}} + Hi {{view.content.name}} + {{/collection}} + ``` + + And the following application code + + ```javascript + App = Ember.Application.create() + App.items = [ + Ember.Object.create({name: 'Dave'}), + Ember.Object.create({name: 'Mary'}), + Ember.Object.create({name: 'Sara'}) + ] + ``` + + Will result in the HTML structure below + + ```html +
    +
    Hi Dave
    +
    Hi Mary
    +
    Hi Sara
    +
    + ``` + + ### Blockless use in a collection + + If you provide an `itemViewClass` option that has its own `template` you can + omit the block. + + The following template: + + ```handlebars + {{collection contentBinding="App.items" itemViewClass="App.AnItemView"}} + ``` + + And application code + + ```javascript + App = Ember.Application.create(); + App.items = [ + Ember.Object.create({name: 'Dave'}), + Ember.Object.create({name: 'Mary'}), + Ember.Object.create({name: 'Sara'}) + ]; + + App.AnItemView = Ember.View.extend({ + template: Ember.Handlebars.compile("Greetings {{view.content.name}}") + }); + ``` + + Will result in the HTML structure below + + ```html +
    +
    Greetings Dave
    +
    Greetings Mary
    +
    Greetings Sara
    +
    + ``` + + ### Specifying a CollectionView subclass + + By default the `{{collection}}` helper will create an instance of + `Ember.CollectionView`. You can supply a `Ember.CollectionView` subclass to + the helper by passing it as the first argument: + + ```handlebars + {{#collection App.MyCustomCollectionClass contentBinding="App.items"}} + Hi {{view.content.name}} + {{/collection}} + ``` + + ### Forwarded `item.*`-named Options + + As with the `{{view}}`, helper options passed to the `{{collection}}` will be + set on the resulting `Ember.CollectionView` as properties. Additionally, + options prefixed with `item` will be applied to the views rendered for each + item (note the camelcasing): + + ```handlebars + {{#collection contentBinding="App.items" + itemTagName="p" + itemClassNames="greeting"}} + Howdy {{view.content.name}} + {{/collection}} + ``` + + Will result in the following HTML structure: + + ```html +
    +

    Howdy Dave

    +

    Howdy Mary

    +

    Howdy Sara

    +
    + ``` + + @method collection + @for Ember.Handlebars.helpers + @param {String} path + @param {Hash} options + @return {String} HTML string + @deprecated Use `{{each}}` helper instead. + */ + function collectionHelper(path, options) { + + // If no path is provided, treat path param as options. + if (path && path.data && path.data.isRenderData) { + options = path; + path = undefined; + } else { + } + + var fn = options.fn; + var data = options.data; + var inverse = options.inverse; + var view = options.data.view; + + + var controller, container; + // If passed a path string, convert that into an object. + // Otherwise, just default to the standard class. + var collectionClass; + if (path) { + controller = data.keywords.controller; + container = controller && controller.container; + collectionClass = handlebarsGet(this, path, options) || container.lookupFactory('view:' + path); + } + else { + collectionClass = CollectionView; + } + + var hash = options.hash, itemHash = {}, match; + + // Extract item view class if provided else default to the standard class + var collectionPrototype = collectionClass.proto(), itemViewClass; + + if (hash.itemView) { + controller = data.keywords.controller; + container = controller.container; + itemViewClass = container.lookupFactory('view:' + hash.itemView); + } else if (hash.itemViewClass) { + itemViewClass = handlebarsGet(collectionPrototype, hash.itemViewClass, options); + } else { + itemViewClass = collectionPrototype.itemViewClass; + } + + + delete hash.itemViewClass; + delete hash.itemView; + + // Go through options passed to the {{collection}} helper and extract options + // that configure item views instead of the collection itself. + for (var prop in hash) { + if (hash.hasOwnProperty(prop)) { + match = prop.match(/^item(.)(.*)$/); + + if (match && prop !== 'itemController') { + // Convert itemShouldFoo -> shouldFoo + itemHash[match[1].toLowerCase() + match[2]] = hash[prop]; + // Delete from hash as this will end up getting passed to the + // {{view}} helper method. + delete hash[prop]; + } + } + } + + if (fn) { + itemHash.template = fn; + delete options.fn; + } + + var emptyViewClass; + if (inverse && inverse !== EmberHandlebars.VM.noop) { + emptyViewClass = get(collectionPrototype, 'emptyViewClass'); + emptyViewClass = emptyViewClass.extend({ + template: inverse, + tagName: itemHash.tagName + }); + } else if (hash.emptyViewClass) { + emptyViewClass = handlebarsGet(this, hash.emptyViewClass, options); + } + if (emptyViewClass) { hash.emptyView = emptyViewClass; } + + if (hash.keyword) { + itemHash._context = this; + } else { + itemHash._context = alias('content'); + } + + var viewOptions = ViewHelper.propertiesFromHTMLOptions({ data: data, hash: itemHash }, this); + hash.itemViewClass = itemViewClass.extend(viewOptions); + + options.helperName = options.helperName || 'collection'; + + return helpers.view.call(this, collectionClass, options); + } + + __exports__["default"] = collectionHelper; + }); +define("ember-handlebars/helpers/debug", + ["ember-metal/core","ember-metal/utils","ember-metal/logger","ember-metal/property_get","ember-handlebars/ext","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __exports__) { + "use strict"; + /*jshint debug:true*/ + + /** + @module ember + @submodule ember-handlebars + */ + var Ember = __dependency1__["default"]; + // Ember.FEATURES, + var inspect = __dependency2__.inspect; + var Logger = __dependency3__["default"]; + + var get = __dependency4__.get; + var normalizePath = __dependency5__.normalizePath; + var handlebarsGet = __dependency5__.handlebarsGet; + + var a_slice = [].slice; + + /** + `log` allows you to output the value of variables in the current rendering + context. `log` also accepts primitive types such as strings or numbers. + + ```handlebars + {{log "myVariable:" myVariable }} + ``` + + @method log + @for Ember.Handlebars.helpers + @param {String} property + */ + function logHelper() { + var params = a_slice.call(arguments, 0, -1), + options = arguments[arguments.length - 1], + logger = Logger.log, + values = [], + allowPrimitives = true; + + for (var i = 0; i < params.length; i++) { + var type = options.types[i]; + + if (type === 'ID' || !allowPrimitives) { + var context = (options.contexts && options.contexts[i]) || this, + normalized = normalizePath(context, params[i], options.data); + + if (normalized.path === 'this') { + values.push(normalized.root); + } else { + values.push(handlebarsGet(normalized.root, normalized.path, options)); + } + } else { + values.push(params[i]); + } + } + + logger.apply(logger, values); + }; + + /** + Execute the `debugger` statement in the current context. + + ```handlebars + {{debugger}} + ``` + + Before invoking the `debugger` statement, there + are a few helpful variables defined in the + body of this helper that you can inspect while + debugging that describe how and where this + helper was invoked: + + - templateContext: this is most likely a controller + from which this template looks up / displays properties + - typeOfTemplateContext: a string description of + what the templateContext is + + For example, if you're wondering why a value `{{foo}}` + isn't rendering as expected within a template, you + could place a `{{debugger}}` statement, and when + the `debugger;` breakpoint is hit, you can inspect + `templateContext`, determine if it's the object you + expect, and/or evaluate expressions in the console + to perform property lookups on the `templateContext`: + + ``` + > templateContext.get('foo') // -> "" + ``` + + @method debugger + @for Ember.Handlebars.helpers + @param {String} property + */ + function debuggerHelper(options) { + + // These are helpful values you can inspect while debugging. + var templateContext = this; + var typeOfTemplateContext = inspect(templateContext); + + debugger; + } + + __exports__.logHelper = logHelper; + __exports__.debuggerHelper = debuggerHelper; + }); +define("ember-handlebars/helpers/each", + ["ember-metal/core","ember-handlebars-compiler","ember-runtime/system/string","ember-metal/property_get","ember-metal/property_set","ember-handlebars/views/metamorph_view","ember-views/views/collection_view","ember-metal/binding","ember-runtime/controllers/controller","ember-runtime/controllers/array_controller","ember-runtime/mixins/array","ember-runtime/copy","ember-metal/run_loop","ember-metal/observer","ember-metal/events","ember-handlebars/ext","ember-metal/computed","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __dependency11__, __dependency12__, __dependency13__, __dependency14__, __dependency15__, __dependency16__, __dependency17__, __exports__) { + "use strict"; + + /** + @module ember + @submodule ember-handlebars + */ + var Ember = __dependency1__["default"]; + // Ember.assert;, Ember.K + // var emberAssert = Ember.assert, + var K = Ember.K; + + var EmberHandlebars = __dependency2__["default"]; + var helpers = EmberHandlebars.helpers; + + var fmt = __dependency3__.fmt; + var get = __dependency4__.get; + var set = __dependency5__.set; + var _Metamorph = __dependency6__._Metamorph; + var _MetamorphView = __dependency6__._MetamorphView; + var CollectionView = __dependency7__["default"]; + var Binding = __dependency8__.Binding; + var ControllerMixin = __dependency9__.ControllerMixin; + var ArrayController = __dependency10__["default"]; + var EmberArray = __dependency11__["default"]; + var copy = __dependency12__["default"]; + var run = __dependency13__["default"]; + var addObserver = __dependency14__.addObserver; + var removeObserver = __dependency14__.removeObserver; + var addBeforeObserver = __dependency14__.addBeforeObserver; + var removeBeforeObserver = __dependency14__.removeBeforeObserver; + var on = __dependency15__.on; + var handlebarsGet = __dependency16__.handlebarsGet; + var computed = __dependency17__.computed; + + var handlebarsGet = __dependency16__.handlebarsGet; + + var EachView = CollectionView.extend(_Metamorph, { + + init: function() { + var itemController = get(this, 'itemController'); + var binding; + + if (itemController) { + var controller = get(this, 'controller.container').lookupFactory('controller:array').create({ + _isVirtual: true, + parentController: get(this, 'controller'), + itemController: itemController, + target: get(this, 'controller'), + _eachView: this + }); + + this.disableContentObservers(function() { + set(this, 'content', controller); + binding = new Binding('content', '_eachView.dataSource').oneWay(); + binding.connect(controller); + }); + + set(this, '_arrayController', controller); + } else { + this.disableContentObservers(function() { + binding = new Binding('content', 'dataSource').oneWay(); + binding.connect(this); + }); + } + + return this._super(); + }, + + _assertArrayLike: function(content) { + }, + + disableContentObservers: function(callback) { + removeBeforeObserver(this, 'content', null, '_contentWillChange'); + removeObserver(this, 'content', null, '_contentDidChange'); + + callback.call(this); + + addBeforeObserver(this, 'content', null, '_contentWillChange'); + addObserver(this, 'content', null, '_contentDidChange'); + }, + + itemViewClass: _MetamorphView, + emptyViewClass: _MetamorphView, + + createChildView: function(view, attrs) { + view = this._super(view, attrs); + + // At the moment, if a container view subclass wants + // to insert keywords, it is responsible for cloning + // the keywords hash. This will be fixed momentarily. + var keyword = get(this, 'keyword'); + var content = get(view, 'content'); + + if (keyword) { + var data = get(view, 'templateData'); + + data = copy(data); + data.keywords = view.cloneKeywords(); + set(view, 'templateData', data); + + // In this case, we do not bind, because the `content` of + // a #each item cannot change. + data.keywords[keyword] = content; + } + + // If {{#each}} is looping over an array of controllers, + // point each child view at their respective controller. + if (content && content.isController) { + set(view, 'controller', content); + } + + return view; + }, + + destroy: function() { + if (!this._super()) { return; } + + var arrayController = get(this, '_arrayController'); + + if (arrayController) { + arrayController.destroy(); + } + + return this; + } + }); + + // Defeatureify doesn't seem to like nested functions that need to be removed + function _addMetamorphCheck() { + EachView.reopen({ + _checkMetamorph: on('didInsertElement', function() { + }) + }); + } + + // until ember-debug is es6ed + var runInDebug = function(f){f()}; + + var GroupedEach = EmberHandlebars.GroupedEach = function(context, path, options) { + var self = this, + normalized = EmberHandlebars.normalizePath(context, path, options.data); + + this.context = context; + this.path = path; + this.options = options; + this.template = options.fn; + this.containingView = options.data.view; + this.normalizedRoot = normalized.root; + this.normalizedPath = normalized.path; + this.content = this.lookupContent(); + + this.addContentObservers(); + this.addArrayObservers(); + + this.containingView.on('willClearRender', function() { + self.destroy(); + }); + }; + + GroupedEach.prototype = { + contentWillChange: function() { + this.removeArrayObservers(); + }, + + contentDidChange: function() { + this.content = this.lookupContent(); + this.addArrayObservers(); + this.rerenderContainingView(); + }, + + contentArrayWillChange: K, + + contentArrayDidChange: function() { + this.rerenderContainingView(); + }, + + lookupContent: function() { + return handlebarsGet(this.normalizedRoot, this.normalizedPath, this.options); + }, + + addArrayObservers: function() { + if (!this.content) { return; } + + this.content.addArrayObserver(this, { + willChange: 'contentArrayWillChange', + didChange: 'contentArrayDidChange' + }); + }, + + removeArrayObservers: function() { + if (!this.content) { return; } + + this.content.removeArrayObserver(this, { + willChange: 'contentArrayWillChange', + didChange: 'contentArrayDidChange' + }); + }, + + addContentObservers: function() { + addBeforeObserver(this.normalizedRoot, this.normalizedPath, this, this.contentWillChange); + addObserver(this.normalizedRoot, this.normalizedPath, this, this.contentDidChange); + }, + + removeContentObservers: function() { + removeBeforeObserver(this.normalizedRoot, this.normalizedPath, this.contentWillChange); + removeObserver(this.normalizedRoot, this.normalizedPath, this.contentDidChange); + }, + + render: function() { + if (!this.content) { return; } + + var content = this.content, + contentLength = get(content, 'length'), + data = this.options.data, + template = this.template; + + data.insideEach = true; + for (var i = 0; i < contentLength; i++) { + template(content.objectAt(i), { data: data }); + } + }, + + rerenderContainingView: function() { + var self = this; + run.scheduleOnce('render', this, function() { + // It's possible it's been destroyed after we enqueued a re-render call. + if (!self.destroyed) { + self.containingView.rerender(); + } + }); + }, + + destroy: function() { + this.removeContentObservers(); + if (this.content) { + this.removeArrayObservers(); + } + this.destroyed = true; + } + }; + + /** + The `{{#each}}` helper loops over elements in a collection, rendering its + block once for each item. It is an extension of the base Handlebars `{{#each}}` + helper: + + ```javascript + Developers = [{name: 'Yehuda'},{name: 'Tom'}, {name: 'Paul'}]; + ``` + + ```handlebars + {{#each Developers}} + {{name}} + {{/each}} + ``` + + `{{each}}` supports an alternative syntax with element naming: + + ```handlebars + {{#each person in Developers}} + {{person.name}} + {{/each}} + ``` + + When looping over objects that do not have properties, `{{this}}` can be used + to render the object: + + ```javascript + DeveloperNames = ['Yehuda', 'Tom', 'Paul'] + ``` + + ```handlebars + {{#each DeveloperNames}} + {{this}} + {{/each}} + ``` + ### {{else}} condition + `{{#each}}` can have a matching `{{else}}`. The contents of this block will render + if the collection is empty. + + ``` + {{#each person in Developers}} + {{person.name}} + {{else}} +

    Sorry, nobody is available for this task.

    + {{/each}} + ``` + ### Specifying a View class for items + If you provide an `itemViewClass` option that references a view class + with its own `template` you can omit the block. + + The following template: + + ```handlebars + {{#view App.MyView }} + {{each view.items itemViewClass="App.AnItemView"}} + {{/view}} + ``` + + And application code + + ```javascript + App = Ember.Application.create({ + MyView: Ember.View.extend({ + items: [ + Ember.Object.create({name: 'Dave'}), + Ember.Object.create({name: 'Mary'}), + Ember.Object.create({name: 'Sara'}) + ] + }) + }); + + App.AnItemView = Ember.View.extend({ + template: Ember.Handlebars.compile("Greetings {{name}}") + }); + ``` + + Will result in the HTML structure below + + ```html +
    +
    Greetings Dave
    +
    Greetings Mary
    +
    Greetings Sara
    +
    + ``` + + If an `itemViewClass` is defined on the helper, and therefore the helper is not + being used as a block, an `emptyViewClass` can also be provided optionally. + The `emptyViewClass` will match the behavior of the `{{else}}` condition + described above. That is, the `emptyViewClass` will render if the collection + is empty. + + ### Representing each item with a Controller. + By default the controller lookup within an `{{#each}}` block will be + the controller of the template where the `{{#each}}` was used. If each + item needs to be presented by a custom controller you can provide a + `itemController` option which references a controller by lookup name. + Each item in the loop will be wrapped in an instance of this controller + and the item itself will be set to the `content` property of that controller. + + This is useful in cases where properties of model objects need transformation + or synthesis for display: + + ```javascript + App.DeveloperController = Ember.ObjectController.extend({ + isAvailableForHire: function() { + return !this.get('content.isEmployed') && this.get('content.isSeekingWork'); + }.property('isEmployed', 'isSeekingWork') + }) + ``` + + ```handlebars + {{#each person in developers itemController="developer"}} + {{person.name}} {{#if person.isAvailableForHire}}Hire me!{{/if}} + {{/each}} + ``` + + Each itemController will receive a reference to the current controller as + a `parentController` property. + + ### (Experimental) Grouped Each + + When used in conjunction with the experimental [group helper](https://github.com/emberjs/group-helper), + you can inform Handlebars to re-render an entire group of items instead of + re-rendering them one at a time (in the event that they are changed en masse + or an item is added/removed). + + ```handlebars + {{#group}} + {{#each people}} + {{firstName}} {{lastName}} + {{/each}} + {{/group}} + ``` + + This can be faster than the normal way that Handlebars re-renders items + in some cases. + + If for some reason you have a group with more than one `#each`, you can make + one of the collections be updated in normal (non-grouped) fashion by setting + the option `groupedRows=true` (counter-intuitive, I know). + + For example, + + ```handlebars + {{dealershipName}} + + {{#group}} + {{#each dealers}} + {{firstName}} {{lastName}} + {{/each}} + + {{#each car in cars groupedRows=true}} + {{car.make}} {{car.model}} {{car.color}} + {{/each}} + {{/group}} + ``` + Any change to `dealershipName` or the `dealers` collection will cause the + entire group to be re-rendered. However, changes to the `cars` collection + will be re-rendered individually (as normal). + + Note that `group` behavior is also disabled by specifying an `itemViewClass`. + + @method each + @for Ember.Handlebars.helpers + @param [name] {String} name for item (used with `in`) + @param [path] {String} path + @param [options] {Object} Handlebars key/value pairs of options + @param [options.itemViewClass] {String} a path to a view class used for each item + @param [options.itemController] {String} name of a controller to be created for each item + @param [options.groupedRows] {boolean} enable normal item-by-item rendering when inside a `#group` helper + */ + function eachHelper(path, options) { + var ctx, helperName = 'each'; + + if (arguments.length === 4) { + + var keywordName = arguments[0]; + + + options = arguments[3]; + path = arguments[2]; + + helperName += ' ' + keywordName + ' in ' + path; + + if (path === '') { path = "this"; } + + options.hash.keyword = keywordName; + + } else if (arguments.length === 1) { + options = path; + path = 'this'; + } else { + helperName += ' ' + path; + } + + options.hash.dataSourceBinding = path; + // Set up emptyView as a metamorph with no tag + //options.hash.emptyViewClass = Ember._MetamorphView; + + // can't rely on this default behavior when use strict + ctx = this || window; + + options.helperName = options.helperName || helperName; + + if (options.data.insideGroup && !options.hash.groupedRows && !options.hash.itemViewClass) { + new GroupedEach(ctx, path, options).render(); + } else { + // ES6TODO: figure out how to do this without global lookup. + return helpers.collection.call(ctx, 'Ember.Handlebars.EachView', options); + } + } + + __exports__.EachView = EachView; + __exports__.GroupedEach = GroupedEach; + __exports__.eachHelper = eachHelper; + }); +define("ember-handlebars/helpers/loc", + ["ember-runtime/system/string","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var loc = __dependency1__.loc; + + /** + @module ember + @submodule ember-handlebars + */ + + // ES6TODO: + // Pretty sure this can be expressed as + // var locHelper EmberStringUtils.loc ? + + /** + Calls [Ember.String.loc](/api/classes/Ember.String.html#method_loc) with the + provided string. + + This is a convenient way to localize text. For example: + + ```html + + ``` + + Take note that `"welcome"` is a string and not an object + reference. + + See [Ember.String.loc](/api/classes/Ember.String.html#method_loc) for how to + set up localized string references. + + @method loc + @for Ember.Handlebars.helpers + @param {String} str The string to format + @see {Ember.String#loc} + */ + function locHelper(str) { + return loc(str); + } + + __exports__["default"] = locHelper; + }); +define("ember-handlebars/helpers/partial", + ["ember-metal/core","ember-metal/is_none","ember-handlebars/ext","ember-handlebars/helpers/binding","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __exports__) { + "use strict"; + var Ember = __dependency1__["default"]; + // Ember.assert + // var emberAssert = Ember.assert; + + var isNone = __dependency2__.isNone; + var handlebarsGet = __dependency3__.handlebarsGet; + var bind = __dependency4__.bind; + + /** + @module ember + @submodule ember-handlebars + */ + + /** + The `partial` helper renders another template without + changing the template context: + + ```handlebars + {{foo}} + {{partial "nav"}} + ``` + + The above example template will render a template named + "_nav", which has the same context as the parent template + it's rendered into, so if the "_nav" template also referenced + `{{foo}}`, it would print the same thing as the `{{foo}}` + in the above example. + + If a "_nav" template isn't found, the `partial` helper will + fall back to a template named "nav". + + ## Bound template names + + The parameter supplied to `partial` can also be a path + to a property containing a template name, e.g.: + + ```handlebars + {{partial someTemplateName}} + ``` + + The above example will look up the value of `someTemplateName` + on the template context (e.g. a controller) and use that + value as the name of the template to render. If the resolved + value is falsy, nothing will be rendered. If `someTemplateName` + changes, the partial will be re-rendered using the new template + name. + + ## Setting the partial's context with `with` + + The `partial` helper can be used in conjunction with the `with` + helper to set a context that will be used by the partial: + + ```handlebars + {{#with currentUser}} + {{partial "user_info"}} + {{/with}} + ``` + + @method partial + @for Ember.Handlebars.helpers + @param {String} partialName the name of the template to render minus the leading underscore + */ + + function partialHelper(name, options) { + + var context = (options.contexts && options.contexts.length) ? options.contexts[0] : this; + + options.helperName = options.helperName || 'partial'; + + if (options.types[0] === "ID") { + // Helper was passed a property path; we need to + // create a binding that will re-render whenever + // this property changes. + options.fn = function(context, fnOptions) { + var partialName = handlebarsGet(context, name, fnOptions); + renderPartial(context, partialName, fnOptions); + }; + + return bind.call(context, name, options, true, exists); + } else { + // Render the partial right into parent template. + renderPartial(context, name, options); + } + } + + function exists(value) { + return !isNone(value); + } + + function renderPartial(context, name, options) { + var nameParts = name.split("/"); + var lastPart = nameParts[nameParts.length - 1]; + + nameParts[nameParts.length - 1] = "_" + lastPart; + + var view = options.data.view; + var underscoredName = nameParts.join("/"); + var template = view.templateForName(underscoredName); + var deprecatedTemplate = !template && view.templateForName(name); + + + template = template || deprecatedTemplate; + + template(context, { data: options.data }); + } + + __exports__["default"] = partialHelper; + }); +define("ember-handlebars/helpers/shared", + ["ember-handlebars/ext","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var handlebarsGet = __dependency1__.handlebarsGet; + + function resolvePaths(options) { + var ret = [], + contexts = options.contexts, + roots = options.roots, + data = options.data; + + for (var i=0, l=contexts.length; i + {{#with loggedInUser}} + Last Login: {{lastLogin}} + User Info: {{template "user_info"}} + {{/with}} + + ``` + + ```html + + ``` + + ```handlebars + {{#if isUser}} + {{template "user_info"}} + {{else}} + {{template "unlogged_user_info"}} + {{/if}} + ``` + + This helper looks for templates in the global `Ember.TEMPLATES` hash. If you + add ` <%= javascript_include_tag "species" %> <%= csrf_meta_tags %> + <%= csp_meta_tag %> <% if Rails.env.production? %> diff --git a/app/views/layouts/trade.html.erb b/app/views/layouts/trade.html.erb index 75ca1b49f..fa289b9bc 100644 --- a/app/views/layouts/trade.html.erb +++ b/app/views/layouts/trade.html.erb @@ -18,6 +18,7 @@ <%= javascript_include_tag "trade" %> <%= csrf_meta_tags %> + <%= csp_meta_tag %> <%= yield %> diff --git a/config/application.rb b/config/application.rb index aef27351d..65c891a7c 100644 --- a/config/application.rb +++ b/config/application.rb @@ -10,7 +10,7 @@ module SAPI class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 5.1 + config.load_defaults 5.2 # Settings in config/environments/* take precedence over those specified here. # Application configuration can go into files in config/initializers diff --git a/config/environments/production.rb b/config/environments/production.rb index 4fb295e5b..23defb097 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -51,7 +51,7 @@ # Use the lowest log level to ensure availability of diagnostic information # when problems arise. - config.log_level = :debug + config.log_level = :warn # @see https://github.com/heartcombo/devise#password-reset-tokens-and-rails-logs # Prepend all log lines with the following tags. config.log_tags = [ :request_id ] diff --git a/config/environments/staging.rb b/config/environments/staging.rb index 7840d1345..435915493 100644 --- a/config/environments/staging.rb +++ b/config/environments/staging.rb @@ -14,15 +14,13 @@ config.consider_all_requests_local = false config.action_controller.perform_caching = true - # Enable Rack::Cache to put a simple HTTP cache in front of your application - # Add `rack-cache` to your Gemfile before enabling this. - # For large-scale production use, consider using a caching reverse proxy like - # NGINX, varnish or squid. - # config.action_dispatch.rack_cache = true + # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] + # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). + # config.require_master_key = true # Disable serving static files from the `/public` folder by default since # Apache or NGINX already handles this. - config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present? + config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? # Compress JavaScripts and CSS. config.assets.js_compressor = :uglifier @@ -31,16 +29,23 @@ # Don't fallback to assets pipeline if a precompiled asset is missed. config.assets.compile = false - # Asset digests allow you to set far-future HTTP expiration dates on all assets, - # yet still be able to expire them through the digest params. - config.assets.digest = true - # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.action_controller.asset_host = 'http://assets.example.com' + # Specifies the header that your server uses for sending files. - # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache + # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX + # Store uploaded files on the local file system (see config/storage.yml for options) + config.active_storage.service = :local + + # Mount Action Cable outside main process or domain + # config.action_cable.mount_path = nil + # config.action_cable.url = 'wss://example.com/cable' + # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # config.force_ssl = true @@ -49,16 +54,15 @@ config.log_level = :warn # @see https://github.com/heartcombo/devise#password-reset-tokens-and-rails-logs # Prepend all log lines with the following tags. - # config.log_tags = [ :subdomain, :uuid ] - - # Use a different logger for distributed setups. - # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) + config.log_tags = [ :request_id ] # Use a different cache store in staging. config.cache_store = :memory_store, { size: 64.megabytes } - # Enable serving of images, stylesheets, and JavaScripts from an asset server. - # config.action_controller.asset_host = 'http://assets.example.com' + # Use a real queuing backend for Active Job (and separate queues per environment) + # config.active_job.queue_adapter = :resque + # config.active_job.queue_name_prefix = "sapi_#{Rails.env}" + config.action_mailer.perform_caching = false # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. @@ -75,6 +79,16 @@ # Use default logging formatter so that PID and timestamp are not suppressed. config.log_formatter = ::Logger::Formatter.new + # Use a different logger for distributed setups. + # require 'syslog/logger' + # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') + + if ENV["RAILS_LOG_TO_STDOUT"].present? + logger = ActiveSupport::Logger.new(STDOUT) + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + end + # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false From c5d44f46591a91c1002313872443550802ed22a4 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Thu, 25 Jan 2024 21:04:15 +0000 Subject: [PATCH 101/241] upgrade rspec gem --- Gemfile | 2 +- Gemfile.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Gemfile b/Gemfile index c1289d0c8..42d7e08e4 100644 --- a/Gemfile +++ b/Gemfile @@ -120,7 +120,7 @@ group :development do end group :test, :development do - gem "rspec-rails", '4.1.1' # TODO: should upgrade once to rails 5.2 + gem "rspec-rails", '5.1.2' # TODO: should upgrade once to rails 6.1 gem 'rspec-collection_matchers', '~> 1.2', '>= 1.2.1' gem "json_spec", '1.1.5' gem 'database_cleaner', '~> 2.0', '>= 2.0.2' diff --git a/Gemfile.lock b/Gemfile.lock index e7126532b..72ee65f18 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -450,10 +450,10 @@ GEM rspec-mocks (3.12.6) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-rails (4.1.1) - actionpack (>= 4.2) - activesupport (>= 4.2) - railties (>= 4.2) + rspec-rails (5.1.2) + actionpack (>= 5.2) + activesupport (>= 5.2) + railties (>= 5.2) rspec-core (~> 3.10) rspec-expectations (~> 3.10) rspec-mocks (~> 3.10) @@ -660,7 +660,7 @@ DEPENDENCIES responders (~> 2.0) rest-client (= 1.8.0) rspec-collection_matchers (~> 1.2, >= 1.2.1) - rspec-rails (= 4.1.1) + rspec-rails (= 5.1.2) rubocop-rails rubocop-rspec rubyzip (~> 2.3, >= 2.3.2) From 5f9d79610cf68c57e28dfae07756163e19468911 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Thu, 25 Jan 2024 21:44:15 +0000 Subject: [PATCH 102/241] upgrade pg_search and strong_migration --- Gemfile | 4 ++-- Gemfile.lock | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Gemfile b/Gemfile index 42d7e08e4..4ab0b79d7 100644 --- a/Gemfile +++ b/Gemfile @@ -25,7 +25,7 @@ gem 'dalli', '2.7.10' # TODO: latest is 3.2.6. I believe should be fine to upgra gem 'pg', '~> 1.5', '>= 1.5.4' gem 'pg_array_parser', '~> 0.0.9' gem 'nested-hstore', '~> 0.1.2' -gem 'pg_search', '2.3.0' # TODO: can upgrade to newer version after Rails 5.2 +gem 'pg_search', '~> 2.3', '>= 2.3.6' gem 'oj', '3.14.2' # optimised JSON (picked by multi_json) # TODO: to upgrade to newer version, need >=Ruby 2.7 gem 'nokogiri', '1.12.5' # TODO: 1.12.5 is the last version support 2.5. New version need Ruby 2.6+ gem 'inherited_resources', '1.9.0' # Deprecated (https://github.com/activeadmin/inherited_resources#notice) # TODO: need upgrade when upgrade to Rails 6 @@ -70,7 +70,7 @@ gem 'rails-observers', '~> 0.1.5' # A feature that removed from core in Rails 4. # See https://github.com/sstephenson/execjs#readme for more supported runtimes # gem 'therubyracer', :platforms => :ruby -gem 'strong_migrations', '0.7.9' # TODO: should upgrade when we upgrade to rails 5.2 +gem 'strong_migrations', '~> 1.7' # Use ActiveModel has_secure_password # gem 'bcrypt', '~> 3.1.7' diff --git a/Gemfile.lock b/Gemfile.lock index 72ee65f18..f84eca85c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -348,9 +348,9 @@ GEM pdfkit (0.8.7.3) pg (1.5.4) pg_array_parser (0.0.9) - pg_search (2.3.0) - activerecord (>= 4.2) - activesupport (>= 4.2) + pg_search (2.3.6) + activerecord (>= 5.2) + activesupport (>= 5.2) power_assert (2.0.3) prawn (0.13.2) pdf-reader (~> 1.2) @@ -541,8 +541,8 @@ GEM net-sftp (>= 2.1.2) net-ssh (>= 2.8.0) ssrf_filter (1.1.2) - strong_migrations (0.7.9) - activerecord (>= 5) + strong_migrations (1.7.0) + activerecord (>= 5.2) susy (2.2.14) sass (>= 3.3.0, < 3.5) sync (0.5.0) @@ -646,7 +646,7 @@ DEPENDENCIES pdfkit (~> 0.8.7.3) pg (~> 1.5, >= 1.5.4) pg_array_parser (~> 0.0.9) - pg_search (= 2.3.0) + pg_search (~> 2.3, >= 2.3.6) prawn (= 0.13.2) puma (~> 3.11) rack-cors (= 0.3.0) @@ -674,7 +674,7 @@ DEPENDENCIES slackistrano (= 0.1.9) spring spring-watcher-listen (~> 2.0.0) - strong_migrations (= 0.7.9) + strong_migrations (~> 1.7) susy (~> 2.2, >= 2.2.14) test-unit (= 3.1.5) uglifier (>= 1.3.0) From 691fc42ef4cb1da7485a2d2a43155f9c265b07e9 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Thu, 25 Jan 2024 22:30:21 +0000 Subject: [PATCH 103/241] upgrade paper trail --- Gemfile | 8 ++++---- Gemfile.lock | 30 +++++++++++++++--------------- config/initializers/paper_trail.rb | 5 ----- 3 files changed, 19 insertions(+), 24 deletions(-) diff --git a/Gemfile b/Gemfile index 4ab0b79d7..4afb5629e 100644 --- a/Gemfile +++ b/Gemfile @@ -27,8 +27,8 @@ gem 'pg_array_parser', '~> 0.0.9' gem 'nested-hstore', '~> 0.1.2' gem 'pg_search', '~> 2.3', '>= 2.3.6' gem 'oj', '3.14.2' # optimised JSON (picked by multi_json) # TODO: to upgrade to newer version, need >=Ruby 2.7 -gem 'nokogiri', '1.12.5' # TODO: 1.12.5 is the last version support 2.5. New version need Ruby 2.6+ -gem 'inherited_resources', '1.9.0' # Deprecated (https://github.com/activeadmin/inherited_resources#notice) # TODO: need upgrade when upgrade to Rails 6 +gem 'nokogiri', '1.13.10' # TODO: 1.13.10 is the last version support 2.6. New version need Ruby 2.7+ +gem 'inherited_resources', '1.13.1' # Deprecated (https://github.com/activeadmin/inherited_resources#notice) # TODO: need upgrade when upgrade to Rails 6 gem 'mobility', '~> 1.2', '>= 1.2.9' gem 'devise', '4.4.3' # TODO: version 4.4.3 work under <=Rails 5.3 and <=Ruby 2.6 gem 'cancancan', '2.3.0' # TODO, can upgrade to 3.0 after Rails 6 @@ -40,7 +40,7 @@ gem 'uuidtools', '~> 2.2' # For Ahoy. (https://github.com/ankane/ahoy/blob/v2.2. # rspec ./spec/controllers/admin/nomenclature_changes/split_controller_spec.rb:191 gem 'wicked', '1.3.4' -gem 'groupdate', '5.2.4' # TODO: can upgrade after rails 5.2 and newer ruby 2.6 +gem 'groupdate', '6.2.1' # TODO: can upgrade after rails 6.1 and newer ruby 3 gem 'rubyzip', '~> 2.3', '>= 2.3.2' gem 'responders', '~> 2.0' # https://guides.rubyonrails.org/v4.2/upgrading_ruby_on_rails.html#responders @@ -146,7 +146,7 @@ end gem 'geoip', '1.3.5' # TODO: no change logs, no idea if safe to update. Latest version is 1.6.4 @ 2018 gem 'request_store', '~> 1.5', '>= 1.5.1' -gem 'paper_trail', '10.3.1' # TODO: latest is 15.1.0. Can upgrade to newer version when we at Rails 5.2 +gem 'paper_trail', '13.0.0' # TODO: latest is 15.1.0. Can upgrade to newer version when we at Rails 6 gem 'dotenv-rails', '2.0.1' diff --git a/Gemfile.lock b/Gemfile.lock index f84eca85c..0d7d06fe0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -232,8 +232,8 @@ GEM i18n (>= 0.7) multi_json request_store (>= 1.0) - groupdate (5.2.4) - activesupport (>= 5) + groupdate (6.2.1) + activesupport (>= 5.2) handlebars-source (1.0.12) has_scope (0.8.2) actionpack (>= 5.2) @@ -249,11 +249,11 @@ GEM image_processing (1.12.2) mini_magick (>= 4.9.5, < 5) ruby-vips (>= 2.0.17, < 3) - inherited_resources (1.9.0) - actionpack (>= 4.2, < 5.3) + inherited_resources (1.13.1) + actionpack (>= 5.2, < 7.1) has_scope (~> 0.6) - railties (>= 4.2, < 5.3) - responders + railties (>= 5.2, < 7.1) + responders (>= 2, < 4) io-like (0.3.1) jmespath (1.6.2) jquery-rails (4.6.0) @@ -297,7 +297,7 @@ GEM mime-types (2.99.3) mini_magick (4.12.0) mini_mime (1.1.5) - mini_portile2 (2.6.1) + mini_portile2 (2.8.5) minitest (5.21.2) mobility (1.2.9) i18n (>= 0.6.10, < 2) @@ -326,14 +326,14 @@ GEM net-ssh (7.2.1) netrc (0.11.0) nio4r (2.7.0) - nokogiri (1.12.5) - mini_portile2 (~> 2.6.1) + nokogiri (1.13.10) + mini_portile2 (~> 2.8.0) racc (~> 1.4) numerizer (0.1.1) oj (3.14.2) orm_adapter (0.5.0) - paper_trail (10.3.1) - activerecord (>= 4.2) + paper_trail (13.0.0) + activerecord (>= 5.2) request_store (~> 1.1) parallel (1.24.0) parser (3.3.0.5) @@ -628,10 +628,10 @@ DEPENDENCIES factory_bot_rails (= 4.11.1) geoip (= 1.3.5) gon (~> 6.4) - groupdate (= 5.2.4) + groupdate (= 6.2.1) handlebars-source (= 1.0.12) httparty (~> 0.21.0) - inherited_resources (= 1.9.0) + inherited_resources (= 1.13.1) jslint_on_rails (= 1.1.1) json_spec (= 1.1.5) kaminari (~> 1.2, >= 1.2.2) @@ -640,9 +640,9 @@ DEPENDENCIES mobility (~> 1.2, >= 1.2.9) nested-hstore (~> 0.1.2) nested_form (~> 0.3.2) - nokogiri (= 1.12.5) + nokogiri (= 1.13.10) oj (= 3.14.2) - paper_trail (= 10.3.1) + paper_trail (= 13.0.0) pdfkit (~> 0.8.7.3) pg (~> 1.5, >= 1.5.4) pg_array_parser (~> 0.0.9) diff --git a/config/initializers/paper_trail.rb b/config/initializers/paper_trail.rb index 9cfcb752e..ee89a658f 100644 --- a/config/initializers/paper_trail.rb +++ b/config/initializers/paper_trail.rb @@ -1,10 +1,5 @@ # config/initializers/paper_trail.rb -PaperTrail.config.track_associations = false - -# the following line is required for PaperTrail >= 4.0.0 and < 12.0.0 with Rails -PaperTrail::Rails::Engine.eager_load! - class TaxonConceptVersion < PaperTrail::Version # Migrated to Strong Parameters # attr_accessible :taxon_concept_id, From 6ee39ec7df52a3bc155091fb8a5b809d1f9fc1db Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 26 Jan 2024 00:12:23 +0000 Subject: [PATCH 104/241] upgrade factory bot, with `FactoryBot.use_parent_strategy = false` to keep the same behaviour. --- Gemfile | 2 +- Gemfile.lock | 12 ++++++------ spec/spec_helper.rb | 4 ++++ 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Gemfile b/Gemfile index 4afb5629e..6c62454b8 100644 --- a/Gemfile +++ b/Gemfile @@ -138,7 +138,7 @@ group :test do gem 'rails-controller-testing' gem "codeclimate-test-reporter", '0.1.1', require: nil # TODO, should be removed - gem 'factory_bot_rails', '4.11.1' + gem 'factory_bot_rails', '5.2.0' gem 'simplecov', '0.22.0', :require => false # TODO: latest gem 'coveralls', '0.7.1', :require => false end diff --git a/Gemfile.lock b/Gemfile.lock index 0d7d06fe0..6eb644deb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -217,11 +217,11 @@ GEM errbase (0.2.2) erubi (1.12.0) execjs (2.9.1) - factory_bot (4.11.1) - activesupport (>= 3.0.0) - factory_bot_rails (4.11.1) - factory_bot (~> 4.11.1) - railties (>= 3.0.0) + factory_bot (5.2.0) + activesupport (>= 4.2.0) + factory_bot_rails (5.2.0) + factory_bot (~> 5.2.0) + railties (>= 4.2.0) ffi (1.16.3) geocoder (1.8.2) geoip (1.3.5) @@ -625,7 +625,7 @@ DEPENDENCIES ember-data-source (= 1.13.0) ember-rails (~> 0.21.0) ember-source (= 1.8.0) - factory_bot_rails (= 4.11.1) + factory_bot_rails (= 5.2.0) geoip (= 1.3.5) gon (~> 6.4) groupdate (= 6.2.1) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 07dfbdb75..56a0d9f0f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -55,6 +55,10 @@ config.include SapiSpec::Helpers config.before(:all) do + # https://github.com/thoughtbot/factory_bot/issues/1255 + # https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md#build-strategies-1 + FactoryBot.use_parent_strategy = false + DatabaseCleaner.clean_with(:deletion, { :cache_tables => false }) @user = create(:user) RequestStore.store[:track_who_does_it_current_user] = @user From 91e0b3ea483e879ce5a02d873e1b7a3ecb70bd39 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 26 Jan 2024 14:37:20 +0000 Subject: [PATCH 105/241] Upgrade Ahoy, and update GDPR related config. --- Gemfile | 2 +- Gemfile.lock | 8 +++----- config/initializers/ahoy.rb | 13 +++++++++++++ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/Gemfile b/Gemfile index 6c62454b8..46c456ef6 100644 --- a/Gemfile +++ b/Gemfile @@ -32,7 +32,7 @@ gem 'inherited_resources', '1.13.1' # Deprecated (https://github.com/activeadmin gem 'mobility', '~> 1.2', '>= 1.2.9' gem 'devise', '4.4.3' # TODO: version 4.4.3 work under <=Rails 5.3 and <=Ruby 2.6 gem 'cancancan', '2.3.0' # TODO, can upgrade to 3.0 after Rails 6 -gem 'ahoy_matey', '3.3.0' # TODO: latest 5.0.2. Can't upgrade to 4.0 until upgrade to Rails 5.2 +gem 'ahoy_matey', '4.2.1' # TODO: latest 5.0.2. Can't upgrade to 5.0 until upgrade to Rails 6 gem 'uuidtools', '~> 2.2' # For Ahoy. (https://github.com/ankane/ahoy/blob/v2.2.1/docs/Ahoy-2-Upgrade.md#activerecordstore) # TODO: starting from v1.4, it break our test due to redirection changes: diff --git a/Gemfile.lock b/Gemfile.lock index 6eb644deb..ab5877e3a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -56,10 +56,9 @@ GEM addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) afm (0.2.2) - ahoy_matey (3.3.0) - activesupport (>= 5) + ahoy_matey (4.2.1) + activesupport (>= 5.2) device_detector - geocoder (>= 1.4.5) safely_block (>= 0.2.1) airbrussh (1.5.1) sshkit (>= 1.6.1, != 1.7.0) @@ -223,7 +222,6 @@ GEM factory_bot (~> 5.2.0) railties (>= 4.2.0) ffi (1.16.3) - geocoder (1.8.2) geoip (1.3.5) globalid (1.1.0) activesupport (>= 5.0) @@ -592,7 +590,7 @@ DEPENDENCIES actionpack-page_caching (~> 1.2, >= 1.2.4) active_model_serializers (= 0.8.4) acts-as-taggable-on (= 8.1.0) - ahoy_matey (= 3.3.0) + ahoy_matey (= 4.2.1) annotate (= 2.5.0) appsignal (= 1.3.3) aws-sdk (~> 2) diff --git a/config/initializers/ahoy.rb b/config/initializers/ahoy.rb index 168640987..41e76e86d 100644 --- a/config/initializers/ahoy.rb +++ b/config/initializers/ahoy.rb @@ -1,11 +1,20 @@ require 'sapi/geoip' + class Ahoy::Store < Ahoy::DatabaseStore UUID_NAMESPACE = UUIDTools::UUID.parse("dcd74c26-8fc9-453a-a9c2-afc445c3258d") + def authenticate(data) + # disables automatic linking of visits and users + end + def visit_model Ahoy::Visit end + def event_model + Ahoy::Event + end + def visit @visit ||= visit_model.find_by(id: ensure_uuid(ahoy.visit_token)) if ahoy.visit_token end @@ -33,12 +42,16 @@ def ensure_uuid(id) UUIDTools::UUID.sha1_create(UUID_NAMESPACE, id).to_s end end + Ahoy.quiet = false Ahoy.api = true Ahoy.server_side_visits = :when_needed Ahoy.user_agent_parser = :device_detector Ahoy.bot_detection_version = 2 Ahoy.track_bots = Rails.env.test? +Ahoy.geocode = false # we use our own geocoder (Sapi::GeoIP) +Ahoy.mask_ips = true +Ahoy.cookies = false # TODO: when upgrade to Ahoy v5, change value to :none # https://github.com/ankane/ahoy/tree/v2.2.1#exceptions Safely.report_exception_method = ->(e) { Appsignal.add_exception(exception) if defined? Appsignal } From e84d5e55da6ad434d283b5e5291f60d81ba04e55 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 26 Jan 2024 15:07:52 +0000 Subject: [PATCH 106/241] DEPRECATION WARNING: Dangerous query method (method whose arguments are used as raw SQL) called with non-attribute argument(s) --- app/controllers/admin/tags_controller.rb | 2 +- .../checklist/pdf/history_annotations_key.rb | 2 +- app/models/checklist/pdf/index_annotations_key.rb | 4 ++-- app/models/eu_decision_type.rb | 4 ++-- app/models/trade_restriction.rb | 14 ++++++++------ 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/app/controllers/admin/tags_controller.rb b/app/controllers/admin/tags_controller.rb index ab2eb2b47..96e5ed672 100644 --- a/app/controllers/admin/tags_controller.rb +++ b/app/controllers/admin/tags_controller.rb @@ -7,7 +7,7 @@ class Admin::TagsController < Admin::SimpleCrudController def collection @tags ||= end_of_association_chain.page(params[:page]). - order('UPPER(name) ASC, model ASC'). + order(Arel.sql('UPPER(name) ASC'), :model). search(params[:query]) end diff --git a/app/models/checklist/pdf/history_annotations_key.rb b/app/models/checklist/pdf/history_annotations_key.rb index 3ad05b19a..403117225 100644 --- a/app/models/checklist/pdf/history_annotations_key.rb +++ b/app/models/checklist/pdf/history_annotations_key.rb @@ -14,7 +14,7 @@ def hash_annotations_key tex = "\\hashAnnotationsHistoryInfo" + "\n\n" cops = CitesCop.order('effective_at') cops.each do |cop| - annotations = cop.hash_annotations.order('SUBSTRING(symbol FROM 2)::INT') + annotations = cop.hash_annotations.order(Arel.sql('SUBSTRING(symbol FROM 2)::INT')) if annotations.empty? tex << "No hash annotations found.\n\n" end diff --git a/app/models/checklist/pdf/index_annotations_key.rb b/app/models/checklist/pdf/index_annotations_key.rb index db1211f7e..38a27bda3 100644 --- a/app/models/checklist/pdf/index_annotations_key.rb +++ b/app/models/checklist/pdf/index_annotations_key.rb @@ -26,7 +26,7 @@ def hash_annotations_key tex << "\\section*{\\hashAnnotations}\n" tex << "\\hashAnnotationsIndexInfo" + "\n\n" cop = CitesCop.find_by_is_current(true) - annotations = cop && cop.hash_annotations.order('SUBSTRING(symbol FROM 2)::INT') + annotations = cop && cop.hash_annotations.order(Arel.sql('SUBSTRING(symbol FROM 2)::INT')) unless annotations && !annotations.empty? tex << "No current hash annotations found.\n\n" return tex @@ -53,7 +53,7 @@ def non_hash_annotations ). where('taxon_concept_id = original_taxon_concept_id'). where('cites_listing_changes_mview.ann_symbol IS NOT NULL'). - order("cites_listing_changes_mview.ann_symbol::INT").map do |lc| + order(Arel.sql('cites_listing_changes_mview.ann_symbol::INT')).map do |lc| { :taxon_concept => lc.taxon_concept, :symbol => lc.ann_symbol, diff --git a/app/models/eu_decision_type.rb b/app/models/eu_decision_type.rb index b0391f24a..1ba1942f6 100644 --- a/app/models/eu_decision_type.rb +++ b/app/models/eu_decision_type.rb @@ -18,9 +18,9 @@ class EuDecisionType < ApplicationRecord :suspension, :srg_referral scope :opinions, -> { where('decision_type <> ?', EuDecisionType::SUSPENSION). - order('UPPER(name) ASC') } + order(Arel.sql('UPPER(name) ASC')) } scope :suspensions, -> { where(:decision_type => EuDecisionType::SUSPENSION). - order('UPPER(name) ASC') } + order(Arel.sql('UPPER(name) ASC')) } validates :name, presence: true, uniqueness: true validates :decision_type, presence: true diff --git a/app/models/trade_restriction.rb b/app/models/trade_restriction.rb index babe0c3bf..06a4cb3f3 100644 --- a/app/models/trade_restriction.rb +++ b/app/models/trade_restriction.rb @@ -114,12 +114,14 @@ def self.export_query(filters) filter_geo_entities(filters). filter_years(filters). filter_taxon_concepts(filters). - where(:public_display => true). - order(' - taxon_concepts.name_status ASC, - taxon_concepts_mview.taxonomic_position ASC, - trade_restrictions.start_date DESC, geo_entities.name_en ASC, - trade_restrictions.notes ASC') + where(public_display: true). + order( + 'taxon_concepts.name_status': :asc, + 'taxon_concepts_mview.taxonomic_position': :asc, + start_date: :desc, + 'geo_entities.name_en': :asc, + notes: :asc + ) end # Gets the display text for each CSV_COLUMNS From 3ca18d55fcd1ba75879cf1b464c1cbd9a1dd05ff Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 26 Jan 2024 15:39:28 +0000 Subject: [PATCH 107/241] fix cache not able to cache ActiveRecordRelation https://stackoverflow.com/questions/19288142/cant-dump-anonymous-class-module-error-on-caching-in-rails-3 --- app/controllers/admin/documents_controller.rb | 5 ++++- lib/modules/search_cache.rb | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/controllers/admin/documents_controller.rb b/app/controllers/admin/documents_controller.rb index 658ce0e4a..7c49a0d4a 100644 --- a/app/controllers/admin/documents_controller.rb +++ b/app/controllers/admin/documents_controller.rb @@ -88,7 +88,10 @@ def collection # TO DO: figure out if the cascading feature has been completed, and if so move the # pagination back into the search class and out of the controllers. @documents = Kaminari::PaginatableArray.new( - @search.cached_results.limit(@search.per_page).offset(@search.offset).to_a, + # @search.cached_results.limit(@search.per_page).offset(@search.offset).to_a, + # Leonardo: rollback https://github.com/unepwcmc/SAPI/commit/52f6439bdd05ce8bbf4e5121c2a8af427668b079 + # cached_results return array, not ActiveRecord Relation, cannot chain to use .limit(), etc. + @search.cached_results, limit: @search.per_page, offset: @search.offset, total_count: @search.cached_total_cnt diff --git a/lib/modules/search_cache.rb b/lib/modules/search_cache.rb index 0764501d9..6ed015edc 100644 --- a/lib/modules/search_cache.rb +++ b/lib/modules/search_cache.rb @@ -12,7 +12,7 @@ module SearchCache def cached_results Rails.cache.fetch(results_cache_key, :expires_in => 24.hours) do - results + results.to_a end end From e0432e350d615b0ef0af895f2398287d38d82dd3 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 26 Jan 2024 16:14:49 +0000 Subject: [PATCH 108/241] enable all rails 5.2 default --- config/application.rb | 5 ----- config/initializers/0_new_framework_defaults_5_2.rb | 12 ++++++------ config/initializers/locale.rb | 4 ++++ 3 files changed, 10 insertions(+), 11 deletions(-) create mode 100644 config/initializers/locale.rb diff --git a/config/application.rb b/config/application.rb index 65c891a7c..db33b9d59 100644 --- a/config/application.rb +++ b/config/application.rb @@ -34,10 +34,5 @@ class Application < Rails::Application :"trade/shipment_observer", :"trade/trade_data_download_observer", :change_observer, :document_observer, :nomenclature_change_observer, :geo_entity_observer - - config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')] - - config.i18n.available_locales = %i[en es fr] - config.i18n.default_locale = :en end end diff --git a/config/initializers/0_new_framework_defaults_5_2.rb b/config/initializers/0_new_framework_defaults_5_2.rb index c383d072b..4c3a4be4c 100644 --- a/config/initializers/0_new_framework_defaults_5_2.rb +++ b/config/initializers/0_new_framework_defaults_5_2.rb @@ -8,7 +8,7 @@ # Make Active Record use stable #cache_key alongside new #cache_version method. # This is needed for recyclable cache keys. -# Rails.application.config.active_record.cache_versioning = true +Rails.application.config.active_record.cache_versioning = true # Use AES-256-GCM authenticated encryption for encrypted cookies. # Also, embed cookie expiry in signed or encrypted cookies for increased security. @@ -17,22 +17,22 @@ # It's best enabled when your entire app is migrated and stable on 5.2. # # Existing cookies will be converted on read then written with the new scheme. -# Rails.application.config.action_dispatch.use_authenticated_cookie_encryption = true +Rails.application.config.action_dispatch.use_authenticated_cookie_encryption = true # Use AES-256-GCM authenticated encryption as default cipher for encrypting messages # instead of AES-256-CBC, when use_authenticated_message_encryption is set to true. -# Rails.application.config.active_support.use_authenticated_message_encryption = true +Rails.application.config.active_support.use_authenticated_message_encryption = true # Add default protection from forgery to ActionController::Base instead of in # ApplicationController. -# Rails.application.config.action_controller.default_protect_from_forgery = true +Rails.application.config.action_controller.default_protect_from_forgery = true # Store boolean values are in sqlite3 databases as 1 and 0 instead of 't' and # 'f' after migrating old data. # Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true # Use SHA-1 instead of MD5 to generate non-sensitive digests, such as the ETag header. -# Rails.application.config.active_support.use_sha1_digests = true +Rails.application.config.active_support.use_sha1_digests = true # Make `form_with` generate id attributes for any generated HTML tags. -# Rails.application.config.action_view.form_with_generates_ids = true +Rails.application.config.action_view.form_with_generates_ids = true diff --git a/config/initializers/locale.rb b/config/initializers/locale.rb new file mode 100644 index 000000000..cfce29824 --- /dev/null +++ b/config/initializers/locale.rb @@ -0,0 +1,4 @@ +I18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')] + +I18n.available_locales = %i[en es fr] +I18n.default_locale = :en From 3b0ab0d087887d285f4ce9b62647d4e69b5d4b55 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 26 Jan 2024 16:51:13 +0000 Subject: [PATCH 109/241] Upgrade and reconfig chartkick gem --- Gemfile | 2 +- Gemfile.lock | 4 ++-- app/assets/javascripts/admin.js | 1 + app/views/layouts/admin.html.erb | 6 +++--- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Gemfile b/Gemfile index 46c456ef6..0c2942512 100644 --- a/Gemfile +++ b/Gemfile @@ -173,7 +173,7 @@ gem 'test-unit', '3.1.5' # annoyingly, rails console won't start without it in s gem 'susy', '~> 2.2', '>= 2.2.14' # TODO: Deprecated. (https://github.com/oddbird/susy#power-tools-for-the-web-deprecated) gem 'gon', '~> 6.4' -gem "chartkick", '2.3.5' # TODO: latest 5.0.5 @ 2023. Should upgrade to v4 once we upgrade to Rails 5.2+ and Ruby 2.6+ +gem "chartkick", '4.2.1' # TODO: latest 5.0.5 @ 2023. Should upgrade to v4 once we upgrade to Rails 6 and Ruby 2.7 gem 'nested_form', '~> 0.3.2' # TODO: Deprecated. (https://github.com/ryanb/nested_form#unmaintained) gem 'bootstrap-sass', '2.3.2.2' # TODO: latest 3.4.1 @ 2019. Can't upgrade unless we sure bootstrap v3 backward compatible with boostrap v2 diff --git a/Gemfile.lock b/Gemfile.lock index ab5877e3a..943c5a727 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -143,7 +143,7 @@ GEM marcel (~> 1.0.0) mini_mime (>= 0.1.3) ssrf_filter (~> 1.0) - chartkick (2.3.5) + chartkick (4.2.1) childprocess (4.1.0) chromedriver-helper (2.1.1) archive-zip (~> 0.10) @@ -610,7 +610,7 @@ DEPENDENCIES capistrano-sidekiq (= 1.0.2) capybara (>= 2.15) carrierwave (= 2.2.5) - chartkick (= 2.3.5) + chartkick (= 4.2.1) chromedriver-helper codeclimate-test-reporter (= 0.1.1) coffee-rails (~> 4.2) diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js index db8f2da3d..e5cdde110 100644 --- a/app/assets/javascripts/admin.js +++ b/app/assets/javascripts/admin.js @@ -20,4 +20,5 @@ //= require jquery.chained //= require html.sortable //= require underscore +//= require chartkick //= require_tree ./admin diff --git a/app/views/layouts/admin.html.erb b/app/views/layouts/admin.html.erb index aed63e7a7..c3ce4f439 100644 --- a/app/views/layouts/admin.html.erb +++ b/app/views/layouts/admin.html.erb @@ -12,13 +12,13 @@ + <% if controller_name == 'api_usage' %> + <%= javascript_include_tag "https://www.gstatic.com/charts/loader.js" %> + <% end %> <%= stylesheet_link_tag "admin" %> <%= javascript_include_tag "admin" %> <%= csrf_meta_tags %> <%= csp_meta_tag %> - <% if controller_name == 'api_usage' %> - <%= javascript_include_tag "//www.google.com/jsapi", "chartkick" %> - <% end %> From ddb64289f1656effdc547efaff42b9ba763f6acc Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 26 Jan 2024 18:43:08 +0000 Subject: [PATCH 110/241] Use Sidekiq to send Devise's emails --- app/models/user.rb | 4 ++++ config/application.rb | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/app/models/user.rb b/app/models/user.rb index 7516df4f9..dbaf670f1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -120,4 +120,8 @@ def set_default_role self.role ||= 'api' end + # https://github.com/heartcombo/devise/tree/v4.4.3#active-job-integration + def send_devise_notification(notification, *args) + devise_mailer.send(notification, self, *args).deliver_later + end end diff --git a/config/application.rb b/config/application.rb index db33b9d59..1437b1d05 100644 --- a/config/application.rb +++ b/config/application.rb @@ -34,5 +34,9 @@ class Application < Rails::Application :"trade/shipment_observer", :"trade/trade_data_download_observer", :change_observer, :document_observer, :nomenclature_change_observer, :geo_entity_observer + + # Active Job + config.active_job.queue_adapter = :sidekiq + config.action_mailer.deliver_later_queue_name = 'default' end end From 9f0fdf573d7d21ab1d6ccf960fe50473abcd3ac1 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 26 Jan 2024 18:55:10 +0000 Subject: [PATCH 111/241] Rails 5.2 build-in redis store (https://github.com/redis-store/redis-rails/tree/master#a-quick-note-about-rails-52) --- Gemfile | 3 +-- Gemfile.lock | 19 ------------------- 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/Gemfile b/Gemfile index 0c2942512..12f9a38a3 100644 --- a/Gemfile +++ b/Gemfile @@ -46,10 +46,9 @@ gem 'rubyzip', '~> 2.3', '>= 2.3.2' gem 'responders', '~> 2.0' # https://guides.rubyonrails.org/v4.2/upgrading_ruby_on_rails.html#responders # TODO: need Sidekiq 6 and Rails 6, before we can migrate worker to job, due to sidekiq_options (https://github.com/sidekiq/sidekiq/issues/4281) -gem 'sidekiq', '5.2.10' # TODO, Ruby 2.7 need version 6.0.5 sidekiq +gem 'sidekiq', '5.2.10' # TODO, when upgrade to Ruby 2.7, need to upgrade Sidekiq to v6.0.5 gem 'sidekiq-status', '2.1.3' # TODO: upgrade to v3 when Sidekiq upgrade to 6 gem 'sidekiq-unique-jobs', '7.1.31' # TODO: can upgrade to latest when sidekiq upgrade to 7 -gem 'redis-rails', '5.0.2' # TODO: latest, may remove this Gem when upgrade to Rails 5.2. (https://github.com/redis-store/redis-rails/tree/master#a-quick-note-about-rails-52) gem 'whenever', '0.11.0', :require => false # TODO: latest version 1.0 @ 2019. Should migrate to sidekiq-cron. gem 'httparty', '~> 0.21.0' diff --git a/Gemfile.lock b/Gemfile.lock index 943c5a727..330f3d210 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -362,8 +362,6 @@ GEM rack-protection (3.2.0) base64 (>= 0.1.0) rack (~> 2.2, >= 2.2.4) - rack-session (1.0.2) - rack (< 3) rack-test (2.1.0) rack (>= 1.3) rails (5.2.8.1) @@ -407,22 +405,6 @@ GEM rbnacl-libsodium (1.0.16) rbnacl (>= 3.0.1) redis (4.5.1) - redis-actionpack (5.4.0) - actionpack (>= 5, < 8) - redis-rack (>= 2.1.0, < 4) - redis-store (>= 1.1.0, < 2) - redis-activesupport (5.3.0) - activesupport (>= 3, < 8) - redis-store (>= 1.3, < 2) - redis-rack (3.0.0) - rack-session (>= 0.2.0) - redis-store (>= 1.2, < 2) - redis-rails (5.0.2) - redis-actionpack (>= 5.0, < 6) - redis-activesupport (>= 5.0, < 6) - redis-store (>= 1.2, < 2) - redis-store (1.10.0) - redis (>= 4, < 6) regexp_parser (2.9.0) request_store (1.5.1) rack (>= 1.4) @@ -653,7 +635,6 @@ DEPENDENCIES rails-observers (~> 0.1.5) rbnacl (= 4.0.2) rbnacl-libsodium (= 1.0.16) - redis-rails (= 5.0.2) request_store (~> 1.5, >= 1.5.1) responders (~> 2.0) rest-client (= 1.8.0) From c89a3864cecaef350f68cb4ccc6d64d1c8ed5ba4 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 26 Jan 2024 19:14:22 +0000 Subject: [PATCH 112/241] Upgrade Sidekiq to 7, get ready to migrate worker to active job --- Gemfile | 4 ++-- Gemfile.lock | 21 ++++++++------------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/Gemfile b/Gemfile index 12f9a38a3..5e0a7ecb5 100644 --- a/Gemfile +++ b/Gemfile @@ -46,8 +46,8 @@ gem 'rubyzip', '~> 2.3', '>= 2.3.2' gem 'responders', '~> 2.0' # https://guides.rubyonrails.org/v4.2/upgrading_ruby_on_rails.html#responders # TODO: need Sidekiq 6 and Rails 6, before we can migrate worker to job, due to sidekiq_options (https://github.com/sidekiq/sidekiq/issues/4281) -gem 'sidekiq', '5.2.10' # TODO, when upgrade to Ruby 2.7, need to upgrade Sidekiq to v6.0.5 -gem 'sidekiq-status', '2.1.3' # TODO: upgrade to v3 when Sidekiq upgrade to 6 +gem 'sidekiq', '< 7' # TODO, latest is 7 +gem 'sidekiq-status', '~> 3.0', '>= 3.0.3' gem 'sidekiq-unique-jobs', '7.1.31' # TODO: can upgrade to latest when sidekiq upgrade to 7 gem 'whenever', '0.11.0', :require => false # TODO: latest version 1.0 @ 2019. Should migrate to sidekiq-cron. diff --git a/Gemfile.lock b/Gemfile.lock index 330f3d210..0ab46c308 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -88,7 +88,6 @@ GEM barber (0.12.2) ember-source (>= 1.0, < 3.1) execjs (>= 1.2, < 3) - base64 (0.2.0) bcrypt (3.1.20) bcrypt_pbkdf (1.1.0) bindex (0.8.1) @@ -359,9 +358,6 @@ GEM racc (1.7.3) rack (2.2.8) rack-cors (0.3.0) - rack-protection (3.2.0) - base64 (>= 0.1.0) - rack (~> 2.2, >= 2.2.4) rack-test (2.1.0) rack (>= 1.3) rails (5.2.8.1) @@ -404,7 +400,7 @@ GEM ffi rbnacl-libsodium (1.0.16) rbnacl (>= 3.0.1) - redis (4.5.1) + redis (4.8.1) regexp_parser (2.9.0) request_store (1.5.1) rack (>= 1.4) @@ -479,14 +475,13 @@ GEM childprocess (>= 0.5, < 5.0) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2) - sidekiq (5.2.10) - connection_pool (~> 2.2, >= 2.2.2) + sidekiq (6.5.12) + connection_pool (>= 2.2.5, < 3) rack (~> 2.0) - rack-protection (>= 1.5.0) - redis (~> 4.5, < 4.6.0) - sidekiq-status (2.1.3) + redis (>= 4.5.0, < 5) + sidekiq-status (3.0.3) chronic_duration - sidekiq (>= 5.0) + sidekiq (>= 6.0, < 8) sidekiq-unique-jobs (7.1.31) brpoplpush-redis_script (> 0.1.1, <= 2.0.0) concurrent-ruby (~> 1.0, >= 1.0.5) @@ -645,8 +640,8 @@ DEPENDENCIES rubyzip (~> 2.3, >= 2.3.2) sass-rails (~> 5.0) selenium-webdriver - sidekiq (= 5.2.10) - sidekiq-status (= 2.1.3) + sidekiq (< 7) + sidekiq-status (~> 3.0, >= 3.0.3) sidekiq-unique-jobs (= 7.1.31) simplecov (= 0.22.0) sitemap_generator (~> 6.3) From 67e2ba99619f111050637dc19e870c3ebf7de4fa Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Mon, 29 Jan 2024 08:36:38 +0000 Subject: [PATCH 113/241] https://stackoverflow.com/a/71326657/556780 --- .../api/trade_downloads_cache_cleanup_controller.rb | 2 +- app/models/change_observer.rb | 4 ++-- app/models/cites_suspension_observer.rb | 2 +- app/models/document_search.rb | 2 +- app/models/eu_decision_observer.rb | 2 +- app/models/eu_suspension_regulation_observer.rb | 4 ++-- app/models/quota_observer.rb | 2 +- app/models/taxon_concept_observer.rb | 4 ++-- app/models/trade/annual_report_upload.rb | 2 +- app/models/trade/batch_update.rb | 2 +- app/models/trade/shipment_observer.rb | 4 ++-- app/models/trade/trade_data_download_observer.rb | 2 +- 12 files changed, 16 insertions(+), 16 deletions(-) diff --git a/app/controllers/api/trade_downloads_cache_cleanup_controller.rb b/app/controllers/api/trade_downloads_cache_cleanup_controller.rb index 5c3a226ad..12dfa355e 100644 --- a/app/controllers/api/trade_downloads_cache_cleanup_controller.rb +++ b/app/controllers/api/trade_downloads_cache_cleanup_controller.rb @@ -4,7 +4,7 @@ class Api::TradeDownloadsCacheCleanupController < ApplicationController def index message = '' begin - DownloadsCacheCleanupWorker.perform_async(:shipments) + DownloadsCacheCleanupWorker.perform_async('shipments') rescue message = 'Something went wrong' end diff --git a/app/models/change_observer.rb b/app/models/change_observer.rb index 5ed249762..63adb43fb 100644 --- a/app/models/change_observer.rb +++ b/app/models/change_observer.rb @@ -66,7 +66,7 @@ def before_destroy(model) def clear_cache(model) return unless model.respond_to?(:taxon_concept) - DownloadsCacheCleanupWorker.perform_async(model.class.to_s.tableize.to_sym) + DownloadsCacheCleanupWorker.perform_async(model.class.to_s.tableize) end def clear_show_tc_serializer_cache @@ -85,6 +85,6 @@ def bump_dependents_timestamp(taxon_concept, updated_by_id) dependents_updated_at: Time.now, dependents_updated_by_id: updated_by_id ) - DownloadsCacheCleanupWorker.perform_async(:taxon_concepts) + DownloadsCacheCleanupWorker.perform_async('taxon_concepts') end end diff --git a/app/models/cites_suspension_observer.rb b/app/models/cites_suspension_observer.rb index 7cf68d41d..d03815dd9 100644 --- a/app/models/cites_suspension_observer.rb +++ b/app/models/cites_suspension_observer.rb @@ -1,7 +1,7 @@ class CitesSuspensionObserver < TradeRestrictionObserver def after_destroy(cites_suspension) - DownloadsCacheCleanupWorker.perform_async(:cites_suspensions) + DownloadsCacheCleanupWorker.perform_async('cites_suspensions') end end diff --git a/app/models/document_search.rb b/app/models/document_search.rb index 3c3a08e16..383419242 100644 --- a/app/models/document_search.rb +++ b/app/models/document_search.rb @@ -252,7 +252,7 @@ def self.refresh_citations_and_documents def self.clear_cache RefreshDocumentsWorker.perform_async - DownloadsCacheCleanupWorker.perform_async(:documents) + DownloadsCacheCleanupWorker.perform_async('documents') end end diff --git a/app/models/eu_decision_observer.rb b/app/models/eu_decision_observer.rb index d06114982..66c08187c 100644 --- a/app/models/eu_decision_observer.rb +++ b/app/models/eu_decision_observer.rb @@ -1,7 +1,7 @@ class EuDecisionObserver < ActiveRecord::Observer def after_destroy(eu_decision) - DownloadsCacheCleanupWorker.perform_async(:eu_decisions) + DownloadsCacheCleanupWorker.perform_async('eu_decisions') end end diff --git a/app/models/eu_suspension_regulation_observer.rb b/app/models/eu_suspension_regulation_observer.rb index 68eecab2b..4f45aa1ef 100644 --- a/app/models/eu_suspension_regulation_observer.rb +++ b/app/models/eu_suspension_regulation_observer.rb @@ -6,12 +6,12 @@ def after_create(eu_suspension_regulation) eu_suspension_regulation.eu_suspensions_event_id, eu_suspension_regulation.id ) - DownloadsCacheCleanupWorker.perform_async(:eu_decisions) + DownloadsCacheCleanupWorker.perform_async('eu_decisions') end end def after_update(eu_suspension_regulation) eu_suspension_regulation.touch_suspensions_and_taxa - DownloadsCacheCleanupWorker.perform_async(:eu_decisions) + DownloadsCacheCleanupWorker.perform_async('eu_decisions') end end diff --git a/app/models/quota_observer.rb b/app/models/quota_observer.rb index 8ff5efbed..00110632f 100644 --- a/app/models/quota_observer.rb +++ b/app/models/quota_observer.rb @@ -1,7 +1,7 @@ class QuotaObserver < TradeRestrictionObserver def after_destroy(quota) - DownloadsCacheCleanupWorker.perform_async(:quotas) + DownloadsCacheCleanupWorker.perform_async('quotas') end end diff --git a/app/models/taxon_concept_observer.rb b/app/models/taxon_concept_observer.rb index 6dfbcf2f2..1f0e8e5b3 100644 --- a/app/models/taxon_concept_observer.rb +++ b/app/models/taxon_concept_observer.rb @@ -40,7 +40,7 @@ def after_destroy(taxon_concept) Species::Search.increment_cache_iterator Species::TaxonConceptPrefixMatcher.increment_cache_iterator Checklist::Checklist.increment_cache_iterator - DownloadsCacheCleanupWorker.perform_async(:taxon_concepts) + DownloadsCacheCleanupWorker.perform_async('taxon_concepts') end def after_update(taxon_concept) @@ -103,7 +103,7 @@ def after_save(taxon_concept) if taxon_concept.name_status == 'H' taxon_concept.rebuild_relationships(taxon_concept.hybrid_parents_ids) end - DownloadsCacheCleanupWorker.perform_async(:taxon_concepts) + DownloadsCacheCleanupWorker.perform_async('taxon_concepts') end end diff --git a/app/models/trade/annual_report_upload.rb b/app/models/trade/annual_report_upload.rb index 20ab731d7..77b3d442d 100644 --- a/app/models/trade/annual_report_upload.rb +++ b/app/models/trade/annual_report_upload.rb @@ -94,7 +94,7 @@ def submit(submitter) FileUtils.remove_dir(Rails.root.join('public', store_dir), :force => true) # clear downloads cache - DownloadsCacheCleanupWorker.perform_async(:shipments) + DownloadsCacheCleanupWorker.perform_async('shipments') # flag as submitted update_attributes({ # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. diff --git a/app/models/trade/batch_update.rb b/app/models/trade/batch_update.rb index 81b7b2592..39ce3e216 100644 --- a/app/models/trade/batch_update.rb +++ b/app/models/trade/batch_update.rb @@ -21,7 +21,7 @@ def execute(update_params) Trade::Shipment.transaction do affected_shipments = @shipments.count @shipments.update_all(update_params.to_h) - DownloadsCacheCleanupWorker.perform_async(:shipments) + DownloadsCacheCleanupWorker.perform_async('shipments') end PermitCleanupWorker.perform_async(disconnected_permits_ids) affected_shipments diff --git a/app/models/trade/shipment_observer.rb b/app/models/trade/shipment_observer.rb index 3e0c300af..546d92f25 100644 --- a/app/models/trade/shipment_observer.rb +++ b/app/models/trade/shipment_observer.rb @@ -19,13 +19,13 @@ def before_destroy(shipment) end def after_save(shipment) - DownloadsCacheCleanupWorker.perform_async(:shipments) + DownloadsCacheCleanupWorker.perform_async('shipments') disconnected_permits_ids = @old_permits_ids - shipment.permits_ids PermitCleanupWorker.perform_async(disconnected_permits_ids) end def after_destroy(shipment) - DownloadsCacheCleanupWorker.perform_async(:shipments) + DownloadsCacheCleanupWorker.perform_async('shipments') disconnected_permits_ids = @old_permits_ids PermitCleanupWorker.perform_async(disconnected_permits_ids) end diff --git a/app/models/trade/trade_data_download_observer.rb b/app/models/trade/trade_data_download_observer.rb index 1abdab0a0..7b309bb67 100644 --- a/app/models/trade/trade_data_download_observer.rb +++ b/app/models/trade/trade_data_download_observer.rb @@ -1,7 +1,7 @@ class Trade::TradeDataDownloadObserver < ActiveRecord::Observer def after_save(download_log) - DownloadsCacheCleanupWorker.perform_async(:trade_download_stats) + DownloadsCacheCleanupWorker.perform_async('trade_download_stats') end end From ad94baa190b1e6493bcbb37240aac0afdfbd12d1 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Mon, 29 Jan 2024 13:07:27 +0000 Subject: [PATCH 114/241] DEPRECATION WARNING: Dangerous query method (method whose arguments are used as raw SQL) called with non-attribute argument(s) --- Capfile.old | 30 ------------------- .../admin/eu_decision_types_controller.rb | 2 +- .../admin/srg_histories_controller.rb | 2 +- 3 files changed, 2 insertions(+), 32 deletions(-) delete mode 100644 Capfile.old diff --git a/Capfile.old b/Capfile.old deleted file mode 100644 index 5dd20ef0d..000000000 --- a/Capfile.old +++ /dev/null @@ -1,30 +0,0 @@ -# Load DSL and set up stages -require 'capistrano/setup' - -# Include default deployment tasks -require 'capistrano/deploy' - -# Include tasks from other gems included in your Gemfile -# -# For documentation on these, see for example: -# -# https://github.com/capistrano/rvm -# https://github.com/capistrano/rbenv -# https://github.com/capistrano/chruby -# https://github.com/capistrano/bundler -# https://github.com/capistrano/rails -# https://github.com/capistrano/passenger -# -require 'capistrano/rvm' -# require 'capistrano/rbenv' -# require 'capistrano/chruby' -require 'capistrano/bundler' -require 'capistrano/rails/assets' -require 'capistrano/rails/migrations' -#require 'capistrano/slack -require 'capistrano/sidekiq' -# require 'capistrano/passenger' -require 'whenever/capistrano' -# Load custom tasks from `lib/capistrano/tasks` if you have any defined -Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r } - diff --git a/app/controllers/admin/eu_decision_types_controller.rb b/app/controllers/admin/eu_decision_types_controller.rb index 49bd5574a..d0459697d 100644 --- a/app/controllers/admin/eu_decision_types_controller.rb +++ b/app/controllers/admin/eu_decision_types_controller.rb @@ -12,7 +12,7 @@ def index def collection @eu_decision_types ||= end_of_association_chain.page(params[:page]). - order('UPPER(name) ASC') + order(Arel.sql('UPPER(name) ASC')) end private diff --git a/app/controllers/admin/srg_histories_controller.rb b/app/controllers/admin/srg_histories_controller.rb index 7298860a6..745f9e153 100644 --- a/app/controllers/admin/srg_histories_controller.rb +++ b/app/controllers/admin/srg_histories_controller.rb @@ -3,7 +3,7 @@ class Admin::SrgHistoriesController < Admin::StandardAuthorizationController def collection @srg_histories ||= end_of_association_chain.page(params[:page]). - order('UPPER(name) ASC') + order(Arel.sql('UPPER(name) ASC')) end private From 1498fab679b6e2b9b05b0fc458b6101468a38375 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Mon, 29 Jan 2024 15:39:14 +0000 Subject: [PATCH 115/241] No ES6 for assets precompile --- app/assets/javascripts/cites_trade/application.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/cites_trade/application.js b/app/assets/javascripts/cites_trade/application.js index a727fe0e2..3a5365069 100644 --- a/app/assets/javascripts/cites_trade/application.js +++ b/app/assets/javascripts/cites_trade/application.js @@ -484,7 +484,7 @@ $(document).ready(function(){ function setEuDisclaimerVisibility () { ['imp', 'exp'].forEach(function (type) { - const disclaimerEl = $('#eu_disclaimer_' + type) + var disclaimerEl = $('#eu_disclaimer_' + type) if (disclaimerEl.length) { hasEuDisclaimer(type) ? disclaimerEl.show() : disclaimerEl.hide() @@ -493,7 +493,7 @@ $(document).ready(function(){ } function hasEuDisclaimer (type) { - const selections = $('#'+ type + 'cty').val() + var selections = $('#'+ type + 'cty').val() if (!selections) { return false From 7e25647d29b033e342a10d6104ff23680f126385 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Mon, 29 Jan 2024 21:16:27 +0000 Subject: [PATCH 116/241] New docker container for cap deploy --- Capfile | 4 ++-- Dockerfile | 8 ++++---- Dockerfile.cap-deploy | 40 ++++++++++++++++++++++++++++++++++++++++ Gemfile | 2 +- Gemfile.lock | 7 ++++--- config/deploy.rb | 4 ++++ config/deploy/staging.rb | 3 --- config/schedule.rb | 2 +- docker-compose.yml | 21 +++++++++++++++++++++ 9 files changed, 77 insertions(+), 14 deletions(-) create mode 100644 Dockerfile.cap-deploy diff --git a/Capfile b/Capfile index fcc99a2ce..fea6d1f88 100644 --- a/Capfile +++ b/Capfile @@ -37,7 +37,7 @@ require 'capistrano/maintenance' require 'whenever/capistrano' require 'capistrano/local_precompile' require 'capistrano/sidekiq' - - +install_plugin Capistrano::Sidekiq +install_plugin Capistrano::Sidekiq::Systemd Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r } diff --git a/Dockerfile b/Dockerfile index 00fe5b167..c0bfd94ce 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,12 +14,12 @@ RUN apt-get update && apt-get install -y --force-yes \ RUN mkdir /SAPI WORKDIR /SAPI -COPY Gemfile /SAPI/Gemfile -COPY Gemfile.lock /SAPI/Gemfile.lock +# COPY Gemfile /SAPI/Gemfile +# COPY Gemfile.lock /SAPI/Gemfile.lock RUN gem install bundler -v 1.17.3 -RUN bundle install +# RUN bundle install -COPY . /SAPI +# COPY . /SAPI EXPOSE 3000 CMD ["rails", "server", "-b", "0.0.0.0"] diff --git a/Dockerfile.cap-deploy b/Dockerfile.cap-deploy new file mode 100644 index 000000000..7e5125693 --- /dev/null +++ b/Dockerfile.cap-deploy @@ -0,0 +1,40 @@ +# Dockerfile +# FROM --platform=linux/amd64 debian +FROM ruby:2.6.10 + +ENV DEBIAN_FRONTEND=noninteractive +# Rails and SAPI has some additional dependencies, e.g. rake requires a JS +# runtime, so attempt to get these from apt, where possible +RUN apt-get update && apt-get install -y --force-yes \ + # For RVM + gnupg procps curl libssl-dev \ + # For assets local_precompile (cap deploy) + rsync nodejs \ + # gems + libpq-dev postgresql-client + +RUN mkdir /SAPI +WORKDIR /SAPI + +# https://stackoverflow.com/questions/43612927/how-to-correctly-install-rvm-in-docker +RUN gpg --keyserver keyserver.ubuntu.com --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB +RUN curl -sSL https://get.rvm.io | bash -s +RUN /bin/bash -l -c ". /etc/profile.d/rvm.sh && rvm install 2.6.10" +# RVM installed in multi-user mode. However cap assume rvm is installed in single user mode. +# Create a soft link to fake it. +RUN mkdir -p ~/.rvm/bin && ln -s /usr/local/rvm/bin/rvm ~/.rvm/bin/rvm + +# Host SSH key/config +RUN mkdir -p ~/.ssh && ln -s /run/secrets/host_ssh_key ~/.ssh/id_ed25519 && ln -s /run/secrets/host_ssh_config ~/.ssh/config + +ENTRYPOINT ["/bin/bash", "-l"] + +########################################## +## Run the following in container +########################################## +# /bin/bash +# source /etc/profile.d/rvm.sh +# bundle +# eval "$(ssh-agent -s)" +# ssh-add +# cap staging deploy diff --git a/Gemfile b/Gemfile index 5e0a7ecb5..77fa9d791 100644 --- a/Gemfile +++ b/Gemfile @@ -103,7 +103,7 @@ group :development do gem 'capistrano-maintenance', '1.0.0', require: false gem 'capistrano-passenger', '0.2.0', require: false gem 'capistrano-local-precompile', '1.2.0', require: false - gem 'capistrano-sidekiq', '1.0.2' + gem 'capistrano-sidekiq', '~> 2.3', '>= 2.3.1' gem 'slackistrano', '0.1.9', require: false gem 'brightbox', '2.3.9' gem 'rack-cors', '0.3.0' ,:require => 'rack/cors' # TODO: remove when upgrade Rails. diff --git a/Gemfile.lock b/Gemfile.lock index 0ab46c308..4a91b1647 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -122,9 +122,10 @@ GEM capistrano-rvm (0.1.2) capistrano (~> 3.0) sshkit (~> 1.2) - capistrano-sidekiq (1.0.2) + capistrano-sidekiq (2.3.1) capistrano (>= 3.9.0) - sidekiq (>= 3.4) + capistrano-bundler + sidekiq (>= 6.0) capybara (3.36.0) addressable matrix @@ -584,7 +585,7 @@ DEPENDENCIES capistrano-passenger (= 0.2.0) capistrano-rails (= 1.4.0) capistrano-rvm (= 0.1.2) - capistrano-sidekiq (= 1.0.2) + capistrano-sidekiq (~> 2.3, >= 2.3.1) capybara (>= 2.15) carrierwave (= 2.2.5) chartkick (= 4.2.1) diff --git a/config/deploy.rb b/config/deploy.rb index 46fd4af41..f80533a34 100644 --- a/config/deploy.rb +++ b/config/deploy.rb @@ -22,6 +22,10 @@ set :rvm_type, :user set :rvm_ruby_version, '2.6.10' +# Sidekiq config +set :sidekiq_service_unit_user, :system +set :sidekiq_service_unit_name, 'sidekiq_sapi' + # Default value for :log_level is :debug # set :log_level, :debug diff --git a/config/deploy/staging.rb b/config/deploy/staging.rb index e8d88fef6..2039a027a 100644 --- a/config/deploy/staging.rb +++ b/config/deploy/staging.rb @@ -13,9 +13,6 @@ set :app_port, "80" -set :whenever_environment, proc { fetch :stage } -require 'whenever/capistrano' - # server-based syntax # ====================== # Defines a single server with a list of roles and multiple properties. diff --git a/config/schedule.rb b/config/schedule.rb index fcb94158d..87d1421a4 100644 --- a/config/schedule.rb +++ b/config/schedule.rb @@ -1,6 +1,6 @@ set :output, 'log/cron.log' -db_migrate_rebuild_period = @environment == 'production' ? :saturday : :day +db_migrate_rebuild_period = Rails.env.production? ? :saturday : :day every db_migrate_rebuild_period, :at => '1:42am' do rake "db:migrate:rebuild" diff --git a/docker-compose.yml b/docker-compose.yml index 1e8279009..b32d9a311 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,6 +24,21 @@ services: POSTGRES_HOST_AUTH_METHOD: "trust" POSTGRES_DB: "sapi_development" + deploy: + container_name: sapi-cap-deploy + build: + context: ./ + dockerfile: Dockerfile.cap-deploy + volumes: + - '.:/SAPI' + networks: + - sapi + stdin_open: true + tty: true + secrets: + - host_ssh_key + - host_ssh_config + rails: container_name: sapi-rails build: @@ -91,3 +106,9 @@ volumes: pgdata: bundler_gems: redis_data: + +secrets: + host_ssh_key: + file: ~/.ssh/id_ed25519 + host_ssh_config: + file: ~/.ssh/config From 9e88e34a101623fc563d32e4743fc96d58c16fee Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Tue, 30 Jan 2024 09:14:25 +0000 Subject: [PATCH 117/241] Ruby 2.7 --- .ruby-version | 2 +- .travis.yml | 2 +- Dockerfile | 2 +- Dockerfile.cap-deploy | 4 ++-- Gemfile | 2 +- Gemfile.lock | 2 +- config/deploy.rb | 2 +- config/deploy/staging.rb | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.ruby-version b/.ruby-version index a04abec91..6a81b4c83 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.6.10 +2.7.8 diff --git a/.travis.yml b/.travis.yml index eef2cd5f1..6ef32e0d6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ cache: bundler bundler_args: --without development production staging sudo: false rvm: - - 2.6.10 + - 2.7.8 addons: postgresql: 9.4 code_climate: diff --git a/Dockerfile b/Dockerfile index c0bfd94ce..633fa1e8a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Dockerfile -FROM ruby:2.6.10 +FROM ruby:2.7.8 # Rails and SAPI has some additional dependencies, e.g. rake requires a JS # runtime, so attempt to get these from apt, where possible diff --git a/Dockerfile.cap-deploy b/Dockerfile.cap-deploy index 7e5125693..c1559879d 100644 --- a/Dockerfile.cap-deploy +++ b/Dockerfile.cap-deploy @@ -1,6 +1,6 @@ # Dockerfile # FROM --platform=linux/amd64 debian -FROM ruby:2.6.10 +FROM ruby:2.7.8 ENV DEBIAN_FRONTEND=noninteractive # Rails and SAPI has some additional dependencies, e.g. rake requires a JS @@ -19,7 +19,7 @@ WORKDIR /SAPI # https://stackoverflow.com/questions/43612927/how-to-correctly-install-rvm-in-docker RUN gpg --keyserver keyserver.ubuntu.com --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB RUN curl -sSL https://get.rvm.io | bash -s -RUN /bin/bash -l -c ". /etc/profile.d/rvm.sh && rvm install 2.6.10" +RUN /bin/bash -l -c ". /etc/profile.d/rvm.sh && rvm install 2.7.8" # RVM installed in multi-user mode. However cap assume rvm is installed in single user mode. # Create a soft link to fake it. RUN mkdir -p ~/.rvm/bin && ln -s /usr/local/rvm/bin/rvm ~/.rvm/bin/rvm diff --git a/Gemfile b/Gemfile index 77fa9d791..80f45c507 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } -ruby '2.6.10' +ruby '2.7.8' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' gem 'rails', '5.2.8.1' diff --git a/Gemfile.lock b/Gemfile.lock index 4a91b1647..12f9bfe12 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -660,7 +660,7 @@ DEPENDENCIES wkhtmltopdf-binary (~> 0.12.6.6) RUBY VERSION - ruby 2.6.10p229 + ruby 2.7.8p225 BUNDLED WITH 1.17.3 diff --git a/config/deploy.rb b/config/deploy.rb index f80533a34..fd9e5ebaf 100644 --- a/config/deploy.rb +++ b/config/deploy.rb @@ -20,7 +20,7 @@ # set :format, :pretty set :rvm_type, :user -set :rvm_ruby_version, '2.6.10' +set :rvm_ruby_version, '2.7.8' # Sidekiq config set :sidekiq_service_unit_user, :system diff --git a/config/deploy/staging.rb b/config/deploy/staging.rb index 2039a027a..3578ae487 100644 --- a/config/deploy/staging.rb +++ b/config/deploy/staging.rb @@ -1,5 +1,5 @@ set :stage, :staging -set :branch, :develop +set :branch, ENV['CAP_BRANCH'] || 'develop' server "sapi-staging.linode.unep-wcmc.org", user: "wcmc", roles: %w{app web db} From 2c302c61088755023d36ff0f226295a9b7b2dc33 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Tue, 30 Jan 2024 09:28:05 +0000 Subject: [PATCH 118/241] upgrade gems after ruby 2.7 --- Gemfile | 4 ++-- Gemfile.lock | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Gemfile b/Gemfile index 80f45c507..42a06a62b 100644 --- a/Gemfile +++ b/Gemfile @@ -26,8 +26,8 @@ gem 'pg', '~> 1.5', '>= 1.5.4' gem 'pg_array_parser', '~> 0.0.9' gem 'nested-hstore', '~> 0.1.2' gem 'pg_search', '~> 2.3', '>= 2.3.6' -gem 'oj', '3.14.2' # optimised JSON (picked by multi_json) # TODO: to upgrade to newer version, need >=Ruby 2.7 -gem 'nokogiri', '1.13.10' # TODO: 1.13.10 is the last version support 2.6. New version need Ruby 2.7+ +gem 'oj', '~> 3.16', '>= 3.16.3' # optimised JSON (picked by multi_json) +gem 'nokogiri', '~> 1.15', '>= 1.15.5' # TODO: New version need Ruby 3 gem 'inherited_resources', '1.13.1' # Deprecated (https://github.com/activeadmin/inherited_resources#notice) # TODO: need upgrade when upgrade to Rails 6 gem 'mobility', '~> 1.2', '>= 1.2.9' gem 'devise', '4.4.3' # TODO: version 4.4.3 work under <=Rails 5.3 and <=Ruby 2.6 diff --git a/Gemfile.lock b/Gemfile.lock index 12f9bfe12..5281d092c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -90,6 +90,7 @@ GEM execjs (>= 1.2, < 3) bcrypt (3.1.20) bcrypt_pbkdf (1.1.0) + bigdecimal (3.1.6) bindex (0.8.1) bootsnap (1.17.1) msgpack (~> 1.2) @@ -324,11 +325,12 @@ GEM net-ssh (7.2.1) netrc (0.11.0) nio4r (2.7.0) - nokogiri (1.13.10) - mini_portile2 (~> 2.8.0) + nokogiri (1.15.5) + mini_portile2 (~> 2.8.2) racc (~> 1.4) numerizer (0.1.1) - oj (3.14.2) + oj (3.16.3) + bigdecimal (>= 3.0) orm_adapter (0.5.0) paper_trail (13.0.0) activerecord (>= 5.2) @@ -616,8 +618,8 @@ DEPENDENCIES mobility (~> 1.2, >= 1.2.9) nested-hstore (~> 0.1.2) nested_form (~> 0.3.2) - nokogiri (= 1.13.10) - oj (= 3.14.2) + nokogiri (~> 1.15, >= 1.15.5) + oj (~> 3.16, >= 3.16.3) paper_trail (= 13.0.0) pdfkit (~> 0.8.7.3) pg (~> 1.5, >= 1.5.4) From a4cb0dc483badf16f74e5fbd23560ba6b8ce3698 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Tue, 30 Jan 2024 10:59:33 +0000 Subject: [PATCH 119/241] goodbye whenever, welcome sidekiq-cron --- Gemfile | 2 +- Gemfile.lock | 15 +++++-- app/jobs/downloads_cache_update_job.rb | 7 ++++ .../elibrary_refresh_document_search_job.rb | 17 ++++++++ app/jobs/rebuild_job.rb | 12 ++++++ app/jobs/refresh_sitemap_job.rb | 8 ++++ app/jobs/rst_processes_import_job.rb | 7 ++++ app/jobs/sunday_cleanup_job.rb | 40 +++++++++++++++++++ config/routes.rb | 4 +- config/schedule.rb | 29 -------------- config/schedule.yml | 24 +++++++++++ spec/jobs/downloads_cache_update_job_spec.rb | 5 +++ ...ibrary_refresh_document_search_job_spec.rb | 5 +++ spec/jobs/rebuild_job_spec.rb | 5 +++ spec/jobs/refresh_sitemap_job_spec.rb | 5 +++ spec/jobs/rst_processes_import_job_spec.rb | 5 +++ spec/jobs/sunday_cleanup_job_spec.rb | 5 +++ 17 files changed, 160 insertions(+), 35 deletions(-) create mode 100644 app/jobs/downloads_cache_update_job.rb create mode 100644 app/jobs/elibrary_refresh_document_search_job.rb create mode 100644 app/jobs/rebuild_job.rb create mode 100644 app/jobs/refresh_sitemap_job.rb create mode 100644 app/jobs/rst_processes_import_job.rb create mode 100644 app/jobs/sunday_cleanup_job.rb delete mode 100644 config/schedule.rb create mode 100644 config/schedule.yml create mode 100644 spec/jobs/downloads_cache_update_job_spec.rb create mode 100644 spec/jobs/elibrary_refresh_document_search_job_spec.rb create mode 100644 spec/jobs/rebuild_job_spec.rb create mode 100644 spec/jobs/refresh_sitemap_job_spec.rb create mode 100644 spec/jobs/rst_processes_import_job_spec.rb create mode 100644 spec/jobs/sunday_cleanup_job_spec.rb diff --git a/Gemfile b/Gemfile index 42a06a62b..275bca69f 100644 --- a/Gemfile +++ b/Gemfile @@ -49,8 +49,8 @@ gem 'responders', '~> 2.0' # https://guides.rubyonrails.org/v4.2/upgrading_ruby_ gem 'sidekiq', '< 7' # TODO, latest is 7 gem 'sidekiq-status', '~> 3.0', '>= 3.0.3' gem 'sidekiq-unique-jobs', '7.1.31' # TODO: can upgrade to latest when sidekiq upgrade to 7 +gem 'sidekiq-cron', '~> 1.12' -gem 'whenever', '0.11.0', :require => false # TODO: latest version 1.0 @ 2019. Should migrate to sidekiq-cron. gem 'httparty', '~> 0.21.0' gem 'kaminari', '~> 1.2', '>= 1.2.2' # TODO: Suggest migrate to pagy gem. diff --git a/Gemfile.lock b/Gemfile.lock index 5281d092c..4cbef0df9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -149,7 +149,6 @@ GEM chromedriver-helper (2.1.1) archive-zip (~> 0.10) nokogiri (~> 1.8) - chronic (0.10.2) chronic_duration (0.10.6) numerizer (~> 0.1.1) codeclimate-test-reporter (0.1.1) @@ -216,6 +215,8 @@ GEM handlebars-source (~> 1.0) errbase (0.2.2) erubi (1.12.0) + et-orbi (1.2.7) + tzinfo execjs (2.9.1) factory_bot (5.2.0) activesupport (>= 4.2.0) @@ -223,6 +224,9 @@ GEM factory_bot (~> 5.2.0) railties (>= 4.2.0) ffi (1.16.3) + fugit (1.9.0) + et-orbi (~> 1, >= 1.2.7) + raabro (~> 1.4) geoip (1.3.5) globalid (1.1.0) activesupport (>= 5.0) @@ -358,6 +362,7 @@ GEM ttfunk (~> 1.0.3) public_suffix (5.0.4) puma (3.12.6) + raabro (1.4.0) racc (1.7.3) rack (2.2.8) rack-cors (0.3.0) @@ -482,6 +487,10 @@ GEM connection_pool (>= 2.2.5, < 3) rack (~> 2.0) redis (>= 4.5.0, < 5) + sidekiq-cron (1.12.0) + fugit (~> 1.8) + globalid (>= 1.0.1) + sidekiq (>= 6) sidekiq-status (3.0.3) chronic_duration sidekiq (>= 6.0, < 8) @@ -554,8 +563,6 @@ GEM websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) - whenever (0.11.0) - chronic (>= 0.6.3) wicked (1.3.4) railties (>= 3.0.7) wkhtmltopdf-binary (0.12.6.6) @@ -644,6 +651,7 @@ DEPENDENCIES sass-rails (~> 5.0) selenium-webdriver sidekiq (< 7) + sidekiq-cron (~> 1.12) sidekiq-status (~> 3.0, >= 3.0.3) sidekiq-unique-jobs (= 7.1.31) simplecov (= 0.22.0) @@ -657,7 +665,6 @@ DEPENDENCIES uglifier (>= 1.3.0) uuidtools (~> 2.2) web-console (>= 3.3.0) - whenever (= 0.11.0) wicked (= 1.3.4) wkhtmltopdf-binary (~> 0.12.6.6) diff --git a/app/jobs/downloads_cache_update_job.rb b/app/jobs/downloads_cache_update_job.rb new file mode 100644 index 000000000..868530add --- /dev/null +++ b/app/jobs/downloads_cache_update_job.rb @@ -0,0 +1,7 @@ +class DownloadsCacheUpdateJob < ApplicationJob + queue_as :admin + + def perform(*args) + DownloadsCache.update + end +end diff --git a/app/jobs/elibrary_refresh_document_search_job.rb b/app/jobs/elibrary_refresh_document_search_job.rb new file mode 100644 index 000000000..4c446a512 --- /dev/null +++ b/app/jobs/elibrary_refresh_document_search_job.rb @@ -0,0 +1,17 @@ +class ElibraryRefreshDocumentSearchJob < ApplicationJob + queue_as :admin + + def perform(*args) + if DocumentSearch.citations_need_refreshing? + elapsed_time = Benchmark.realtime do + DocumentSearch.refresh_citations_and_documents + end + puts "#{Time.now} Citations & documents refreshed in #{elapsed_time}s" + elsif DocumentSearch.documents_need_refreshing? + elapsed_time = Benchmark.realtime do + DocumentSearch.refresh_documents + end + puts "#{Time.now} Documents refreshed in #{elapsed_time}s" + end + end +end diff --git a/app/jobs/rebuild_job.rb b/app/jobs/rebuild_job.rb new file mode 100644 index 000000000..5717de030 --- /dev/null +++ b/app/jobs/rebuild_job.rb @@ -0,0 +1,12 @@ +class RebuildJob < ApplicationJob + queue_as :admin + + def perform(*args) + if Rails.env.production? + # Only run on Saturday in production + Sapi.rebuild if Date.today.saturday? + else + Sapi.rebuild + end + end +end diff --git a/app/jobs/refresh_sitemap_job.rb b/app/jobs/refresh_sitemap_job.rb new file mode 100644 index 000000000..a44f1d0f8 --- /dev/null +++ b/app/jobs/refresh_sitemap_job.rb @@ -0,0 +1,8 @@ +class RefreshSitemapJob < ApplicationJob + queue_as :admin + + def perform(*args) + # https://github.com/kjvarga/sitemap_generator/issues/231 + SitemapGenerator::Interpreter.run + end +end diff --git a/app/jobs/rst_processes_import_job.rb b/app/jobs/rst_processes_import_job.rb new file mode 100644 index 000000000..e5e6686ff --- /dev/null +++ b/app/jobs/rst_processes_import_job.rb @@ -0,0 +1,7 @@ +class RstProcessesImportJob < ApplicationJob + queue_as :admin + + def perform(*args) + Import::Rst::RstCases.import_all + end +end diff --git a/app/jobs/sunday_cleanup_job.rb b/app/jobs/sunday_cleanup_job.rb new file mode 100644 index 000000000..d05b8e1c0 --- /dev/null +++ b/app/jobs/sunday_cleanup_job.rb @@ -0,0 +1,40 @@ +class SundayCleanupJob < ApplicationJob + queue_as :admin + + def perform(*args) + # rake "dashboard_stats:cache:update" + DashboardStatsCache.update_dashboard_stats + + # rake "db:common_names:cleanup" + Rails.logger.warn "### rake db:common_names:cleanup" + objects_to_delete = CommonName. + joins('LEFT JOIN taxon_commons tc ON tc.common_name_id = common_names.id'). + where('tc.id IS NULL') + Rails.logger.warn "Going to delete #{objects_to_delete.count} common names" + sql = <<-SQL + WITH objects_to_delete AS ( + #{objects_to_delete.to_sql} + ) + DELETE FROM common_names + USING objects_to_delete + WHERE common_names.id = objects_to_delete.id + SQL + ApplicationRecord.connection.execute sql + + # rake "db:taxon_names:cleanup" + Rails.logger.warn "### rake db:taxon_names:cleanup" + objects_to_delete = TaxonName. + joins('LEFT JOIN taxon_concepts tc ON tc.taxon_name_id = taxon_names.id'). + where('tc.id IS NULL') + Rails.logger.warn "Going to delete #{objects_to_delete.count} taxon names" + sql = <<-SQL + WITH objects_to_delete AS ( + #{objects_to_delete.to_sql} + ) + DELETE FROM taxon_names + USING objects_to_delete + WHERE taxon_names.id = objects_to_delete.id + SQL + ApplicationRecord.connection.execute sql + end +end diff --git a/config/routes.rb b/config/routes.rb index 9d7e27379..210228f3e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,3 +1,6 @@ +require 'sidekiq/web' +require 'sidekiq/cron/web' + Rails.application.routes.draw do devise_for :users, :controllers => { :passwords => "passwords", :registrations => "registrations", :sessions => "sessions" } as :user do @@ -17,7 +20,6 @@ get 'admin/api_usage/overview' => 'admin/api_usage#index', :as => 'api_usage_overview' get 'admin/api_usage/user_overview/:id' => 'admin/api_usage#show', :as => 'api_user_usage' - require 'sidekiq/web' mount Sidekiq::Web => '/sidekiq' namespace :api do namespace :v1 do diff --git a/config/schedule.rb b/config/schedule.rb deleted file mode 100644 index 87d1421a4..000000000 --- a/config/schedule.rb +++ /dev/null @@ -1,29 +0,0 @@ -set :output, 'log/cron.log' - -db_migrate_rebuild_period = Rails.env.production? ? :saturday : :day - -every db_migrate_rebuild_period, :at => '1:42am' do - rake "db:migrate:rebuild" -end - -every :day, :at => '4:05am' do - rake "downloads:cache:update" -end - -every :sunday, :at => '4:45am' do - rake "dashboard_stats:cache:update" - rake "db:common_names:cleanup" - rake "db:taxon_names:cleanup" -end - -every 5.minutes do - rake "elibrary:refresh_document_search" -end - -every 1.day, :at => '5:30 am' do - rake "-s sitemap:refresh" -end - -every :sunday, :at => '1:30am' do - rake "rst_processes:import" -end diff --git a/config/schedule.yml b/config/schedule.yml new file mode 100644 index 000000000..ce5959de4 --- /dev/null +++ b/config/schedule.yml @@ -0,0 +1,24 @@ +rebuild_job: + cron: "42 1 * * *" + class: "RebuildJob" + queue: admin +downloads_cache_update_job: + cron: "5 4 * * *" + class: "DownloadsCacheUpdateJob" + queue: admin +sunday_cleanup_job: + cron: "45 4 * * 0" + class: "SundayCleanupJob" + queue: admin +elibrary_refresh_document_search_job: + cron: "*/5 * * * *" + class: "ElibraryRefreshDocumentSearchJob" + queue: admin +refresh_sitemap_job: + cron: "30 5 * * *" + class: "RefreshSitemapJob" + queue: admin +rst_processes_import_job: + cron: "30 1 * * 0" + class: "RstProcessesImportJob" + queue: admin diff --git a/spec/jobs/downloads_cache_update_job_spec.rb b/spec/jobs/downloads_cache_update_job_spec.rb new file mode 100644 index 000000000..ac7758fd2 --- /dev/null +++ b/spec/jobs/downloads_cache_update_job_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe DownloadsCacheUpdateJob, type: :job do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/jobs/elibrary_refresh_document_search_job_spec.rb b/spec/jobs/elibrary_refresh_document_search_job_spec.rb new file mode 100644 index 000000000..65dfb5c13 --- /dev/null +++ b/spec/jobs/elibrary_refresh_document_search_job_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe ElibraryRefreshDocumentSearchJob, type: :job do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/jobs/rebuild_job_spec.rb b/spec/jobs/rebuild_job_spec.rb new file mode 100644 index 000000000..709f211ff --- /dev/null +++ b/spec/jobs/rebuild_job_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe RebuildJob, type: :job do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/jobs/refresh_sitemap_job_spec.rb b/spec/jobs/refresh_sitemap_job_spec.rb new file mode 100644 index 000000000..3aa5d0c9a --- /dev/null +++ b/spec/jobs/refresh_sitemap_job_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe RefreshSitemapJob, type: :job do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/jobs/rst_processes_import_job_spec.rb b/spec/jobs/rst_processes_import_job_spec.rb new file mode 100644 index 000000000..436edd23f --- /dev/null +++ b/spec/jobs/rst_processes_import_job_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe RstProcessesImportJob, type: :job do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/jobs/sunday_cleanup_job_spec.rb b/spec/jobs/sunday_cleanup_job_spec.rb new file mode 100644 index 000000000..6a0a52aeb --- /dev/null +++ b/spec/jobs/sunday_cleanup_job_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe SundayCleanupJob, type: :job do + pending "add some examples to (or delete) #{__FILE__}" +end From 0779695b4b2612cbbf7662efe069fa65e464499d Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Tue, 30 Jan 2024 11:40:28 +0000 Subject: [PATCH 120/241] remove whenever --- Capfile | 1 - lib/capistrano/tasks/config.rake | 9 --------- 2 files changed, 10 deletions(-) diff --git a/Capfile b/Capfile index fea6d1f88..666fbafec 100644 --- a/Capfile +++ b/Capfile @@ -34,7 +34,6 @@ require 'capistrano/rails/migrations' require 'capistrano/passenger' # Load custom tasks from `lib/capistrano/tasks` if you have any defined require 'capistrano/maintenance' -require 'whenever/capistrano' require 'capistrano/local_precompile' require 'capistrano/sidekiq' install_plugin Capistrano::Sidekiq diff --git a/lib/capistrano/tasks/config.rake b/lib/capistrano/tasks/config.rake index 4f827f765..eb001f599 100644 --- a/lib/capistrano/tasks/config.rake +++ b/lib/capistrano/tasks/config.rake @@ -287,15 +287,6 @@ namespace :config do end end -namespace :config do - desc "Update crontab with whenever" - task :setup do - on roles(:app, :db) do - execute "cd '#{fetch(:backup_path)}' && /bin/bash -l -c '/home/#{fetch(:deploy_user)}/.rvm/gems/ruby-2.1.3/bin/whenever -f config/#{fetch(:application)}-schedule.rb --update-crontab'" - end - end -end - namespace :config do desc "Configure app specific nagios monitoring" task :setup do From 7eaba6d23ba641b2d86d45e624a2225fdde6b8f0 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Tue, 30 Jan 2024 12:15:33 +0000 Subject: [PATCH 121/241] permit params for cache --- app/controllers/api/v1/purposes_controller.rb | 2 +- app/controllers/api/v1/sources_controller.rb | 2 +- app/controllers/api/v1/terms_controller.rb | 2 +- app/controllers/api/v1/units_controller.rb | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/controllers/api/v1/purposes_controller.rb b/app/controllers/api/v1/purposes_controller.rb index 8b124c105..f5228ad4b 100644 --- a/app/controllers/api/v1/purposes_controller.rb +++ b/app/controllers/api/v1/purposes_controller.rb @@ -1,6 +1,6 @@ class Api::V1::PurposesController < ApplicationController caches_action :index, :cache_path => Proc.new { |c| - { :locale => "en" }.merge(c.params.select { |k, v| !v.blank? && "locale" == k }) + { :locale => "en" }.merge(c.params.permit!.select { |k, v| !v.blank? && "locale" == k }) } def index @purposes = Purpose.all.order(:code) diff --git a/app/controllers/api/v1/sources_controller.rb b/app/controllers/api/v1/sources_controller.rb index 5f2c12df3..ae5753308 100644 --- a/app/controllers/api/v1/sources_controller.rb +++ b/app/controllers/api/v1/sources_controller.rb @@ -1,6 +1,6 @@ class Api::V1::SourcesController < ApplicationController caches_action :index, :cache_path => Proc.new { |c| - { :locale => "en" }.merge(c.params.select { |k, v| !v.blank? && "locale" == k }) + { :locale => "en" }.merge(c.params.permit!.select { |k, v| !v.blank? && "locale" == k }) } def index @sources = Source.all.order(:code) diff --git a/app/controllers/api/v1/terms_controller.rb b/app/controllers/api/v1/terms_controller.rb index 59274f4d2..c28d70ce2 100644 --- a/app/controllers/api/v1/terms_controller.rb +++ b/app/controllers/api/v1/terms_controller.rb @@ -1,6 +1,6 @@ class Api::V1::TermsController < ApplicationController caches_action :index, :cache_path => Proc.new { |c| - { :locale => "en" }.merge(c.params.select { |k, v| !v.blank? && "locale" == k }) + { :locale => "en" }.merge(c.params.permit!.select { |k, v| !v.blank? && "locale" == k }) } def index @terms = Term.all.order(:code) diff --git a/app/controllers/api/v1/units_controller.rb b/app/controllers/api/v1/units_controller.rb index c90ebb8fb..361c20993 100644 --- a/app/controllers/api/v1/units_controller.rb +++ b/app/controllers/api/v1/units_controller.rb @@ -1,8 +1,9 @@ class Api::V1::UnitsController < ApplicationController caches_action :index, :cache_path => Proc.new { |c| - { :locale => "en" }.merge(c.params.select { |k, v| !v.blank? && "locale" == k }) + { :locale => "en" }.merge(c.params.permit!.select { |k, v| !v.blank? && "locale" == k }) } def index + # { :locale => "en" }.merge(params.select { |k, v| !v.blank? && "locale" == k }) @units = Unit.all.order(:code) render :json => @units, :each_serializer => Species::UnitSerializer, From 361f2b79877d070f93f4df4434c2c37a6adfc540 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Wed, 31 Jan 2024 09:27:11 +0000 Subject: [PATCH 122/241] fix rspec --- spec/jobs/downloads_cache_update_job_spec.rb | 2 +- spec/jobs/elibrary_refresh_document_search_job_spec.rb | 2 +- spec/jobs/rebuild_job_spec.rb | 2 +- spec/jobs/refresh_sitemap_job_spec.rb | 2 +- spec/jobs/rst_processes_import_job_spec.rb | 2 +- spec/jobs/sunday_cleanup_job_spec.rb | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/jobs/downloads_cache_update_job_spec.rb b/spec/jobs/downloads_cache_update_job_spec.rb index ac7758fd2..18f1468f8 100644 --- a/spec/jobs/downloads_cache_update_job_spec.rb +++ b/spec/jobs/downloads_cache_update_job_spec.rb @@ -1,4 +1,4 @@ -require 'rails_helper' +require 'spec_helper' RSpec.describe DownloadsCacheUpdateJob, type: :job do pending "add some examples to (or delete) #{__FILE__}" diff --git a/spec/jobs/elibrary_refresh_document_search_job_spec.rb b/spec/jobs/elibrary_refresh_document_search_job_spec.rb index 65dfb5c13..965361409 100644 --- a/spec/jobs/elibrary_refresh_document_search_job_spec.rb +++ b/spec/jobs/elibrary_refresh_document_search_job_spec.rb @@ -1,4 +1,4 @@ -require 'rails_helper' +require 'spec_helper' RSpec.describe ElibraryRefreshDocumentSearchJob, type: :job do pending "add some examples to (or delete) #{__FILE__}" diff --git a/spec/jobs/rebuild_job_spec.rb b/spec/jobs/rebuild_job_spec.rb index 709f211ff..1396ba492 100644 --- a/spec/jobs/rebuild_job_spec.rb +++ b/spec/jobs/rebuild_job_spec.rb @@ -1,4 +1,4 @@ -require 'rails_helper' +require 'spec_helper' RSpec.describe RebuildJob, type: :job do pending "add some examples to (or delete) #{__FILE__}" diff --git a/spec/jobs/refresh_sitemap_job_spec.rb b/spec/jobs/refresh_sitemap_job_spec.rb index 3aa5d0c9a..917153e59 100644 --- a/spec/jobs/refresh_sitemap_job_spec.rb +++ b/spec/jobs/refresh_sitemap_job_spec.rb @@ -1,4 +1,4 @@ -require 'rails_helper' +require 'spec_helper' RSpec.describe RefreshSitemapJob, type: :job do pending "add some examples to (or delete) #{__FILE__}" diff --git a/spec/jobs/rst_processes_import_job_spec.rb b/spec/jobs/rst_processes_import_job_spec.rb index 436edd23f..94800a77f 100644 --- a/spec/jobs/rst_processes_import_job_spec.rb +++ b/spec/jobs/rst_processes_import_job_spec.rb @@ -1,4 +1,4 @@ -require 'rails_helper' +require 'spec_helper' RSpec.describe RstProcessesImportJob, type: :job do pending "add some examples to (or delete) #{__FILE__}" diff --git a/spec/jobs/sunday_cleanup_job_spec.rb b/spec/jobs/sunday_cleanup_job_spec.rb index 6a0a52aeb..e870354ba 100644 --- a/spec/jobs/sunday_cleanup_job_spec.rb +++ b/spec/jobs/sunday_cleanup_job_spec.rb @@ -1,4 +1,4 @@ -require 'rails_helper' +require 'spec_helper' RSpec.describe SundayCleanupJob, type: :job do pending "add some examples to (or delete) #{__FILE__}" From 6d4e1f770f7d0f46d5e4e3927a2c088eee5965de Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Wed, 31 Jan 2024 09:56:09 +0000 Subject: [PATCH 123/241] Fix incorrect `head` when API with bad token. --- app/controllers/api/v1/shipments_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/v1/shipments_controller.rb b/app/controllers/api/v1/shipments_controller.rb index 5a316c139..5628f47e2 100644 --- a/app/controllers/api/v1/shipments_controller.rb +++ b/app/controllers/api/v1/shipments_controller.rb @@ -166,7 +166,7 @@ def sanitized_attributes def authenticate token = request.headers['X-Authentication-Token'] unless token == Rails.application.secrets[:shipments_api_token] - head status: :unauthorized + head :unauthorized return false end end From 9782df9be66efe8306f871d79c57adadd6999622 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Wed, 31 Jan 2024 10:48:00 +0000 Subject: [PATCH 124/241] Fix bugs by code review --- .../v1/document_geo_entities_controller.rb | 15 -------- .../api/v1/documents_controller.rb | 4 +- .../api/v1/eu_decisions_controller.rb | 2 +- app/controllers/api/v1/events_controller.rb | 2 +- .../api/v1/shipments_controller.rb | 38 ++++++++++--------- .../api/v1/trade_plus_filters_controller.rb | 5 ++- app/controllers/api/v1/units_controller.rb | 1 - config/routes.rb | 5 +-- lib/modules/trade/grouping/base.rb | 4 ++ 9 files changed, 34 insertions(+), 42 deletions(-) diff --git a/app/controllers/api/v1/document_geo_entities_controller.rb b/app/controllers/api/v1/document_geo_entities_controller.rb index 8034bf3a1..1892b0a51 100644 --- a/app/controllers/api/v1/document_geo_entities_controller.rb +++ b/app/controllers/api/v1/document_geo_entities_controller.rb @@ -1,6 +1,4 @@ class Api::V1::DocumentGeoEntitiesController < ApplicationController - before_action :set_locale - def index @geo_entities = GeoEntity.current.includes(:geo_entity_type). order("name_#{I18n.locale}") @@ -26,17 +24,4 @@ def index each_serializer: Species::GeoEntitySerializer, meta: { total: @geo_entities.count } end - - private - - def set_locale - locale = params[:locale].try(:downcase).try(:strip) || - 'en' - I18n.locale = - if ['en', 'es', 'fr'].include?(locale) - locale - else - 'en' - end - end end diff --git a/app/controllers/api/v1/documents_controller.rb b/app/controllers/api/v1/documents_controller.rb index 32fab4383..6a209523e 100644 --- a/app/controllers/api/v1/documents_controller.rb +++ b/app/controllers/api/v1/documents_controller.rb @@ -34,7 +34,7 @@ def index #TODO move pagination and ordering to the document_search module after refactoring of SQL mviews page = params[:page] || 1 - per_page = params[:per_page] || 100 + per_page = (params[:per_page] || 100).to_i ordered_docs = if params[:taxon_concepts_ids].present? && params[:event_type] == 'IdMaterials' @@ -58,7 +58,7 @@ def index meta: { total: @search.cached_total_cnt, page: @search.page, - per_page: @search.per_page + per_page: per_page } end diff --git a/app/controllers/api/v1/eu_decisions_controller.rb b/app/controllers/api/v1/eu_decisions_controller.rb index bb132a17c..25e5ee695 100644 --- a/app/controllers/api/v1/eu_decisions_controller.rb +++ b/app/controllers/api/v1/eu_decisions_controller.rb @@ -60,7 +60,7 @@ def eu_decision_select_attrs end def sanitized_params - filters = permitted_params.inject({}) do |h, (k,v)| + filters = permitted_params.to_h.inject({}) do |h, (k,v)| h[k] = v.reject(&:empty?).map!(&:to_i) h end diff --git a/app/controllers/api/v1/events_controller.rb b/app/controllers/api/v1/events_controller.rb index bcc6f950b..82d244b92 100644 --- a/app/controllers/api/v1/events_controller.rb +++ b/app/controllers/api/v1/events_controller.rb @@ -4,7 +4,7 @@ def index @events = Event. select([:id, :name, :type, :published_at]). where(type: Event.elibrary_event_types.map(&:name)). - order('type, published_at DESC') + order(:type, published_at: :desc) render :json => @events, :each_serializer => Species::EventSerializer, :meta => { :total => @events.count(:all) } diff --git a/app/controllers/api/v1/shipments_controller.rb b/app/controllers/api/v1/shipments_controller.rb index 5628f47e2..2ef5433d0 100644 --- a/app/controllers/api/v1/shipments_controller.rb +++ b/app/controllers/api/v1/shipments_controller.rb @@ -8,7 +8,7 @@ class Api::V1::ShipmentsController < ApplicationController end def chart_query - @chart_data = Rails.cache.fetch(['chart_data', params], expires_in: 1.week) do + @chart_data = Rails.cache.fetch(['chart_data', permit_params], expires_in: 1.week) do @grouping_class.new(['issue_type', 'year']) .countries_reported_range(params[:year]) end @@ -55,20 +55,20 @@ def country_query # Compliance tool search & full list action def search_query - query = @grouping_class.new(sanitized_attributes, params) + query = @grouping_class.new(sanitized_attributes, permit_params) data = query.run - @search_data = Rails.cache.fetch(['search_data', params], expires_in: 1.week) do - query.build_hash(data, params) + @search_data = Rails.cache.fetch(['search_data', permit_params], expires_in: 1.week) do + query.build_hash(data, permit_params) end - @filtered_data = query.filter(@search_data, params) + @filtered_data = query.filter(@search_data, permit_params) render :json => Kaminari.paginate_array(@filtered_data).page(params[:page]).per(params[:per_page]), - :meta => metadata(@filtered_data, params) + :meta => metadata(@filtered_data, permit_params) end def over_time_query # TODO Remember to implement permitted parameters here - query = @grouping_class.new(sanitized_attributes, params) - @over_time_data = Rails.cache.fetch(['over_time_data', params], expires_in: 1.week) do + query = @grouping_class.new(sanitized_attributes, permit_params) + @over_time_data = Rails.cache.fetch(['over_time_data', permit_params], expires_in: 1.week) do query.over_time_data end @@ -78,8 +78,8 @@ def over_time_query # TODO refactor to merge this method and the over_time one above together def aggregated_over_time_query # TODO Remember to implement permitted parameters here - query = @grouping_class.new(sanitized_attributes, params) - @aggregated_over_time_data = Rails.cache.fetch(['aggregated_over_time_data', params], expires_in: 1.week) do + query = @grouping_class.new(sanitized_attributes, permit_params) + @aggregated_over_time_data = Rails.cache.fetch(['aggregated_over_time_data', permit_params], expires_in: 1.week) do query.aggregated_over_time_data end @@ -87,26 +87,26 @@ def aggregated_over_time_query end def download_data - @download_data = Rails.cache.fetch(['download_data', params], expires_in: 1.week) do + @download_data = Rails.cache.fetch(['download_data', permit_params], expires_in: 1.week) do Trade::DownloadDataRetriever.dashboard_download(download_params).to_a end render :json => @download_data end def search_download_data - @download_data = Rails.cache.fetch(['search_download_data', params], expires_in: 1.week) do + @download_data = Rails.cache.fetch(['search_download_data', permit_params], expires_in: 1.week) do Trade::DownloadDataRetriever.search_download(download_params).to_a end render :json => @download_data end def search_download_all_data - query = @grouping_class.new(sanitized_attributes, params) + query = @grouping_class.new(sanitized_attributes, permit_params) data = query.run - @search_download_all_data = Rails.cache.fetch(['search_download_all_data', params], expires_in: 1.week) do - search_data = query.build_hash(data, params) - filtered_data = query.filter(search_data, params) - data_ids = query.filter_download_data(filtered_data, params) + @search_download_all_data = Rails.cache.fetch(['search_download_all_data', permit_params], expires_in: 1.week) do + search_data = query.build_hash(data, permit_params) + filtered_data = query.filter(search_data, permit_params) + data_ids = query.filter_download_data(filtered_data, permit_params) hash_params = params_hash_builder(data_ids, download_params) Trade::DownloadDataRetriever.search_download(hash_params).to_a end @@ -115,6 +115,10 @@ def search_download_all_data private + def permit_params + params.permit! + end + def set_pagination_headers(data, params) data = instance_variable_get("@#{data}").presence # Make sure the count works for both TradeView and ComplianceTool diff --git a/app/controllers/api/v1/trade_plus_filters_controller.rb b/app/controllers/api/v1/trade_plus_filters_controller.rb index 799ca37cc..b2c659bce 100644 --- a/app/controllers/api/v1/trade_plus_filters_controller.rb +++ b/app/controllers/api/v1/trade_plus_filters_controller.rb @@ -2,8 +2,9 @@ class Api::V1::TradePlusFiltersController < ApplicationController respond_to :json def index - filters_service = Trade::TradePlusFilters.new(params['locale']) - filters = Rails.cache.fetch(['trade_plus_filters', params['locale']], expires_in: 4.weeks) do + locale = params['locale'] || I18n.default_locale.to_s + filters_service = Trade::TradePlusFilters.new(locale) + filters = Rails.cache.fetch(['trade_plus_filters', locale], expires_in: 4.weeks) do res = ApplicationRecord.connection.execute(filters_service.query) filters_service.response_ordering(res) end diff --git a/app/controllers/api/v1/units_controller.rb b/app/controllers/api/v1/units_controller.rb index 361c20993..69bada19f 100644 --- a/app/controllers/api/v1/units_controller.rb +++ b/app/controllers/api/v1/units_controller.rb @@ -3,7 +3,6 @@ class Api::V1::UnitsController < ApplicationController { :locale => "en" }.merge(c.params.permit!.select { |k, v| !v.blank? && "locale" == k }) } def index - # { :locale => "en" }.merge(params.select { |k, v| !v.blank? && "locale" == k }) @units = Unit.all.order(:code) render :json => @units, :each_serializer => Species::UnitSerializer, diff --git a/config/routes.rb b/config/routes.rb index 210228f3e..827761932 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -32,7 +32,7 @@ resources :purposes, :only => [:index] resources :trade_plus_filters, only: :index resources :eu_decisions, only: :index - resources :documents do + resources :documents, :only => [:index, :show] do collection do get 'download_zip' end @@ -41,7 +41,6 @@ resources :events, only: [:index] resources :document_tags, only: [:index] get '/dashboard_stats/:iso_code' => 'dashboard_stats#index' - resources :shipments, only: [:index] get '/shipments/chart' => 'shipments#chart_query' get '/shipments/grouped' => 'shipments#grouped_query' get '/shipments/over_time' => 'shipments#over_time_query' @@ -161,7 +160,7 @@ :only => [:index, :new, :create, :edit, :update, :destroy], :as => :cites_suspensions resources :taxon_instruments, :only => [:index, :new, :create, :edit, :update, :destroy] - resources :cites_captivity_processes, + resources :cites_captivity_processes, :only => [:index, :new, :create, :edit, :update, :destroy] end resources :nomenclature_changes do # TODO: look like only support :index, :show, :destroy diff --git a/lib/modules/trade/grouping/base.rb b/lib/modules/trade/grouping/base.rb index 14ec6e4c9..964edf94a 100644 --- a/lib/modules/trade/grouping/base.rb +++ b/lib/modules/trade/grouping/base.rb @@ -122,6 +122,10 @@ def sanitise_condition condition_attributes = @opts.keep_if do |k, v| filtering_attributes.key?(k.to_sym) && v.present? end + unless condition_attributes.is_a?(Hash) + condition_attributes.permit! + condition_attributes = condition_attributes.to_h + end # Get default attributes if missing from params if @opts[:with_defaults] condition_attributes.reverse_merge!(self.class.default_filtering_attributes) From 5c6036353752f899fb9750e838da51f2e2d4b49e Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Wed, 31 Jan 2024 11:50:41 +0000 Subject: [PATCH 125/241] DEPRECATION WARNING: The success? predicate is deprecated and will be removed in Rails 6.0. Please use successful? as provided by Rack::Response::Helpers. --- spec/controllers/admin/change_types_controller_spec.rb | 2 +- spec/controllers/admin/designations_controller_spec.rb | 2 +- spec/controllers/admin/distributions_controller_spec.rb | 4 ++-- spec/controllers/admin/events_controller_spec.rb | 2 +- spec/controllers/admin/geo_entities_controller_spec.rb | 2 +- spec/controllers/admin/instruments_controller_spec.rb | 2 +- spec/controllers/admin/languages_controller_spec.rb | 2 +- spec/controllers/admin/ranks_controller_spec.rb | 2 +- spec/controllers/admin/references_controller_spec.rb | 2 +- spec/controllers/admin/species_listings_controller_spec.rb | 2 +- spec/controllers/admin/tags_controller_spec.rb | 2 +- spec/controllers/admin/taxon_commons_controller_spec.rb | 2 +- .../admin/taxon_concept_references_controller_spec.rb | 2 +- spec/controllers/admin/taxon_concepts_controller_spec.rb | 2 +- spec/controllers/admin/taxonomies_controller_spec.rb | 2 +- spec/controllers/admin/users_controller_spec.rb | 2 +- spec/controllers/trade/ember_controller_spec.rb | 2 +- spec/controllers/trade/validation_rules_controller_spec.rb | 2 +- 18 files changed, 19 insertions(+), 19 deletions(-) diff --git a/spec/controllers/admin/change_types_controller_spec.rb b/spec/controllers/admin/change_types_controller_spec.rb index d655cb1cb..f8e4db2fc 100644 --- a/spec/controllers/admin/change_types_controller_spec.rb +++ b/spec/controllers/admin/change_types_controller_spec.rb @@ -34,7 +34,7 @@ let(:change_type) { create(:change_type) } it "responds with 200 when successful" do put :update, :format => 'json', params: { :id => change_type.id, :change_type => { :name => 'ZZ' } }, xhr: true - expect(response).to be_success + expect(response).to be_successful end it "responds with json when not successful" do put :update, :format => 'json', params: { :id => change_type.id, :change_type => { :name => nil } }, xhr: true diff --git a/spec/controllers/admin/designations_controller_spec.rb b/spec/controllers/admin/designations_controller_spec.rb index d071ee99f..b874b8e3b 100644 --- a/spec/controllers/admin/designations_controller_spec.rb +++ b/spec/controllers/admin/designations_controller_spec.rb @@ -43,7 +43,7 @@ let(:designation) { create(:designation) } it "responds with 200 when successful" do put :update, :format => 'json', params: { :id => designation.id, :designation => { :name => 'ZZ' } }, xhr: true - expect(response).to be_success + expect(response).to be_successful end it "responds with json when not successful" do put :update, :format => 'json', params: { :id => designation.id, :designation => { :name => nil } }, xhr: true diff --git a/spec/controllers/admin/distributions_controller_spec.rb b/spec/controllers/admin/distributions_controller_spec.rb index 5b16f14fc..8b6fe5732 100644 --- a/spec/controllers/admin/distributions_controller_spec.rb +++ b/spec/controllers/admin/distributions_controller_spec.rb @@ -9,7 +9,7 @@ describe "XHR GET 'new'" do it "returns http success and renders the new template" do get :new, params: { :taxon_concept_id => @taxon_concept.id }, xhr: true - expect(response).to be_success + expect(response).to be_successful expect(response).to render_template('new') end it "assigns @geo_entities (country and territory) with two objects" do @@ -90,7 +90,7 @@ :geo_entity_id => geo_entity.id } } - expect(response).to be_success + expect(response).to be_successful end end diff --git a/spec/controllers/admin/events_controller_spec.rb b/spec/controllers/admin/events_controller_spec.rb index 40c6388ac..7243c8dc0 100644 --- a/spec/controllers/admin/events_controller_spec.rb +++ b/spec/controllers/admin/events_controller_spec.rb @@ -59,7 +59,7 @@ let(:event) { create(:event) } it "responds with 200 when successful" do put :update, :format => 'json', params: { :id => event.id, :event => { :name => 'ZZ' } }, xhr: true - expect(response).to be_success + expect(response).to be_successful end it "responds with json when not successful" do put :update, :format => 'json', params: { :id => event.id, :event => { :name => nil } }, xhr: true diff --git a/spec/controllers/admin/geo_entities_controller_spec.rb b/spec/controllers/admin/geo_entities_controller_spec.rb index abab4363c..27012d472 100644 --- a/spec/controllers/admin/geo_entities_controller_spec.rb +++ b/spec/controllers/admin/geo_entities_controller_spec.rb @@ -57,7 +57,7 @@ let(:geo_entity) { create(:geo_entity, geo_entity_type: country_geo_entity_type) } it "responds with 200 when successful" do put :update, format: 'json', params: { id: geo_entity.id, geo_entity: { iso_code2: 'ZZ' } }, xhr: true - expect(response).to be_success + expect(response).to be_successful end it "responds with json when not successful" do put :update, format: 'json', params: { id: geo_entity.id, geo_entity: { iso_code2: nil } }, xhr: true diff --git a/spec/controllers/admin/instruments_controller_spec.rb b/spec/controllers/admin/instruments_controller_spec.rb index 306b91d6c..0f8c9f4bd 100644 --- a/spec/controllers/admin/instruments_controller_spec.rb +++ b/spec/controllers/admin/instruments_controller_spec.rb @@ -43,7 +43,7 @@ let(:instrument) { create(:instrument) } it "responds with 200 when successful" do put :update, :format => 'json', params: { :id => instrument.id, :instrument => { :name => 'ZZ' } }, xhr: true - expect(response).to be_success + expect(response).to be_successful end it "responds with json when not successful" do put :update, :format => 'json', params: { :id => instrument.id, :instrument => { :name => nil } }, xhr: true diff --git a/spec/controllers/admin/languages_controller_spec.rb b/spec/controllers/admin/languages_controller_spec.rb index c3f0deff6..6aa36b51b 100644 --- a/spec/controllers/admin/languages_controller_spec.rb +++ b/spec/controllers/admin/languages_controller_spec.rb @@ -31,7 +31,7 @@ let(:language) { create(:language) } it "responds with 200 when successful" do put :update, :format => 'json', params: { :id => language.id, :language => { :iso_code1 => 'ZZ' } }, xhr: true - expect(response).to be_success + expect(response).to be_successful end it "responds with json when not successful" do put :update, :format => 'json', params: { :id => language.id, :language => { :iso_code1 => 'zzz' } }, xhr: true diff --git a/spec/controllers/admin/ranks_controller_spec.rb b/spec/controllers/admin/ranks_controller_spec.rb index 80b933e37..ae668629b 100644 --- a/spec/controllers/admin/ranks_controller_spec.rb +++ b/spec/controllers/admin/ranks_controller_spec.rb @@ -31,7 +31,7 @@ let(:rank) { create(:rank) } it "responds with 200 when successful" do put :update, :format => 'json', params: { :id => rank.id, :rank => { :name => 'ZZ' } }, xhr: true - expect(response).to be_success + expect(response).to be_successful end it "responds with json when not successful" do put :update, :format => 'json', params: { :id => rank.id, :rank => { :name => nil } }, xhr: true diff --git a/spec/controllers/admin/references_controller_spec.rb b/spec/controllers/admin/references_controller_spec.rb index 0dd099223..a3a73b4f5 100644 --- a/spec/controllers/admin/references_controller_spec.rb +++ b/spec/controllers/admin/references_controller_spec.rb @@ -43,7 +43,7 @@ let(:reference) { create(:reference) } it "responds with 200 when successful" do put :update, :format => 'json', params: { :id => reference.id, :reference => { :citation => 'ZZ' } }, xhr: true - expect(response).to be_success + expect(response).to be_successful end it "responds with json when not successful" do put :update, :format => 'json', params: { :id => reference.id, :reference => { :citation => nil } }, xhr: true diff --git a/spec/controllers/admin/species_listings_controller_spec.rb b/spec/controllers/admin/species_listings_controller_spec.rb index 6d074ff5a..6062287f5 100644 --- a/spec/controllers/admin/species_listings_controller_spec.rb +++ b/spec/controllers/admin/species_listings_controller_spec.rb @@ -34,7 +34,7 @@ let(:species_listing) { create(:species_listing) } it "responds with 200 when successful" do put :update, :format => 'json', params: { :id => species_listing.id, :species_listing => { :name => 'ZZ' } }, xhr: true - expect(response).to be_success + expect(response).to be_successful end it "responds with json when not successful" do put :update, :format => 'json', params: { :id => species_listing.id, :species_listing => { :name => nil } }, xhr: true diff --git a/spec/controllers/admin/tags_controller_spec.rb b/spec/controllers/admin/tags_controller_spec.rb index 2946d0310..ec9116d37 100644 --- a/spec/controllers/admin/tags_controller_spec.rb +++ b/spec/controllers/admin/tags_controller_spec.rb @@ -27,7 +27,7 @@ context "when JSON" do it "responds with 200 when successful" do put :update, :format => 'json', params: { :id => preset_tag.id, :tag => { dummy: 'test' } }, xhr: true - expect(response).to be_success + expect(response).to be_successful end it "responds with json error when not successful" do put :update, :format => 'json', params: { :id => preset_tag.id, :tag => { :model => 'FakeCategory' } }, xhr: true diff --git a/spec/controllers/admin/taxon_commons_controller_spec.rb b/spec/controllers/admin/taxon_commons_controller_spec.rb index 9f82012a1..e81b244dc 100644 --- a/spec/controllers/admin/taxon_commons_controller_spec.rb +++ b/spec/controllers/admin/taxon_commons_controller_spec.rb @@ -11,7 +11,7 @@ describe "XHR GET 'new'" do it "returns http success and renders the new template" do get :new, params: { :taxon_concept_id => @taxon_concept.id, :format => 'js' }, xhr: true - expect(response).to be_success + expect(response).to be_successful expect(response).to render_template('new') end end diff --git a/spec/controllers/admin/taxon_concept_references_controller_spec.rb b/spec/controllers/admin/taxon_concept_references_controller_spec.rb index 559d5e909..995a4692b 100644 --- a/spec/controllers/admin/taxon_concept_references_controller_spec.rb +++ b/spec/controllers/admin/taxon_concept_references_controller_spec.rb @@ -88,7 +88,7 @@ describe "XHR GET 'new'" do it "returns http success and renders the new template" do get :new, params: { :taxon_concept_id => @taxon_concept.id }, xhr: true, :format => 'js' - expect(response).to be_success + expect(response).to be_successful expect(response).to render_template('new') end end diff --git a/spec/controllers/admin/taxon_concepts_controller_spec.rb b/spec/controllers/admin/taxon_concepts_controller_spec.rb index 0767f4562..1bd9719ae 100644 --- a/spec/controllers/admin/taxon_concepts_controller_spec.rb +++ b/spec/controllers/admin/taxon_concepts_controller_spec.rb @@ -71,7 +71,7 @@ it "responds with 200 when successful" do put :update, :format => 'json', params: { :id => taxon_concept.id, :taxon_concept => { dummy: 'test' } }, xhr: true - expect(response).to be_success + expect(response).to be_successful end it "responds with json error when not successful" do put :update, :format => 'json', params: { :id => taxon_concept.id, diff --git a/spec/controllers/admin/taxonomies_controller_spec.rb b/spec/controllers/admin/taxonomies_controller_spec.rb index 192b6de76..b9e5817af 100644 --- a/spec/controllers/admin/taxonomies_controller_spec.rb +++ b/spec/controllers/admin/taxonomies_controller_spec.rb @@ -43,7 +43,7 @@ let(:taxonomy) { create(:taxonomy) } it "responds with 200 when successful" do put :update, :format => 'json', params: { :id => taxonomy.id, :taxonomy => { :name => 'ZZ' } }, xhr: true - expect(response).to be_success + expect(response).to be_successful end it "responds with json when not successful" do put :update, :format => 'json', params: { :id => taxonomy.id, :taxonomy => { :name => nil } }, xhr: true diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index 27a50e530..0661ce596 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -39,7 +39,7 @@ let(:user) { create(:user) } it "responds with 200 when successful" do put :update, :format => 'js', params: { :id => user.id, :user => { :name => 'ZZ' } }, xhr: true - expect(response).to be_success + expect(response).to be_successful expect(response).to render_template('create') end it "responds with template new when not successful" do diff --git a/spec/controllers/trade/ember_controller_spec.rb b/spec/controllers/trade/ember_controller_spec.rb index 3e5b55b4b..535a7942b 100644 --- a/spec/controllers/trade/ember_controller_spec.rb +++ b/spec/controllers/trade/ember_controller_spec.rb @@ -6,7 +6,7 @@ describe "GET 'start'" do it "returns http success" do get 'start' - expect(response).to be_success + expect(response).to be_successful end end diff --git a/spec/controllers/trade/validation_rules_controller_spec.rb b/spec/controllers/trade/validation_rules_controller_spec.rb index 8e3bbb719..1c8927ad7 100644 --- a/spec/controllers/trade/validation_rules_controller_spec.rb +++ b/spec/controllers/trade/validation_rules_controller_spec.rb @@ -6,7 +6,7 @@ describe "GET index" do it "should return success" do get :index, format: :json - expect(response).to be_success + expect(response).to be_successful end end end From 0d9ee2ab8188332a98772d8f6f60e552301aa81a Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Wed, 31 Jan 2024 15:41:08 +0000 Subject: [PATCH 126/241] downgrade papertail due to https://github.com/paper-trail-gem/paper_trail/blob/master/doc/pt_13_yaml_safe_load.md --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 275bca69f..135c92980 100644 --- a/Gemfile +++ b/Gemfile @@ -145,7 +145,7 @@ end gem 'geoip', '1.3.5' # TODO: no change logs, no idea if safe to update. Latest version is 1.6.4 @ 2018 gem 'request_store', '~> 1.5', '>= 1.5.1' -gem 'paper_trail', '13.0.0' # TODO: latest is 15.1.0. Can upgrade to newer version when we at Rails 6 +gem 'paper_trail', '12.3.0' # TODO: latest is 15.1.0. Can't upgrade until we fix https://github.com/paper-trail-gem/paper_trail/blob/master/doc/pt_13_yaml_safe_load.md gem 'dotenv-rails', '2.0.1' diff --git a/Gemfile.lock b/Gemfile.lock index 4cbef0df9..8d8da5958 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -336,7 +336,7 @@ GEM oj (3.16.3) bigdecimal (>= 3.0) orm_adapter (0.5.0) - paper_trail (13.0.0) + paper_trail (12.3.0) activerecord (>= 5.2) request_store (~> 1.1) parallel (1.24.0) @@ -627,7 +627,7 @@ DEPENDENCIES nested_form (~> 0.3.2) nokogiri (~> 1.15, >= 1.15.5) oj (~> 3.16, >= 3.16.3) - paper_trail (= 13.0.0) + paper_trail (= 12.3.0) pdfkit (~> 0.8.7.3) pg (~> 1.5, >= 1.5.4) pg_array_parser (~> 0.0.9) From 1dfc47319fdb93a8395cb69680d0ab187061fc8c Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 2 Feb 2024 18:28:21 +0000 Subject: [PATCH 127/241] strong param fix --- app/helpers/admin_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/admin_helper.rb b/app/helpers/admin_helper.rb index 76351f3c7..db14ba1f2 100644 --- a/app/helpers/admin_helper.rb +++ b/app/helpers/admin_helper.rb @@ -8,7 +8,7 @@ def ancestors_path(taxon_concept) name = taxon_concept.data["#{r.name.downcase}_name"] id = taxon_concept.data["#{r.name.downcase}_id"] if name && id - link_to(name, params.merge(:taxon_concept_id => id), :title => r.name) + link_to(name, params.permit!.merge(:taxon_concept_id => id), :title => r.name) else nil end From 6f4dfecaf55948c556d2690913c117ec5a465877 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Tue, 30 Jan 2024 18:15:15 +0000 Subject: [PATCH 128/241] Rails 6.0; rails app:update + rails diff; able to start web server but rspec fail --- .gitignore | 10 +- Dockerfile.cap-deploy | 2 + Gemfile | 32 +-- Gemfile.lock | 240 ++++++++++-------- bin/bundle | 3 - bin/setup | 12 +- bin/spring | 15 -- bin/update | 31 --- config/application.rb | 2 +- config/cable.yml | 2 +- config/environments/development.rb | 5 +- config/environments/production.rb | 33 ++- config/environments/test.rb | 20 +- .../0_new_framework_defaults_5_2.rb | 38 --- .../0_new_framework_defaults_6_0.rb | 45 ++++ .../initializers/content_security_policy.rb | 5 + config/puma.rb | 7 +- config/spring.rb | 12 +- 18 files changed, 264 insertions(+), 250 deletions(-) delete mode 100755 bin/bundle delete mode 100755 bin/spring delete mode 100755 bin/update delete mode 100644 config/initializers/0_new_framework_defaults_5_2.rb create mode 100644 config/initializers/0_new_framework_defaults_6_0.rb diff --git a/.gitignore b/.gitignore index 0004f2107..bdeed6a06 100644 --- a/.gitignore +++ b/.gitignore @@ -13,13 +13,15 @@ !/log/.keep !/tmp/.keep -# Ignore uploaded files in development +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/ +!/tmp/pids/.keep + +# Ignore uploaded files in development. /storage/* !/storage/.keep -/node_modules -/yarn-error.log - /public/assets .byebug_history diff --git a/Dockerfile.cap-deploy b/Dockerfile.cap-deploy index c1559879d..3ea5832e8 100644 --- a/Dockerfile.cap-deploy +++ b/Dockerfile.cap-deploy @@ -6,6 +6,8 @@ ENV DEBIAN_FRONTEND=noninteractive # Rails and SAPI has some additional dependencies, e.g. rake requires a JS # runtime, so attempt to get these from apt, where possible RUN apt-get update && apt-get install -y --force-yes \ + # For ruby? + libsodium-dev libgmp3-dev \ # For RVM gnupg procps curl libssl-dev \ # For assets local_precompile (cap deploy) diff --git a/Gemfile b/Gemfile index 135c92980..03a44af37 100644 --- a/Gemfile +++ b/Gemfile @@ -4,17 +4,20 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby '2.7.8' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' -gem 'rails', '5.2.8.1' +gem 'rails', '6.0.6.1' # Use sqlite3 as the database for Active Record # gem 'sqlite3' # Use Puma as the app server -gem 'puma', '~> 3.11' +gem 'puma', '~> 4.1' # Use SCSS for stylesheets -gem 'sass-rails', '~> 5.0' +gem 'sass-rails', '>= 6' +# https://stackoverflow.com/questions/55213868/rails-6-how-to-disable-webpack-and-use-sprockets-instead +gem 'sprockets', '3.7.2' +gem 'sprockets-rails', :require => 'sprockets/railtie' # Use Uglifier as compressor for JavaScript assets gem 'uglifier', '>= 1.3.0' # Use CoffeeScript for .coffee assets and views -gem 'coffee-rails', '~> 4.2' +gem 'coffee-rails', '~> 5.0' # See https://github.com/rails/execjs#readme for more supported runtimes # gem 'mini_racer', platforms: :ruby @@ -27,10 +30,10 @@ gem 'pg_array_parser', '~> 0.0.9' gem 'nested-hstore', '~> 0.1.2' gem 'pg_search', '~> 2.3', '>= 2.3.6' gem 'oj', '~> 3.16', '>= 3.16.3' # optimised JSON (picked by multi_json) -gem 'nokogiri', '~> 1.15', '>= 1.15.5' # TODO: New version need Ruby 3 -gem 'inherited_resources', '1.13.1' # Deprecated (https://github.com/activeadmin/inherited_resources#notice) # TODO: need upgrade when upgrade to Rails 6 +gem 'inherited_resources', '~> 1.14' # Deprecated (https://github.com/activeadmin/inherited_resources#notice) +gem 'nokogiri', '1.15.5' # TODO: New version need Ruby 3 gem 'mobility', '~> 1.2', '>= 1.2.9' -gem 'devise', '4.4.3' # TODO: version 4.4.3 work under <=Rails 5.3 and <=Ruby 2.6 +gem 'devise', '4.7.3' # TODO: need upgrade to 4.8+ when upgrade to rails 6.1 gem 'cancancan', '2.3.0' # TODO, can upgrade to 3.0 after Rails 6 gem 'ahoy_matey', '4.2.1' # TODO: latest 5.0.2. Can't upgrade to 5.0 until upgrade to Rails 6 gem 'uuidtools', '~> 2.2' # For Ahoy. (https://github.com/ankane/ahoy/blob/v2.2.1/docs/Ahoy-2-Upgrade.md#activerecordstore) @@ -43,9 +46,8 @@ gem 'wicked', '1.3.4' gem 'groupdate', '6.2.1' # TODO: can upgrade after rails 6.1 and newer ruby 3 gem 'rubyzip', '~> 2.3', '>= 2.3.2' -gem 'responders', '~> 2.0' # https://guides.rubyonrails.org/v4.2/upgrading_ruby_on_rails.html#responders +gem 'responders', '~> 3.1', '>= 3.1.1' # https://guides.rubyonrails.org/v4.2/upgrading_ruby_on_rails.html#responders -# TODO: need Sidekiq 6 and Rails 6, before we can migrate worker to job, due to sidekiq_options (https://github.com/sidekiq/sidekiq/issues/4281) gem 'sidekiq', '< 7' # TODO, latest is 7 gem 'sidekiq-status', '~> 3.0', '>= 3.0.3' gem 'sidekiq-unique-jobs', '7.1.31' # TODO: can upgrade to latest when sidekiq upgrade to 7 @@ -71,24 +73,24 @@ gem 'rails-observers', '~> 0.1.5' # A feature that removed from core in Rails 4. gem 'strong_migrations', '~> 1.7' -# Use ActiveModel has_secure_password +# Use Active Model has_secure_password # gem 'bcrypt', '~> 3.1.7' # Use ActiveStorage variant # gem 'mini_magick', '~> 4.8' # Reduces boot times through caching; required in config/boot.rb -gem 'bootsnap', '>= 1.1.0', require: false +gem 'bootsnap', '>= 1.4.2', require: false # To use Jbuilder templates for JSON -# gem 'jbuilder', '~> 2.5' +# gem 'jbuilder', '~> 2.7' gem 'rest-client', '1.8.0', require: false # TODO, should upgrade for better compatibility with newer Ruby but breaking change. Seems not many place using it, worth a try. group :development do # Access an interactive console on exception pages or by calling 'console' anywhere in the code. gem 'web-console', '>= 3.3.0' - gem 'listen', '>= 3.0.5', '< 3.2' + gem 'listen', '~> 3.2' # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'spring' gem 'spring-watcher-listen', '~> 2.0.0' @@ -132,8 +134,8 @@ group :test do # Adds support for Capybara system testing and selenium driver gem 'capybara', '>= 2.15' gem 'selenium-webdriver' - # Easy installation and use of chromedriver to run system tests with Chrome - gem 'chromedriver-helper' + # Easy installation and use of web drivers to run system tests with browsers + gem 'webdrivers' gem 'rails-controller-testing' gem "codeclimate-test-reporter", '0.1.1', require: nil # TODO, should be removed diff --git a/Gemfile.lock b/Gemfile.lock index 8d8da5958..0a5bf8350 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,55 +2,69 @@ GEM remote: https://rubygems.org/ specs: Ascii85 (1.0.3) - actioncable (5.2.8.1) - actionpack (= 5.2.8.1) + actioncable (6.0.6.1) + actionpack (= 6.0.6.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailer (5.2.8.1) - actionpack (= 5.2.8.1) - actionview (= 5.2.8.1) - activejob (= 5.2.8.1) + actionmailbox (6.0.6.1) + actionpack (= 6.0.6.1) + activejob (= 6.0.6.1) + activerecord (= 6.0.6.1) + activestorage (= 6.0.6.1) + activesupport (= 6.0.6.1) + mail (>= 2.7.1) + actionmailer (6.0.6.1) + actionpack (= 6.0.6.1) + actionview (= 6.0.6.1) + activejob (= 6.0.6.1) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (5.2.8.1) - actionview (= 5.2.8.1) - activesupport (= 5.2.8.1) + actionpack (6.0.6.1) + actionview (= 6.0.6.1) + activesupport (= 6.0.6.1) rack (~> 2.0, >= 2.0.8) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.2) + rails-html-sanitizer (~> 1.0, >= 1.2.0) actionpack-action_caching (1.2.2) actionpack (>= 4.0.0) actionpack-page_caching (1.2.4) actionpack (>= 4.0.0) - actionview (5.2.8.1) - activesupport (= 5.2.8.1) + actiontext (6.0.6.1) + actionpack (= 6.0.6.1) + activerecord (= 6.0.6.1) + activestorage (= 6.0.6.1) + activesupport (= 6.0.6.1) + nokogiri (>= 1.8.5) + actionview (6.0.6.1) + activesupport (= 6.0.6.1) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.3) + rails-html-sanitizer (~> 1.1, >= 1.2.0) active-model-adapter-source (2.1.1) ember-data-source (>= 1.13, < 3.0) active_model_serializers (0.8.4) activemodel (>= 3.0) - activejob (5.2.8.1) - activesupport (= 5.2.8.1) + activejob (6.0.6.1) + activesupport (= 6.0.6.1) globalid (>= 0.3.6) - activemodel (5.2.8.1) - activesupport (= 5.2.8.1) - activerecord (5.2.8.1) - activemodel (= 5.2.8.1) - activesupport (= 5.2.8.1) - arel (>= 9.0) - activestorage (5.2.8.1) - actionpack (= 5.2.8.1) - activerecord (= 5.2.8.1) - marcel (~> 1.0.0) - activesupport (5.2.8.1) + activemodel (6.0.6.1) + activesupport (= 6.0.6.1) + activerecord (6.0.6.1) + activemodel (= 6.0.6.1) + activesupport (= 6.0.6.1) + activestorage (6.0.6.1) + actionpack (= 6.0.6.1) + activejob (= 6.0.6.1) + activerecord (= 6.0.6.1) + marcel (~> 1.0) + activesupport (6.0.6.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) + zeitwerk (~> 2.2, >= 2.2.2) acts-as-taggable-on (8.1.0) activerecord (>= 5.0, < 6.2) addressable (2.8.6) @@ -67,9 +81,6 @@ GEM appsignal (1.3.3) rack thread_safe - archive-zip (0.12.0) - io-like (~> 0.3.0) - arel (9.0.0) ast (2.4.2) aws-eventstream (1.3.0) aws-sdk (2.11.632) @@ -92,7 +103,7 @@ GEM bcrypt_pbkdf (1.1.0) bigdecimal (3.1.6) bindex (0.8.1) - bootsnap (1.17.1) + bootsnap (1.18.1) msgpack (~> 1.2) bootstrap-sass (2.3.2.2) sass (~> 3.2) @@ -127,7 +138,7 @@ GEM capistrano (>= 3.9.0) capistrano-bundler sidekiq (>= 6.0) - capybara (3.36.0) + capybara (3.39.2) addressable matrix mini_mime (>= 0.1.3) @@ -145,17 +156,13 @@ GEM mini_mime (>= 0.1.3) ssrf_filter (~> 1.0) chartkick (4.2.1) - childprocess (4.1.0) - chromedriver-helper (2.1.1) - archive-zip (~> 0.10) - nokogiri (~> 1.8) chronic_duration (0.10.6) numerizer (~> 0.1.1) codeclimate-test-reporter (0.1.1) simplecov (>= 0.7.1, < 1.0.0) - coffee-rails (4.2.2) + coffee-rails (5.0.0) coffee-script (>= 2.2.0) - railties (>= 4.0.0) + railties (>= 5.2.0) coffee-script (2.4.1) coffee-script-source execjs @@ -177,17 +184,16 @@ GEM database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) date (3.3.4) - device_detector (1.0.7) - devise (4.4.3) + device_detector (1.1.2) + devise (4.7.3) bcrypt (~> 3.0) orm_adapter (~> 0.1) - railties (>= 4.1.0, < 6.0) + railties (>= 4.1.0) responders warden (~> 1.2.3) diff-lcs (1.5.0) docile (1.4.0) - domain_name (0.5.20190701) - unf (>= 0.0.5, < 1.0.0) + domain_name (0.6.20240107) dotenv (2.0.1) dotenv-rails (2.0.1) dotenv (= 2.0.1) @@ -252,12 +258,11 @@ GEM image_processing (1.12.2) mini_magick (>= 4.9.5, < 5) ruby-vips (>= 2.0.17, < 3) - inherited_resources (1.13.1) - actionpack (>= 5.2, < 7.1) - has_scope (~> 0.6) - railties (>= 5.2, < 7.1) - responders (>= 2, < 4) - io-like (0.3.1) + inherited_resources (1.14.0) + actionpack (>= 6.0) + has_scope (>= 0.6) + railties (>= 6.0) + responders (>= 2) jmespath (1.6.2) jquery-rails (4.6.0) rails-dom-testing (>= 1, < 3) @@ -280,12 +285,12 @@ GEM activerecord kaminari-core (= 1.2.2) kaminari-core (1.2.2) + language_server-protocol (3.17.0.3) launchy (2.4.3) addressable (~> 2.3) - listen (3.1.5) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - ruby_dep (~> 1.2) + listen (3.8.0) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) @@ -313,7 +318,7 @@ GEM activerecord activesupport nested_form (0.3.2) - net-imap (0.3.7) + net-imap (0.4.9.1) date net-protocol net-pop (0.1.2) @@ -361,25 +366,28 @@ GEM ruby-rc4 ttfunk (~> 1.0.3) public_suffix (5.0.4) - puma (3.12.6) + puma (4.3.12) + nio4r (~> 2.0) raabro (1.4.0) racc (1.7.3) rack (2.2.8) rack-cors (0.3.0) rack-test (2.1.0) rack (>= 1.3) - rails (5.2.8.1) - actioncable (= 5.2.8.1) - actionmailer (= 5.2.8.1) - actionpack (= 5.2.8.1) - actionview (= 5.2.8.1) - activejob (= 5.2.8.1) - activemodel (= 5.2.8.1) - activerecord (= 5.2.8.1) - activestorage (= 5.2.8.1) - activesupport (= 5.2.8.1) + rails (6.0.6.1) + actioncable (= 6.0.6.1) + actionmailbox (= 6.0.6.1) + actionmailer (= 6.0.6.1) + actionpack (= 6.0.6.1) + actiontext (= 6.0.6.1) + actionview (= 6.0.6.1) + activejob (= 6.0.6.1) + activemodel (= 6.0.6.1) + activerecord (= 6.0.6.1) + activestorage (= 6.0.6.1) + activesupport (= 6.0.6.1) bundler (>= 1.3.0) - railties (= 5.2.8.1) + railties (= 6.0.6.1) sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) @@ -389,16 +397,17 @@ GEM activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.5.0) - loofah (~> 2.19, >= 2.19.1) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) rails-observers (0.1.5) activemodel (>= 4.0) - railties (5.2.8.1) - actionpack (= 5.2.8.1) - activesupport (= 5.2.8.1) + railties (6.0.6.1) + actionpack (= 6.0.6.1) + activesupport (= 6.0.6.1) method_source rake (>= 0.8.7) - thor (>= 0.19.0, < 2.0) + thor (>= 0.20.3, < 2.0) rainbow (3.1.1) rake (13.1.0) rb-fsevent (0.11.2) @@ -412,9 +421,9 @@ GEM regexp_parser (2.9.0) request_store (1.5.1) rack (>= 1.4) - responders (2.4.1) - actionpack (>= 4.2.0, < 6.0) - railties (>= 4.2.0, < 6.0) + responders (3.1.1) + actionpack (>= 5.2) + railties (>= 5.2) rest-client (1.8.0) http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 3.0) @@ -443,46 +452,54 @@ GEM rspec-mocks (~> 3.10) rspec-support (~> 3.10) rspec-support (3.12.1) - rubocop (1.50.2) + rubocop (1.60.2) json (~> 2.3) + language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 3.2.0.0) + parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.28.0, < 2.0) + rubocop-ast (>= 1.30.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) rubocop-ast (1.30.0) parser (>= 3.2.1.0) - rubocop-capybara (2.18.0) + rubocop-capybara (2.20.0) + rubocop (~> 1.41) + rubocop-factory_bot (2.25.1) rubocop (~> 1.41) - rubocop-rails (2.19.1) + rubocop-rails (2.23.1) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) - rubocop-rspec (2.20.0) - rubocop (~> 1.33) + rubocop-ast (>= 1.30.0, < 2.0) + rubocop-rspec (2.26.1) + rubocop (~> 1.40) rubocop-capybara (~> 2.17) + rubocop-factory_bot (~> 2.22) ruby-progressbar (1.13.0) ruby-rc4 (0.1.5) ruby-vips (2.2.0) ffi (~> 1.12) - ruby_dep (1.5.0) rubyzip (2.3.2) safely_block (0.3.0) errbase (>= 0.1.1) sass (3.4.25) - sass-rails (5.1.0) - railties (>= 5.2.0) - sass (~> 3.1) - sprockets (>= 2.8, < 4.0) - sprockets-rails (>= 2.0, < 4.0) - tilt (>= 1.1, < 3) - selenium-webdriver (4.1.0) - childprocess (>= 0.5, < 5.0) + sass-rails (6.0.0) + sassc-rails (~> 2.1, >= 2.1.1) + sassc (2.4.0) + ffi (~> 1.9) + sassc-rails (2.1.2) + railties (>= 4.0.0) + sassc (>= 2.0) + sprockets (> 3.0) + sprockets-rails + tilt + selenium-webdriver (4.9.0) rexml (~> 3.2, >= 3.2.5) - rubyzip (>= 1.2.2) + rubyzip (>= 1.2.2, < 3.0) + websocket (~> 1.0) sidekiq (6.5.12) connection_pool (>= 2.2.5, < 3) rack (~> 2.0) @@ -548,18 +565,20 @@ GEM thread_safe (~> 0.1) uglifier (4.2.0) execjs (>= 0.3.0, < 3) - unf (0.1.4) - unf_ext - unf_ext (0.0.9.1) unicode-display_width (2.5.0) uuidtools (2.2.0) warden (1.2.9) rack (>= 2.0.9) - web-console (3.7.0) - actionview (>= 5.0) - activemodel (>= 5.0) + web-console (4.2.1) + actionview (>= 6.0.0) + activemodel (>= 6.0.0) bindex (>= 0.4.0) - railties (>= 5.0) + railties (>= 6.0.0) + webdrivers (5.3.1) + nokogiri (~> 1.6) + rubyzip (>= 1.3.0) + selenium-webdriver (~> 4.0, < 4.11) + websocket (1.2.10) websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) @@ -568,6 +587,7 @@ GEM wkhtmltopdf-binary (0.12.6.6) xpath (3.2.0) nokogiri (~> 1.8) + zeitwerk (2.6.12) PLATFORMS ruby @@ -582,7 +602,7 @@ DEPENDENCIES appsignal (= 1.3.3) aws-sdk (~> 2) bcrypt_pbkdf (= 1.1.0) - bootsnap (>= 1.1.0) + bootsnap (>= 1.4.2) bootstrap-sass (= 2.3.2.2) brightbox (= 2.3.9) byebug @@ -598,13 +618,12 @@ DEPENDENCIES capybara (>= 2.15) carrierwave (= 2.2.5) chartkick (= 4.2.1) - chromedriver-helper codeclimate-test-reporter (= 0.1.1) - coffee-rails (~> 4.2) + coffee-rails (~> 5.0) coveralls (= 0.7.1) dalli (= 2.7.10) database_cleaner (~> 2.0, >= 2.0.2) - devise (= 4.4.3) + devise (= 4.7.3) dotenv-rails (= 2.0.1) ed25519 (= 1.2.4) ember-data-source (= 1.13.0) @@ -616,16 +635,16 @@ DEPENDENCIES groupdate (= 6.2.1) handlebars-source (= 1.0.12) httparty (~> 0.21.0) - inherited_resources (= 1.13.1) + inherited_resources (~> 1.14) jslint_on_rails (= 1.1.1) json_spec (= 1.1.5) kaminari (~> 1.2, >= 1.2.2) launchy (= 2.4.3) - listen (>= 3.0.5, < 3.2) + listen (~> 3.2) mobility (~> 1.2, >= 1.2.9) nested-hstore (~> 0.1.2) nested_form (~> 0.3.2) - nokogiri (~> 1.15, >= 1.15.5) + nokogiri (= 1.15.5) oj (~> 3.16, >= 3.16.3) paper_trail (= 12.3.0) pdfkit (~> 0.8.7.3) @@ -633,22 +652,22 @@ DEPENDENCIES pg_array_parser (~> 0.0.9) pg_search (~> 2.3, >= 2.3.6) prawn (= 0.13.2) - puma (~> 3.11) + puma (~> 4.1) rack-cors (= 0.3.0) - rails (= 5.2.8.1) + rails (= 6.0.6.1) rails-controller-testing rails-observers (~> 0.1.5) rbnacl (= 4.0.2) rbnacl-libsodium (= 1.0.16) request_store (~> 1.5, >= 1.5.1) - responders (~> 2.0) + responders (~> 3.1, >= 3.1.1) rest-client (= 1.8.0) rspec-collection_matchers (~> 1.2, >= 1.2.1) rspec-rails (= 5.1.2) rubocop-rails rubocop-rspec rubyzip (~> 2.3, >= 2.3.2) - sass-rails (~> 5.0) + sass-rails (>= 6) selenium-webdriver sidekiq (< 7) sidekiq-cron (~> 1.12) @@ -659,12 +678,15 @@ DEPENDENCIES slackistrano (= 0.1.9) spring spring-watcher-listen (~> 2.0.0) + sprockets (= 3.7.2) + sprockets-rails strong_migrations (~> 1.7) susy (~> 2.2, >= 2.2.14) test-unit (= 3.1.5) uglifier (>= 1.3.0) uuidtools (~> 2.2) web-console (>= 3.3.0) + webdrivers wicked (= 1.3.4) wkhtmltopdf-binary (~> 0.12.6.6) diff --git a/bin/bundle b/bin/bundle deleted file mode 100755 index f19acf5b5..000000000 --- a/bin/bundle +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env ruby -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) -load Gem.bin_path('bundler', 'bundle') diff --git a/bin/setup b/bin/setup index 94fd4d797..5853b5ea8 100755 --- a/bin/setup +++ b/bin/setup @@ -1,6 +1,5 @@ #!/usr/bin/env ruby require 'fileutils' -include FileUtils # path to your application root. APP_ROOT = File.expand_path('..', __dir__) @@ -9,24 +8,25 @@ def system!(*args) system(*args) || abort("\n== Command #{args} failed ==") end -chdir APP_ROOT do - # This script is a starting point to setup your application. +FileUtils.chdir APP_ROOT do + # This script is a way to setup or update your development environment automatically. + # This script is idempotent, so that you can run it at anytime and get an expectable outcome. # Add necessary setup steps to this file. puts '== Installing dependencies ==' system! 'gem install bundler --conservative' system('bundle check') || system!('bundle install') - # Install JavaScript dependencies if using Yarn + # Install JavaScript dependencies # system('bin/yarn') # puts "\n== Copying sample files ==" # unless File.exist?('config/database.yml') - # cp 'config/database.yml.sample', 'config/database.yml' + # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' # end puts "\n== Preparing database ==" - system! 'bin/rails db:setup' + system! 'bin/rails db:prepare' puts "\n== Removing old logs and tempfiles ==" system! 'bin/rails log:clear tmp:clear' diff --git a/bin/spring b/bin/spring deleted file mode 100755 index 7fe232c3a..000000000 --- a/bin/spring +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env ruby - -# This file loads spring without using Bundler, in order to be fast. -# It gets overwritten when you run the `spring binstub` command. - -unless defined?(Spring) - require 'rubygems' - require 'bundler' - - if (match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m)) - Gem.paths = { 'GEM_PATH' => [Bundler.bundle_path.to_s, *Gem.path].uniq.join(Gem.path_separator) } - gem 'spring', match[1] - require 'spring/binstub' - end -end diff --git a/bin/update b/bin/update deleted file mode 100755 index 58bfaed51..000000000 --- a/bin/update +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env ruby -require 'fileutils' -include FileUtils - -# path to your application root. -APP_ROOT = File.expand_path('..', __dir__) - -def system!(*args) - system(*args) || abort("\n== Command #{args} failed ==") -end - -chdir APP_ROOT do - # This script is a way to update your development environment automatically. - # Add necessary update steps to this file. - - puts '== Installing dependencies ==' - system! 'gem install bundler --conservative' - system('bundle check') || system!('bundle install') - - # Install JavaScript dependencies if using Yarn - # system('bin/yarn') - - puts "\n== Updating database ==" - system! 'bin/rails db:migrate' - - puts "\n== Removing old logs and tempfiles ==" - system! 'bin/rails log:clear tmp:clear' - - puts "\n== Restarting application server ==" - system! 'bin/rails restart' -end diff --git a/config/application.rb b/config/application.rb index 1437b1d05..18fac72ea 100644 --- a/config/application.rb +++ b/config/application.rb @@ -10,7 +10,7 @@ module SAPI class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 5.2 + config.load_defaults 6.0 # Settings in config/environments/* take precedence over those specified here. # Application configuration can go into files in config/initializers diff --git a/config/cable.yml b/config/cable.yml index c1fe5018d..5fb9fec5a 100644 --- a/config/cable.yml +++ b/config/cable.yml @@ -2,7 +2,7 @@ development: adapter: async test: - adapter: async + adapter: test production: adapter: redis diff --git a/config/environments/development.rb b/config/environments/development.rb index 27fbdd214..bd2b4a5de 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -16,6 +16,7 @@ # Run rails dev:cache to toggle caching. if Rails.root.join('tmp', 'caching-dev.txt').exist? config.action_controller.perform_caching = true + config.action_controller.enable_fragment_cache_logging = true config.cache_store = :memory_store config.public_file_server.headers = { @@ -27,7 +28,7 @@ config.cache_store = :null_store end - # Store uploaded files on the local file system (see config/storage.yml for options) + # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local # Don't care if the mailer can't send. @@ -52,7 +53,7 @@ # Suppress logger output for asset requests. config.assets.quiet = true - # Raises error for missing translations + # Raises error for missing translations. # config.action_view.raise_on_missing_translations = true # Use an evented file watcher to asynchronously detect changes in source code, diff --git a/config/environments/production.rb b/config/environments/production.rb index 23defb097..90c218765 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -24,13 +24,12 @@ # Compress JavaScripts and CSS. config.assets.js_compressor = :uglifier + # Compress CSS using a preprocessor. # config.assets.css_compressor = :sass # Do not fallback to assets pipeline if a precompiled asset is missed. config.assets.compile = false - # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb - # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.action_controller.asset_host = 'http://assets.example.com' @@ -38,10 +37,10 @@ # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX - # Store uploaded files on the local file system (see config/storage.yml for options) + # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local - # Mount Action Cable outside main process or domain + # Mount Action Cable outside main process or domain. # config.action_cable.mount_path = nil # config.action_cable.url = 'wss://example.com/cable' # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] @@ -59,9 +58,10 @@ # Use a different cache store in production. config.cache_store = :mem_cache_store - # Use a real queuing backend for Active Job (and separate queues per environment) + # Use a real queuing backend for Active Job (and separate queues per environment). # config.active_job.queue_adapter = :resque - # config.active_job.queue_name_prefix = "sapi_#{Rails.env}" + # config.active_job.queue_name_prefix = "sapi_production" + config.action_mailer.perform_caching = false # Ignore bad email addresses and do not raise email delivery errors. @@ -92,6 +92,27 @@ # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false + # Inserts middleware to perform automatic connection switching. + # The `database_selector` hash is used to pass options to the DatabaseSelector + # middleware. The `delay` is used to determine how long to wait after a write + # to send a subsequent read to the primary. + # + # The `database_resolver` class is used by the middleware to determine which + # database is appropriate to use based on the time delay. + # + # The `database_resolver_context` class is used by the middleware to set + # timestamps for the last write to the primary. The resolver uses the context + # class timestamps to determine how long to wait before reading from the + # replica. + # + # By default Rails will store a last write timestamp in the session. The + # DatabaseSelector middleware is designed as such you can define your own + # strategy for connection switching and pass that into the middleware through + # these configuration options. + # config.active_record.database_selector = { delay: 2.seconds } + # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver + # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session + # Custom ember settings config.ember.variant = :production diff --git a/config/environments/test.rb b/config/environments/test.rb index 5164afd1d..9579685c1 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,11 +1,13 @@ +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. - # The test environment is used exclusively to run your application's - # test suite. You never need to work with it otherwise. Remember that - # your test database is "scratch space" for the test suite and is wiped - # and recreated between test runs. Don't rely on the data there! - config.cache_classes = true + config.cache_classes = false + config.action_view.cache_template_loading = true # Do not eager load code on boot. This avoids loading your whole application # just for the purpose of running a single test. If you are using a tool that @@ -22,6 +24,7 @@ # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false + config.cache_store = :null_store # Raise exceptions instead of rendering exception templates. config.action_dispatch.show_exceptions = false @@ -29,7 +32,7 @@ # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false - # Store uploaded files on the local file system in a temporary directory + # Store uploaded files on the local file system in a temporary directory. config.active_storage.service = :test config.action_mailer.perform_caching = false @@ -42,12 +45,9 @@ # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr - # Raises error for missing translations + # Raises error for missing translations. # config.action_view.raise_on_missing_translations = true - # Custom cache settings - config.cache_store = :null_store - # Custom ember settings config.ember.variant = :development diff --git a/config/initializers/0_new_framework_defaults_5_2.rb b/config/initializers/0_new_framework_defaults_5_2.rb deleted file mode 100644 index 4c3a4be4c..000000000 --- a/config/initializers/0_new_framework_defaults_5_2.rb +++ /dev/null @@ -1,38 +0,0 @@ -# Be sure to restart your server when you modify this file. -# -# This file contains migration options to ease your Rails 5.2 upgrade. -# -# Once upgraded flip defaults one by one to migrate to the new default. -# -# Read the Guide for Upgrading Ruby on Rails for more info on each option. - -# Make Active Record use stable #cache_key alongside new #cache_version method. -# This is needed for recyclable cache keys. -Rails.application.config.active_record.cache_versioning = true - -# Use AES-256-GCM authenticated encryption for encrypted cookies. -# Also, embed cookie expiry in signed or encrypted cookies for increased security. -# -# This option is not backwards compatible with earlier Rails versions. -# It's best enabled when your entire app is migrated and stable on 5.2. -# -# Existing cookies will be converted on read then written with the new scheme. -Rails.application.config.action_dispatch.use_authenticated_cookie_encryption = true - -# Use AES-256-GCM authenticated encryption as default cipher for encrypting messages -# instead of AES-256-CBC, when use_authenticated_message_encryption is set to true. -Rails.application.config.active_support.use_authenticated_message_encryption = true - -# Add default protection from forgery to ActionController::Base instead of in -# ApplicationController. -Rails.application.config.action_controller.default_protect_from_forgery = true - -# Store boolean values are in sqlite3 databases as 1 and 0 instead of 't' and -# 'f' after migrating old data. -# Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true - -# Use SHA-1 instead of MD5 to generate non-sensitive digests, such as the ETag header. -Rails.application.config.active_support.use_sha1_digests = true - -# Make `form_with` generate id attributes for any generated HTML tags. -Rails.application.config.action_view.form_with_generates_ids = true diff --git a/config/initializers/0_new_framework_defaults_6_0.rb b/config/initializers/0_new_framework_defaults_6_0.rb new file mode 100644 index 000000000..92240ef5f --- /dev/null +++ b/config/initializers/0_new_framework_defaults_6_0.rb @@ -0,0 +1,45 @@ +# Be sure to restart your server when you modify this file. +# +# This file contains migration options to ease your Rails 6.0 upgrade. +# +# Once upgraded flip defaults one by one to migrate to the new default. +# +# Read the Guide for Upgrading Ruby on Rails for more info on each option. + +# Don't force requests from old versions of IE to be UTF-8 encoded. +# Rails.application.config.action_view.default_enforce_utf8 = false + +# Embed purpose and expiry metadata inside signed and encrypted +# cookies for increased security. +# +# This option is not backwards compatible with earlier Rails versions. +# It's best enabled when your entire app is migrated and stable on 6.0. +# Rails.application.config.action_dispatch.use_cookies_with_metadata = true + +# Change the return value of `ActionDispatch::Response#content_type` to Content-Type header without modification. +# Rails.application.config.action_dispatch.return_only_media_type_on_content_type = false + +# Return false instead of self when enqueuing is aborted from a callback. +# Rails.application.config.active_job.return_false_on_aborted_enqueue = true + +# Send Active Storage analysis and purge jobs to dedicated queues. +# Rails.application.config.active_storage.queues.analysis = :active_storage_analysis +# Rails.application.config.active_storage.queues.purge = :active_storage_purge + +# When assigning to a collection of attachments declared via `has_many_attached`, replace existing +# attachments instead of appending. Use #attach to add new attachments without replacing existing ones. +# Rails.application.config.active_storage.replace_on_assign_to_many = true + +# Use ActionMailer::MailDeliveryJob for sending parameterized and normal mail. +# +# The default delivery jobs (ActionMailer::Parameterized::DeliveryJob, ActionMailer::DeliveryJob), +# will be removed in Rails 6.1. This setting is not backwards compatible with earlier Rails versions. +# If you send mail in the background, job workers need to have a copy of +# MailDeliveryJob to ensure all delivery jobs are processed properly. +# Make sure your entire app is migrated and stable on 6.0 before using this setting. +# Rails.application.config.action_mailer.delivery_job = "ActionMailer::MailDeliveryJob" + +# Enable the same cache key to be reused when the object being cached of type +# `ActiveRecord::Relation` changes by moving the volatile information (max updated at and count) +# of the relation's cache key into the cache version to support recycling cache key. +# Rails.application.config.active_record.collection_cache_versioning = true diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb index d3bcaa5ec..35d0f26fc 100644 --- a/config/initializers/content_security_policy.rb +++ b/config/initializers/content_security_policy.rb @@ -11,6 +11,8 @@ # policy.object_src :none # policy.script_src :self, :https # policy.style_src :self, :https +# # If you are using webpack-dev-server then specify webpack-dev-server host +# policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development? # # Specify URI for violation reports # # policy.report_uri "/csp-violation-report-endpoint" @@ -19,6 +21,9 @@ # If you are using UJS then enable automatic nonce generation # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } +# Set the nonce only to specific directives +# Rails.application.config.content_security_policy_nonce_directives = %w(script-src) + # Report CSP violations to a specified URI # For further information see the following documentation: # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only diff --git a/config/puma.rb b/config/puma.rb index b2102072b..5ed443774 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -4,8 +4,9 @@ # the maximum value specified for Puma. Default is set to 5 threads for minimum # and maximum; this matches the default thread size of Active Record. # -threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } -threads threads_count, threads_count +max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } +min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } +threads min_threads_count, max_threads_count # Specifies the `port` that Puma will listen on to receive requests; default is 3000. # @@ -19,7 +20,7 @@ pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } # Specifies the number of `workers` to boot in clustered mode. -# Workers are forked webserver processes. If using threads and workers together +# Workers are forked web server processes. If using threads and workers together # the concurrency of the application would be max `threads` * `workers`. # Workers do not work on JRuby or Windows (both of which do not support # processes). diff --git a/config/spring.rb b/config/spring.rb index 9fa7863f9..db5bf1307 100644 --- a/config/spring.rb +++ b/config/spring.rb @@ -1,6 +1,6 @@ -%w[ - .ruby-version - .rbenv-vars - tmp/restart.txt - tmp/caching-dev.txt -].each { |path| Spring.watch(path) } +Spring.watch( + ".ruby-version", + ".rbenv-vars", + "tmp/restart.txt", + "tmp/caching-dev.txt" +) From b89aa2ca7f3fe27957b62a9a6e7c2f92ad68c84e Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Wed, 31 Jan 2024 11:29:28 +0000 Subject: [PATCH 129/241] rollback to classic autoloader for now --- config/application.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/application.rb b/config/application.rb index 18fac72ea..16faccc5a 100644 --- a/config/application.rb +++ b/config/application.rb @@ -11,6 +11,7 @@ module SAPI class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 6.0 + config.autoloader = :classic # TODO https://guides.rubyonrails.org/classic_to_zeitwerk_howto.html # Settings in config/environments/* take precedence over those specified here. # Application configuration can go into files in config/initializers From 6184763c70066e5de26ebe381cef72fd769ce802 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Wed, 31 Jan 2024 11:29:46 +0000 Subject: [PATCH 130/241] fix rspec for Rails 6 --- ...nomenclature_change_output_reassignments_table.rb | 2 -- spec/controllers/admin/exports_controller_spec.rb | 12 ++++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/db/migrate/20141113153137_create_nomenclature_change_output_reassignments_table.rb b/db/migrate/20141113153137_create_nomenclature_change_output_reassignments_table.rb index d9bf4c32d..8d73b8845 100644 --- a/db/migrate/20141113153137_create_nomenclature_change_output_reassignments_table.rb +++ b/db/migrate/20141113153137_create_nomenclature_change_output_reassignments_table.rb @@ -8,8 +8,6 @@ def change t.text :note_en t.integer :created_by_id, :null => false t.integer :updated_by_id, :null => false - t.datetime :created_at, :null => false - t.datetime :updated_at, :null => false t.text :note_es t.text :note_fr t.text :internal_note diff --git a/spec/controllers/admin/exports_controller_spec.rb b/spec/controllers/admin/exports_controller_spec.rb index ee3225f0c..8d3cdd61d 100644 --- a/spec/controllers/admin/exports_controller_spec.rb +++ b/spec/controllers/admin/exports_controller_spec.rb @@ -20,7 +20,7 @@ allow_any_instance_of(Species::TaxonConceptsNamesExport).to receive(:public_file_name).and_return('taxon_concepts_names.csv') get :download, params: { :data_type => "Names" } expect(response.content_type).to eq("text/csv") - expect(response.headers["Content-Disposition"]).to eq("attachment; filename=\"taxon_concepts_names.csv\"") + expect(response.headers["Content-Disposition"]).to eq("attachment; filename=\"taxon_concepts_names.csv\"; filename*=UTF-8''taxon_concepts_names.csv") end it "redirects when no results" do get :download, params: { :data_type => "Names" } @@ -33,7 +33,7 @@ allow_any_instance_of(Species::TaxonConceptsNamesExport).to receive(:public_file_name).and_return('taxon_concepts_names.csv') get :download, params: { :data_type => "Names", :filters => { :taxonomy => 'CITES_EU' } } expect(response.content_type).to eq("text/csv") - expect(response.headers["Content-Disposition"]).to eq("attachment; filename=\"taxon_concepts_names.csv\"") + expect(response.headers["Content-Disposition"]).to eq("attachment; filename=\"taxon_concepts_names.csv\"; filename*=UTF-8''taxon_concepts_names.csv") end it "redirects when no results" do get :download, params: { :data_type => "Names", :filters => { :taxonomy => 'CITES_EU' } } @@ -46,7 +46,7 @@ allow_any_instance_of(Species::TaxonConceptsNamesExport).to receive(:public_file_name).and_return('taxon_concepts_names.csv') get :download, params: { :data_type => "Names", :filters => { :taxonomy => 'CMS' } } expect(response.content_type).to eq("text/csv") - expect(response.headers["Content-Disposition"]).to eq("attachment; filename=\"taxon_concepts_names.csv\"") + expect(response.headers["Content-Disposition"]).to eq("attachment; filename=\"taxon_concepts_names.csv\"; filename*=UTF-8''taxon_concepts_names.csv") end it "redirects when no results" do get :download, params: { :data_type => "Names", :filters => { :taxonomy => 'CMS' } } @@ -65,7 +65,7 @@ allow_any_instance_of(Species::TaxonConceptsDistributionsExport).to receive(:public_file_name).and_return('taxon_concepts_distributions.csv') get :download, params: { :data_type => "Distributions" } expect(response.content_type).to eq("text/csv") - expect(response.headers["Content-Disposition"]).to eq("attachment; filename=\"taxon_concepts_distributions.csv\"") + expect(response.headers["Content-Disposition"]).to eq("attachment; filename=\"taxon_concepts_distributions.csv\"; filename*=UTF-8''taxon_concepts_distributions.csv") end it "redirects when no results" do get :download, params: { :data_type => "Distributions" } @@ -79,7 +79,7 @@ allow_any_instance_of(Species::TaxonConceptsDistributionsExport).to receive(:public_file_name).and_return('taxon_concepts_distributions.csv') get :download, params: { :data_type => "Distributions", :filters => { :taxonomy => 'CITES_EU' } } expect(response.content_type).to eq("text/csv") - expect(response.headers["Content-Disposition"]).to eq("attachment; filename=\"taxon_concepts_distributions.csv\"") + expect(response.headers["Content-Disposition"]).to eq("attachment; filename=\"taxon_concepts_distributions.csv\"; filename*=UTF-8''taxon_concepts_distributions.csv") end it "redirects when no results" do get :download, params: { :data_type => "Distributions", :filters => { :taxonomy => 'CITES_EU' } } @@ -93,7 +93,7 @@ allow_any_instance_of(Species::TaxonConceptsDistributionsExport).to receive(:public_file_name).and_return('taxon_concepts_distributions.csv') get :download, params: { :data_type => "Distributions", :filters => { :taxonomy => 'CMS' } } expect(response.content_type).to eq("text/csv") - expect(response.headers["Content-Disposition"]).to eq("attachment; filename=\"taxon_concepts_distributions.csv\"") + expect(response.headers["Content-Disposition"]).to eq("attachment; filename=\"taxon_concepts_distributions.csv\"; filename*=UTF-8''taxon_concepts_distributions.csv") end it "redirects when no results" do get :download, params: { :data_type => "Distributions", :filters => { :taxonomy => 'CMS' } } From 2419d932f929e36e469f53c377569e5ace41b1af Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Wed, 31 Jan 2024 11:47:23 +0000 Subject: [PATCH 131/241] fix rspec for Rails 6 --- spec/controllers/cites_trade/exports_controller_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/controllers/cites_trade/exports_controller_spec.rb b/spec/controllers/cites_trade/exports_controller_spec.rb index 37338609c..e53576f88 100644 --- a/spec/controllers/cites_trade/exports_controller_spec.rb +++ b/spec/controllers/cites_trade/exports_controller_spec.rb @@ -18,7 +18,7 @@ allow(Trade::TradeDataDownloadLogger).to receive(:organization_from).and_return("UNEP-WCMC") get :download, params: { :filters => { :report_type => :comptab } } expect(response.content_type).to eq("text/csv") - expect(response.headers["Content-Disposition"]).to eq("attachment; filename=\"shipments.csv\"") + expect(response.headers["Content-Disposition"]).to eq("attachment; filename=\"shipments.csv\"; filename*=UTF-8''shipments.csv") end it "logs download information from public interface to the TradeDataDownload model" do create(:shipment) From 624af4d5488676eaea5a48e1644a3c61c579f94d Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Wed, 31 Jan 2024 14:14:58 +0000 Subject: [PATCH 132/241] Time as_json now convert the value to String. --- app/models/checklist/timeline_event.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/checklist/timeline_event.rb b/app/models/checklist/timeline_event.rb index 20f6c1b3f..d0f73825c 100644 --- a/app/models/checklist/timeline_event.rb +++ b/app/models/checklist/timeline_event.rb @@ -15,6 +15,8 @@ class Checklist::TimelineEvent #:hash_ann_parent_symbol e.g. CoP15 #:pos - position (%) def initialize(options) + options[:effective_at] = Time.zone.parse(options[:effective_at]) if options[:effective_at].is_a?(String) + # if it is an auto-inserted deletion it won't have an id id = options[:id] || ( (options[:species_listing_id] << 16) + From 2cd90e84bc0955c5962e82d827b3d9ef21de73b3 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Wed, 31 Jan 2024 14:33:23 +0000 Subject: [PATCH 133/241] DEPRECATION WARNING: update_attributes is deprecated and will be removed from Rails 6.1 (please, use update instead) --- .../admin/taxon_concept_comments_controller.rb | 2 +- app/controllers/admin/users_controller.rb | 2 +- app/controllers/trade/sandbox_shipments_controller.rb | 2 +- app/controllers/trade/shipments_controller.rb | 2 +- app/controllers/trade/validation_errors_controller.rb | 2 +- app/models/event.rb | 4 ++-- app/models/iucn_mapping_manager.rb | 2 +- app/models/nomenclature_change/full_reassignment.rb | 6 +++--- .../nomenclature_change/input_taxon_concept_processor.rb | 2 +- .../to_accepted_name_transformation.rb | 2 +- .../nomenclature_change/to_synonym_transformation.rb | 2 +- app/models/trade/annual_report_upload.rb | 4 ++-- app/models/trade/validation_rule.rb | 2 +- app/workers/changes_history_generator_worker.rb | 2 +- app/workers/submission_worker.rb | 6 +++--- lib/tasks/import_countries.rake | 2 +- lib/tasks/import_orchids_T_to_A.rake | 2 +- lib/tasks/import_trade_codes.rake | 8 +++++--- lib/tasks/import_trade_names.rake | 5 ++--- lib/tasks/trade_db_resolve_ip_to_country.rake | 2 +- .../trade/annual_report_uploads_controller_spec.rb | 2 +- spec/models/designation_spec.rb | 8 ++++---- spec/models/document_search_spec.rb | 8 ++++---- spec/models/document_spec.rb | 4 ++-- spec/models/geo_entity_search_spec.rb | 2 +- spec/models/nomenclature_change/split/constructor_spec.rb | 2 +- spec/models/taxonomy_spec.rb | 4 ++-- spec/models/trade/inclusion_validation_rule_spec.rb | 6 +++--- spec/models/trade/sandbox_template_spec.rb | 2 +- spec/models/trade/validation_rule_spec.rb | 6 +++--- 30 files changed, 53 insertions(+), 52 deletions(-) diff --git a/app/controllers/admin/taxon_concept_comments_controller.rb b/app/controllers/admin/taxon_concept_comments_controller.rb index 7e990df1c..1c0c2becc 100644 --- a/app/controllers/admin/taxon_concept_comments_controller.rb +++ b/app/controllers/admin/taxon_concept_comments_controller.rb @@ -23,7 +23,7 @@ def create def update @taxon_concept = TaxonConcept.find(params[:taxon_concept_id]) @comment = @taxon_concept.comments.find(params[:id]) - @comment.update_attributes(comment_params) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + @comment.update(comment_params) redirect_to admin_taxon_concept_comments_url(@taxon_concept), notice: 'Operation succeeded' end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 6da5fcc65..910704e6a 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -21,7 +21,7 @@ def update if user_params[:password].blank? @user.update_without_password(user_params) else - @user.update_attributes(user_params) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + @user.update(user_params) end respond_to do |format| format.js { diff --git a/app/controllers/trade/sandbox_shipments_controller.rb b/app/controllers/trade/sandbox_shipments_controller.rb index 1620fd683..c581d2cb3 100644 --- a/app/controllers/trade/sandbox_shipments_controller.rb +++ b/app/controllers/trade/sandbox_shipments_controller.rb @@ -16,7 +16,7 @@ def update aru = Trade::AnnualReportUpload.find(params[:annual_report_upload_id]) sandbox_klass = Trade::SandboxTemplate.ar_klass(aru.sandbox.table_name) @sandbox_shipment = sandbox_klass.find(params[:id]) - @sandbox_shipment.update_attributes(sandbox_shipment_params) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + @sandbox_shipment.update(sandbox_shipment_params) head :no_content end diff --git a/app/controllers/trade/shipments_controller.rb b/app/controllers/trade/shipments_controller.rb index 87b7f13b6..62b3b8d7a 100644 --- a/app/controllers/trade/shipments_controller.rb +++ b/app/controllers/trade/shipments_controller.rb @@ -20,7 +20,7 @@ def create def update @shipment = Trade::Shipment.find(params[:id]) update_params = populate_accepted_taxon_concept(shipment_params) - if @shipment.update_attributes(update_params) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + if @shipment.update(update_params) render :json => @shipment, :status => :ok else render :json => { "errors" => @shipment.errors }, :status => :unprocessable_entity diff --git a/app/controllers/trade/validation_errors_controller.rb b/app/controllers/trade/validation_errors_controller.rb index 6a14e82e1..f08dc83b7 100644 --- a/app/controllers/trade/validation_errors_controller.rb +++ b/app/controllers/trade/validation_errors_controller.rb @@ -8,7 +8,7 @@ def show def update @validation_error = Trade::ValidationError.find(params[:id]) - if @validation_error.update_attributes(validation_error_params) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + if @validation_error.update(validation_error_params) render json: @validation_error, status: :ok else render json: { 'errors' => @validation_error.errors }, diff --git a/app/models/event.rb b/app/models/event.rb index 1d4c5731e..d2ed69491 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -109,11 +109,11 @@ def self.search(query) end def activate! - update_attributes(:is_current => true) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + update(:is_current => true) end def deactivate! - update_attributes(:is_current => false) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + update(:is_current => false) end protected diff --git a/app/models/iucn_mapping_manager.rb b/app/models/iucn_mapping_manager.rb index c847d1d5a..9e3506750 100644 --- a/app/models/iucn_mapping_manager.rb +++ b/app/models/iucn_mapping_manager.rb @@ -42,7 +42,7 @@ def map_taxon_concept(taxon_concept, map, data) begin match = data["result"].first puts "#{taxon_concept.full_name} #{taxon_concept.author_year} <=> #{match["scientific_name"]} #{match["authority"]}" - map.update_attributes( # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + map.update( :iucn_taxon_name => match['scientific_name'], :iucn_taxon_id => match['taxonid'], :iucn_author => match['authority'], diff --git a/app/models/nomenclature_change/full_reassignment.rb b/app/models/nomenclature_change/full_reassignment.rb index 88ea5aa7e..345f56f63 100644 --- a/app/models/nomenclature_change/full_reassignment.rb +++ b/app/models/nomenclature_change/full_reassignment.rb @@ -41,14 +41,14 @@ def process Rails.logger.debug "FULL REASSIGNMENT Document Citations (#{@old_taxon_concept.document_citation_taxon_concepts.count})" # need validations to be applied to avoid duplicates exception @old_taxon_concept.document_citation_taxon_concepts.each do |dctc| - dctc.update_attributes(update_attrs) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + dctc.update(update_attrs) end # shipments Rails.logger.debug "FULL REASSIGNMENT Shipments" Trade::Shipment.where(taxon_concept_id: @old_taxon_concept.id).update_all(update_attrs) - @old_taxon_concept.update_attributes(dependents_updated_at: update_timestamp) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + @old_taxon_concept.update(dependents_updated_at: update_timestamp) @old_taxon_concept.reload - @new_taxon_concept.update_attributes(dependents_updated_at: update_timestamp) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + @new_taxon_concept.update(dependents_updated_at: update_timestamp) end end diff --git a/app/models/nomenclature_change/input_taxon_concept_processor.rb b/app/models/nomenclature_change/input_taxon_concept_processor.rb index c23e1c10a..72c6dda9c 100644 --- a/app/models/nomenclature_change/input_taxon_concept_processor.rb +++ b/app/models/nomenclature_change/input_taxon_concept_processor.rb @@ -8,7 +8,7 @@ def run return false unless @input.taxon_concept Rails.logger.debug("Processing input #{@input.taxon_concept.full_name}") tc = @input.taxon_concept - tc.update_attributes( # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + tc.update( nomenclature_note_en: "#{tc.nomenclature_note_en} #{@input.note_en}", nomenclature_note_es: "#{tc.nomenclature_note_es} #{@input.note_es}", nomenclature_note_fr: "#{tc.nomenclature_note_fr} #{@input.note_fr}" diff --git a/app/models/nomenclature_change/to_accepted_name_transformation.rb b/app/models/nomenclature_change/to_accepted_name_transformation.rb index 716392896..34687ffe9 100644 --- a/app/models/nomenclature_change/to_accepted_name_transformation.rb +++ b/app/models/nomenclature_change/to_accepted_name_transformation.rb @@ -13,7 +13,7 @@ def process @non_accepted_taxon_concept.inverse_trade_name_relationships ) - @non_accepted_taxon_concept.update_attributes( # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + @non_accepted_taxon_concept.update( parent_id: @new_parent.id, name_status: 'A' ) diff --git a/app/models/nomenclature_change/to_synonym_transformation.rb b/app/models/nomenclature_change/to_synonym_transformation.rb index 4edc3da2f..2021ff322 100644 --- a/app/models/nomenclature_change/to_synonym_transformation.rb +++ b/app/models/nomenclature_change/to_synonym_transformation.rb @@ -27,7 +27,7 @@ def process def relink_relationships(relationships, new_taxon_concept) relationships.includes(:taxon_concept, :taxon_relationship_type).each do |rel| Rails.logger.debug "Relinking #{rel.taxon_relationship_type.name} relationship from #{rel.taxon_concept.full_name} to #{new_taxon_concept.full_name}" - rel.update_attributes(taxon_concept_id: new_taxon_concept.id) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + rel.update(taxon_concept_id: new_taxon_concept.id) end end diff --git a/app/models/trade/annual_report_upload.rb b/app/models/trade/annual_report_upload.rb index 77b3d442d..7487fce4e 100644 --- a/app/models/trade/annual_report_upload.rb +++ b/app/models/trade/annual_report_upload.rb @@ -97,11 +97,11 @@ def submit(submitter) DownloadsCacheCleanupWorker.perform_async('shipments') # flag as submitted - update_attributes({ # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + update( submitted_at: DateTime.now, submitted_by_id: submitter.id, number_of_records_submitted: records_submitted - }) + ) # This has been temporarily disabled as originally part of EPIX #ChangesHistoryGeneratorWorker.perform_async(self.id, submitter.id) diff --git a/app/models/trade/validation_rule.rb b/app/models/trade/validation_rule.rb index 9ead03ef6..bda8ebdd1 100644 --- a/app/models/trade/validation_rule.rb +++ b/app/models/trade/validation_rule.rb @@ -106,7 +106,7 @@ def update_or_create_error_record(annual_report_upload, existing_record, error_c if error_count == 0 existing_record.destroy else - existing_record.update_attributes( # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + existing_record.update( error_count: error_count ) end diff --git a/app/workers/changes_history_generator_worker.rb b/app/workers/changes_history_generator_worker.rb index d14540d45..a95b2b277 100644 --- a/app/workers/changes_history_generator_worker.rb +++ b/app/workers/changes_history_generator_worker.rb @@ -23,7 +23,7 @@ def perform(aru_id, user_id) obj = s3.bucket(bucket_name).object(filename) obj.upload_file(tempfile.path) - aru.update_attributes(aws_storage_path: obj.public_url) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + aru.update(aws_storage_path: obj.public_url) rescue Aws::S3::Errors::ServiceError => e Rails.logger.warn "Something went wrong while uploading #{aru.id} to S3" Appsignal.add_exception(e) if defined? Appsignal diff --git a/app/workers/submission_worker.rb b/app/workers/submission_worker.rb index 4078e8651..3ecc8805a 100644 --- a/app/workers/submission_worker.rb +++ b/app/workers/submission_worker.rb @@ -41,11 +41,11 @@ def perform(aru_id, submitter_id) aru.sandbox.destroy # flag as submitted - aru.update_attributes({ # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + aru.update( submitted_at: DateTime.now, submitted_by_id: submitter.id, number_of_records_submitted: records_submitted - }) + ) NotificationMailer.changelog(submitter, aru, tempfile).deliver_now @@ -62,7 +62,7 @@ def upload_on_S3(aru, tempfile) obj = s3.bucket(bucket_name).object(filename) obj.upload_file(tempfile.path) - aru.update_attributes(aws_storage_path: obj.public_url) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + aru.update(aws_storage_path: obj.public_url) rescue Aws::S3::Errors::ServiceError => e Rails.logger.warn "Something went wrong while uploading #{aru.id} to S3" Appsignal.add_exception(e) if defined? Appsignal diff --git a/lib/tasks/import_countries.rake b/lib/tasks/import_countries.rake index 65cec168a..df241a366 100644 --- a/lib/tasks/import_countries.rake +++ b/lib/tasks/import_countries.rake @@ -44,7 +44,7 @@ namespace :import do CSV.foreach("lib/files/country_codes_en_es_fr_utf8.csv") do |row| country = GeoEntity.find_or_initialize_by(iso_code2: row[0].strip.upcase) unless country.id.nil? - country.update_attributes( # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + country.update( :name_fr => row[1].strip, :name_es => row[2].strip ) end diff --git a/lib/tasks/import_orchids_T_to_A.rake b/lib/tasks/import_orchids_T_to_A.rake index babb04cbc..3d5ee8c44 100644 --- a/lib/tasks/import_orchids_T_to_A.rake +++ b/lib/tasks/import_orchids_T_to_A.rake @@ -12,7 +12,7 @@ namespace :import do puts "There was a problem with this Taxon Concept #{row['ID'].strip}" next end - @nomenclature_change.update_attributes(:status => NomenclatureChange::SUBMITTED) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + @nomenclature_change.update(:status => NomenclatureChange::SUBMITTED) end end end diff --git a/lib/tasks/import_trade_codes.rake b/lib/tasks/import_trade_codes.rake index 5bd4ea20d..550b50f3c 100644 --- a/lib/tasks/import_trade_codes.rake +++ b/lib/tasks/import_trade_codes.rake @@ -6,9 +6,11 @@ namespace :import do current_count = klass.count CSV.foreach("lib/files/#{klass.to_s.downcase}_codes_utf8.csv") do |row| code = klass.find_or_initialize_by(code: row[0].strip.upcase) - code.update_attributes(:name_en => row[1].strip, # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. - :name_fr => row[2].strip, - :name_es => row[3].strip) + code.update( + :name_en => row[1].strip, + :name_fr => row[2].strip, + :name_es => row[3].strip + ) end puts "#{klass.count - current_count} new #{klass} added" end diff --git a/lib/tasks/import_trade_names.rake b/lib/tasks/import_trade_names.rake index 73fdcd2f6..a37cb8344 100644 --- a/lib/tasks/import_trade_names.rake +++ b/lib/tasks/import_trade_names.rake @@ -146,15 +146,14 @@ namespace :import do puts "Updating #{tc.full_name}" unless tc.accepted_names.any? puts "from synonym to trade_name" - tc.update_attributes(:name_status => "T", :parent_id => nil, # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. - :legacy_trade_code => cites_code) + tc.update(:name_status => "T", :parent_id => nil, :legacy_trade_code => cites_code) end puts "Update its children's taxon_name_id" tc.children.each do |child| if child.accepted_names.any? puts "looking at #{child.full_name} scientific_name" taxon_name = TaxonName.find_or_create_by(scientific_name: child.full_name) - child.update_attributes(:parent_id => nil, :taxon_name_id => taxon_name.id) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + child.update(:parent_id => nil, :taxon_name_id => taxon_name.id) end end end diff --git a/lib/tasks/trade_db_resolve_ip_to_country.rake b/lib/tasks/trade_db_resolve_ip_to_country.rake index 66b61d885..fb08d2573 100644 --- a/lib/tasks/trade_db_resolve_ip_to_country.rake +++ b/lib/tasks/trade_db_resolve_ip_to_country.rake @@ -5,6 +5,6 @@ task :trade_db_resolve_ip_to_country => :environment do trade_downloads.each do |td| puts "Updating Trade download #{td.id}" geo_ip_data = Sapi::GeoIP.instance.resolve(td.user_ip) - td.update_attributes!(geo_ip_data) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + td.update!(geo_ip_data) end end diff --git a/spec/controllers/trade/annual_report_uploads_controller_spec.rb b/spec/controllers/trade/annual_report_uploads_controller_spec.rb index 60f0721ff..ca4b6b97e 100644 --- a/spec/controllers/trade/annual_report_uploads_controller_spec.rb +++ b/spec/controllers/trade/annual_report_uploads_controller_spec.rb @@ -30,7 +30,7 @@ def exporter_csv @aru.save(:validate => false) @completed_aru = build(:annual_report_upload) @completed_aru.save(:validate => false) - @completed_aru.update_attributes(submitted_at: Time.now) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + @completed_aru.update(submitted_at: Time.now) end it "should return all annual report uploads" do get :index, format: :json diff --git a/spec/models/designation_spec.rb b/spec/models/designation_spec.rb index 5c001216c..f7c9da237 100644 --- a/spec/models/designation_spec.rb +++ b/spec/models/designation_spec.rb @@ -33,14 +33,14 @@ context "when updating a non-protected name" do let(:designation) { create(:designation) } specify { - expect(designation.update_attributes( # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + expect(designation.update( { :name => 'RULES OF INTERGALACTIC TRADE' } )).to be_truthy } end context "when updating a protected name" do specify { - expect(cites.update_attributes( # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + expect(cites.update( { :name => 'RULES OF INTERGALACTIC TRADE' } )).to be_falsey } @@ -48,13 +48,13 @@ context "when updating taxonomy with no dependent objects attached" do let(:designation) { create(:designation) } let(:taxonomy) { create(:taxonomy) } - specify { expect(designation.update_attributes(:taxonomy_id => taxonomy.id)).to be_truthy } # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + specify { expect(designation.update(:taxonomy_id => taxonomy.id)).to be_truthy } end context "when updating taxonomy with dependent objects attached" do let(:designation) { create(:designation) } let!(:change_type) { create(:change_type, :designation => designation) } let(:taxonomy) { create(:taxonomy) } - specify { expect(designation.update_attributes(:taxonomy_id => taxonomy.id)).to be_falsey } # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + specify { expect(designation.update(:taxonomy_id => taxonomy.id)).to be_falsey } end end describe :destroy do diff --git a/spec/models/document_search_spec.rb b/spec/models/document_search_spec.rb index 1d668d96b..d1a943966 100644 --- a/spec/models/document_search_spec.rb +++ b/spec/models/document_search_spec.rb @@ -334,7 +334,7 @@ context "when document updated in last #{DocumentSearch::REFRESH_INTERVAL} minutes" do specify do travel_to(Time.now - (DocumentSearch::REFRESH_INTERVAL - 1).minutes) do - @d.update_attributes!(is_public: true) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + @d.update!(is_public: true) end expect(DocumentSearch.documents_need_refreshing?).to be_truthy @@ -406,7 +406,7 @@ context "updated in last #{DocumentSearch::REFRESH_INTERVAL} minutes" do specify do travel_to(@recent_time) do - @c.update_attributes!(elib_legacy_id: 1) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + @c.update!(elib_legacy_id: 1) end expect(@c.reload.updated_at).to be > @refresh_threshold @@ -455,7 +455,7 @@ context "updated in last #{DocumentSearch::REFRESH_INTERVAL} minutes" do specify do travel_to(@recent_time) do - @c_tc.update_attributes!(taxon_concept_id: create_cites_eu_species.id) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + @c_tc.update!(taxon_concept_id: create_cites_eu_species.id) end expect(@c.reload.updated_at).to be > @refresh_threshold @@ -493,7 +493,7 @@ context "updated in last #{DocumentSearch::REFRESH_INTERVAL} minutes" do specify do travel_to(@recent_time) do - @c_ge.update_attributes!(geo_entity_id: @ge2.id) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + @c_ge.update!(geo_entity_id: @ge2.id) end expect(@c.reload.updated_at).to be > @refresh_threshold diff --git a/spec/models/document_spec.rb b/spec/models/document_spec.rb index 9c7a9b099..1d043e538 100644 --- a/spec/models/document_spec.rb +++ b/spec/models/document_spec.rb @@ -81,13 +81,13 @@ } context "when primary document sort_index_updated" do specify "secondary document sort_index is in sync" do - primary_document.update_attributes(sort_index: 3) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + primary_document.update(sort_index: 3) expect(secondary_document.reload.sort_index).to eq(3) end end context "when secondary document sort_index_updated" do specify "primary document sort_index is in sync" do - secondary_document.update_attributes(sort_index: 3) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + secondary_document.update(sort_index: 3) expect(primary_document.reload.sort_index).to eq(3) end end diff --git a/spec/models/geo_entity_search_spec.rb b/spec/models/geo_entity_search_spec.rb index 9ab78db4b..838c183ad 100644 --- a/spec/models/geo_entity_search_spec.rb +++ b/spec/models/geo_entity_search_spec.rb @@ -108,7 +108,7 @@ subject { GeoEntitySearch.new({ geo_entity_types_set: '3' }) } specify do subject.cached_results - @burma.update_attributes({ is_current: false }) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + @burma.update({ is_current: false }) expect(subject.cached_results).not_to include(@burma) end end diff --git a/spec/models/nomenclature_change/split/constructor_spec.rb b/spec/models/nomenclature_change/split/constructor_spec.rb index 073d7251d..ee214e59c 100644 --- a/spec/models/nomenclature_change/split/constructor_spec.rb +++ b/spec/models/nomenclature_change/split/constructor_spec.rb @@ -188,7 +188,7 @@ # split.input.distribution_reassignments.first. - # update_attributes(output_ids: [split.outputs.first.id]) + # update(output_ids: [split.outputs.first.id]) constructor.build_document_reassignments expect(split.input.document_citation_reassignments.first. output_ids).not_to include(non_default_output.id) diff --git a/spec/models/taxonomy_spec.rb b/spec/models/taxonomy_spec.rb index 8d55efc45..037e7418c 100644 --- a/spec/models/taxonomy_spec.rb +++ b/spec/models/taxonomy_spec.rb @@ -31,10 +31,10 @@ describe :update do context "when updating a non-protected name" do let(:taxonomy) { create(:taxonomy) } - specify { expect(taxonomy.update_attributes({ :name => 'WORLD OF LOLCATS' })).to be_truthy } # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + specify { expect(taxonomy.update({ :name => 'WORLD OF LOLCATS' })).to be_truthy } end context "when updating a protected name" do - specify { expect(cites_eu.update_attributes({ :name => 'WORLD OF LOLCATS' })).to be_falsey } # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + specify { expect(cites_eu.update({ :name => 'WORLD OF LOLCATS' })).to be_falsey } end end describe :destroy do diff --git a/spec/models/trade/inclusion_validation_rule_spec.rb b/spec/models/trade/inclusion_validation_rule_spec.rb index 4d34c840a..4f395796f 100644 --- a/spec/models/trade/inclusion_validation_rule_spec.rb +++ b/spec/models/trade/inclusion_validation_rule_spec.rb @@ -111,8 +111,8 @@ context "when updates and error fixed for all records" do specify "error record is destroyed" do travel_to(Time.now + 1) do - @shipment2.update_attributes(taxon_name: 'Canis lupus') # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. - @shipment3.update_attributes(taxon_name: 'Canis lupus') # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + @shipment2.update(taxon_name: 'Canis lupus') + @shipment3.update(taxon_name: 'Canis lupus') expect { validation_rule.refresh_errors_if_needed(annual_report_upload) }.to change { Trade::ValidationError.count }.by(-1) @@ -123,7 +123,7 @@ context "when updates and error fixed for some records" do specify "error record is updated to reflect new error_count" do travel_to(Time.now + 1) do - @shipment2.update_attributes(taxon_name: 'Canis lupus') # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + @shipment2.update(taxon_name: 'Canis lupus') expect { validation_rule.refresh_errors_if_needed(annual_report_upload) }.to change { @validation_error.reload.error_count }.by(-1) diff --git a/spec/models/trade/sandbox_template_spec.rb b/spec/models/trade/sandbox_template_spec.rb index 8fbc73953..76be7ef57 100644 --- a/spec/models/trade/sandbox_template_spec.rb +++ b/spec/models/trade/sandbox_template_spec.rb @@ -54,7 +54,7 @@ @shipment1 = sandbox_klass.create(:taxon_name => canis_lupus.full_name) end specify { - @shipment1.update_attributes(:taxon_name => canis_aureus.full_name) # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + @shipment1.update(:taxon_name => canis_aureus.full_name) expect(@shipment1.reload.taxon_concept_id).to eq(canis_aureus.id) } end diff --git a/spec/models/trade/validation_rule_spec.rb b/spec/models/trade/validation_rule_spec.rb index ed62b7997..6688b55b1 100644 --- a/spec/models/trade/validation_rule_spec.rb +++ b/spec/models/trade/validation_rule_spec.rb @@ -101,8 +101,8 @@ context "when updates and error fixed for all records" do specify "error record is destroyed" do travel_to(Time.now + 1) do - @shipment2.update_attributes(taxon_name: 'Canis lupus') # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. - @shipment3.update_attributes(taxon_name: 'Canis lupus') # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + @shipment2.update(taxon_name: 'Canis lupus') + @shipment3.update(taxon_name: 'Canis lupus') expect { validation_rule.refresh_errors_if_needed(annual_report_upload) }.to change { Trade::ValidationError.count }.by(-1) @@ -113,7 +113,7 @@ context "when updates and error fixed for some records" do specify "error record is updated to reflect new error_count" do travel_to(Time.now + 1) do - @shipment2.update_attributes(taxon_name: 'Canis lupus') # TODO: `update_attributes` is deprecated in Rails 6, and removed from Rails 7. + @shipment2.update(taxon_name: 'Canis lupus') expect { validation_rule.refresh_errors_if_needed(annual_report_upload) }.to change { @validation_error.reload.error_count }.by(-1) From 2f50a8fad55cadefa420ac4bde9913b3c239e3f0 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Wed, 31 Jan 2024 14:57:45 +0000 Subject: [PATCH 134/241] fix environment (copy from production to staging) --- config/environments/production.rb | 3 +-- config/environments/staging.rb | 24 ++++++++++++++++++++++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/config/environments/production.rb b/config/environments/production.rb index 90c218765..7cd7b0fef 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -35,7 +35,7 @@ # Specifies the header that your server uses for sending files. # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache - # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX + config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local @@ -61,7 +61,6 @@ # Use a real queuing backend for Active Job (and separate queues per environment). # config.active_job.queue_adapter = :resque # config.active_job.queue_name_prefix = "sapi_production" - config.action_mailer.perform_caching = false # Ignore bad email addresses and do not raise email delivery errors. diff --git a/config/environments/staging.rb b/config/environments/staging.rb index 435915493..ce7c7de92 100644 --- a/config/environments/staging.rb +++ b/config/environments/staging.rb @@ -24,13 +24,12 @@ # Compress JavaScripts and CSS. config.assets.js_compressor = :uglifier + # Compress CSS using a preprocessor. # config.assets.css_compressor = :sass # Don't fallback to assets pipeline if a precompiled asset is missed. config.assets.compile = false - # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb - # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.action_controller.asset_host = 'http://assets.example.com' @@ -92,6 +91,27 @@ # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false + # Inserts middleware to perform automatic connection switching. + # The `database_selector` hash is used to pass options to the DatabaseSelector + # middleware. The `delay` is used to determine how long to wait after a write + # to send a subsequent read to the primary. + # + # The `database_resolver` class is used by the middleware to determine which + # database is appropriate to use based on the time delay. + # + # The `database_resolver_context` class is used by the middleware to set + # timestamps for the last write to the primary. The resolver uses the context + # class timestamps to determine how long to wait before reading from the + # replica. + # + # By default Rails will store a last write timestamp in the session. The + # DatabaseSelector middleware is designed as such you can define your own + # strategy for connection switching and pass that into the middleware through + # these configuration options. + # config.active_record.database_selector = { delay: 2.seconds } + # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver + # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session + # Custom ember settings config.ember.variant = :production From 6262531667082e64faa1f00ad68f2469152d2222 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Wed, 31 Jan 2024 15:05:45 +0000 Subject: [PATCH 135/241] enable 6.0 default --- .../initializers/0_new_framework_defaults_6_0.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/config/initializers/0_new_framework_defaults_6_0.rb b/config/initializers/0_new_framework_defaults_6_0.rb index 92240ef5f..223024573 100644 --- a/config/initializers/0_new_framework_defaults_6_0.rb +++ b/config/initializers/0_new_framework_defaults_6_0.rb @@ -14,21 +14,21 @@ # # This option is not backwards compatible with earlier Rails versions. # It's best enabled when your entire app is migrated and stable on 6.0. -# Rails.application.config.action_dispatch.use_cookies_with_metadata = true +Rails.application.config.action_dispatch.use_cookies_with_metadata = true # Change the return value of `ActionDispatch::Response#content_type` to Content-Type header without modification. -# Rails.application.config.action_dispatch.return_only_media_type_on_content_type = false +Rails.application.config.action_dispatch.return_only_media_type_on_content_type = false # Return false instead of self when enqueuing is aborted from a callback. -# Rails.application.config.active_job.return_false_on_aborted_enqueue = true +Rails.application.config.active_job.return_false_on_aborted_enqueue = true # Send Active Storage analysis and purge jobs to dedicated queues. -# Rails.application.config.active_storage.queues.analysis = :active_storage_analysis -# Rails.application.config.active_storage.queues.purge = :active_storage_purge +Rails.application.config.active_storage.queues.analysis = :active_storage_analysis +Rails.application.config.active_storage.queues.purge = :active_storage_purge # When assigning to a collection of attachments declared via `has_many_attached`, replace existing # attachments instead of appending. Use #attach to add new attachments without replacing existing ones. -# Rails.application.config.active_storage.replace_on_assign_to_many = true +Rails.application.config.active_storage.replace_on_assign_to_many = true # Use ActionMailer::MailDeliveryJob for sending parameterized and normal mail. # @@ -37,9 +37,9 @@ # If you send mail in the background, job workers need to have a copy of # MailDeliveryJob to ensure all delivery jobs are processed properly. # Make sure your entire app is migrated and stable on 6.0 before using this setting. -# Rails.application.config.action_mailer.delivery_job = "ActionMailer::MailDeliveryJob" +Rails.application.config.action_mailer.delivery_job = "ActionMailer::MailDeliveryJob" # Enable the same cache key to be reused when the object being cached of type # `ActiveRecord::Relation` changes by moving the volatile information (max updated at and count) # of the relation's cache key into the cache version to support recycling cache key. -# Rails.application.config.active_record.collection_cache_versioning = true +Rails.application.config.active_record.collection_cache_versioning = true From 942df65d87c85bc12660193932690e2a87f121d9 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Wed, 31 Jan 2024 15:30:21 +0000 Subject: [PATCH 136/241] upgrade gems --- Gemfile | 6 +++--- Gemfile.lock | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Gemfile b/Gemfile index 03a44af37..45240d1be 100644 --- a/Gemfile +++ b/Gemfile @@ -34,7 +34,7 @@ gem 'inherited_resources', '~> 1.14' # Deprecated (https://github.com/activeadmi gem 'nokogiri', '1.15.5' # TODO: New version need Ruby 3 gem 'mobility', '~> 1.2', '>= 1.2.9' gem 'devise', '4.7.3' # TODO: need upgrade to 4.8+ when upgrade to rails 6.1 -gem 'cancancan', '2.3.0' # TODO, can upgrade to 3.0 after Rails 6 +gem 'cancancan', '~> 3.5' gem 'ahoy_matey', '4.2.1' # TODO: latest 5.0.2. Can't upgrade to 5.0 until upgrade to Rails 6 gem 'uuidtools', '~> 2.2' # For Ahoy. (https://github.com/ankane/ahoy/blob/v2.2.1/docs/Ahoy-2-Upgrade.md#activerecordstore) @@ -123,7 +123,7 @@ end group :test, :development do gem "rspec-rails", '5.1.2' # TODO: should upgrade once to rails 6.1 gem 'rspec-collection_matchers', '~> 1.2', '>= 1.2.1' - gem "json_spec", '1.1.5' + gem 'json_spec', '~> 1.1', '>= 1.1.5' gem 'database_cleaner', '~> 2.0', '>= 2.0.2' gem "launchy", '2.4.3' # Call 'byebug' anywhere in the code to stop execution and get a debugger console @@ -174,7 +174,7 @@ gem 'test-unit', '3.1.5' # annoyingly, rails console won't start without it in s gem 'susy', '~> 2.2', '>= 2.2.14' # TODO: Deprecated. (https://github.com/oddbird/susy#power-tools-for-the-web-deprecated) gem 'gon', '~> 6.4' -gem "chartkick", '4.2.1' # TODO: latest 5.0.5 @ 2023. Should upgrade to v4 once we upgrade to Rails 6 and Ruby 2.7 +gem 'chartkick', '~> 5.0', '>= 5.0.5' gem 'nested_form', '~> 0.3.2' # TODO: Deprecated. (https://github.com/ryanb/nested_form#unmaintained) gem 'bootstrap-sass', '2.3.2.2' # TODO: latest 3.4.1 @ 2019. Can't upgrade unless we sure bootstrap v3 backward compatible with boostrap v2 diff --git a/Gemfile.lock b/Gemfile.lock index 0a5bf8350..b0bc39bbf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -114,7 +114,7 @@ GEM redis (>= 1.0, < 6) builder (3.2.4) byebug (11.1.3) - cancancan (2.3.0) + cancancan (3.5.0) capistrano (3.11.0) airbrussh (>= 1.0.0) i18n @@ -155,7 +155,7 @@ GEM marcel (~> 1.0.0) mini_mime (>= 0.1.3) ssrf_filter (~> 1.0) - chartkick (4.2.1) + chartkick (5.0.5) chronic_duration (0.10.6) numerizer (~> 0.1.1) codeclimate-test-reporter (0.1.1) @@ -606,7 +606,7 @@ DEPENDENCIES bootstrap-sass (= 2.3.2.2) brightbox (= 2.3.9) byebug - cancancan (= 2.3.0) + cancancan (~> 3.5) capistrano (= 3.11.0) capistrano-bundler (= 1.5.0) capistrano-local-precompile (= 1.2.0) @@ -617,7 +617,7 @@ DEPENDENCIES capistrano-sidekiq (~> 2.3, >= 2.3.1) capybara (>= 2.15) carrierwave (= 2.2.5) - chartkick (= 4.2.1) + chartkick (~> 5.0, >= 5.0.5) codeclimate-test-reporter (= 0.1.1) coffee-rails (~> 5.0) coveralls (= 0.7.1) @@ -637,7 +637,7 @@ DEPENDENCIES httparty (~> 0.21.0) inherited_resources (~> 1.14) jslint_on_rails (= 1.1.1) - json_spec (= 1.1.5) + json_spec (~> 1.1, >= 1.1.5) kaminari (~> 1.2, >= 1.2.2) launchy (= 2.4.3) listen (~> 3.2) From 067a37fe3e4a525176c741309b44c58d33c9d4b9 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Wed, 31 Jan 2024 16:48:21 +0000 Subject: [PATCH 137/241] upgrade gems --- Gemfile | 4 ++-- Gemfile.lock | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index 45240d1be..d88be10a4 100644 --- a/Gemfile +++ b/Gemfile @@ -35,7 +35,7 @@ gem 'nokogiri', '1.15.5' # TODO: New version need Ruby 3 gem 'mobility', '~> 1.2', '>= 1.2.9' gem 'devise', '4.7.3' # TODO: need upgrade to 4.8+ when upgrade to rails 6.1 gem 'cancancan', '~> 3.5' -gem 'ahoy_matey', '4.2.1' # TODO: latest 5.0.2. Can't upgrade to 5.0 until upgrade to Rails 6 +gem 'ahoy_matey', '4.2.1' # TODO: latest 5.0.2. Can't upgrade to 5.0 until upgrade to Rails 6.1 gem 'uuidtools', '~> 2.2' # For Ahoy. (https://github.com/ankane/ahoy/blob/v2.2.1/docs/Ahoy-2-Upgrade.md#activerecordstore) # TODO: starting from v1.4, it break our test due to redirection changes: @@ -57,7 +57,7 @@ gem 'httparty', '~> 0.21.0' gem 'kaminari', '~> 1.2', '>= 1.2.2' # TODO: Suggest migrate to pagy gem. -gem 'acts-as-taggable-on', '8.1.0' # TODO: latest v10 @ 2023. Can upgrade after upgrade to Rails 6. +gem 'acts-as-taggable-on', '9.0.1' # TODO: latest v10 @ 2023. Can upgrade after upgrade to Rails 6.1 gem 'carrierwave', '2.2.5' # TODO: latest is 3.0.5 @ 2023. can upgrade to v3 after Rails 6 # PDF diff --git a/Gemfile.lock b/Gemfile.lock index b0bc39bbf..4cfdd64d8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -65,8 +65,8 @@ GEM minitest (~> 5.1) tzinfo (~> 1.1) zeitwerk (~> 2.2, >= 2.2.2) - acts-as-taggable-on (8.1.0) - activerecord (>= 5.0, < 6.2) + acts-as-taggable-on (9.0.1) + activerecord (>= 6.0, < 7.1) addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) afm (0.2.2) @@ -596,7 +596,7 @@ DEPENDENCIES actionpack-action_caching (~> 1.2, >= 1.2.2) actionpack-page_caching (~> 1.2, >= 1.2.4) active_model_serializers (= 0.8.4) - acts-as-taggable-on (= 8.1.0) + acts-as-taggable-on (= 9.0.1) ahoy_matey (= 4.2.1) annotate (= 2.5.0) appsignal (= 1.3.3) From 524c85a07f9e7c390ec7f98d77887ec7a15f191f Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Thu, 1 Feb 2024 16:27:29 +0000 Subject: [PATCH 138/241] Classic to Zeitwerk (https://guides.rubyonrails.org/classic_to_zeitwerk_howto.html) --- Capfile | 2 +- app/controllers/admin/documents_controller.rb | 4 +- .../api/v1/documents_controller.rb | 2 - .../checklist/documents_controller.rb | 2 - .../trade/trade_data_download_logger.rb | 2 +- app/models/admin.rb | 5 -- .../cites_captivity_process.rb | 0 .../cites_rst_process.rb | 0 app/models/{ => concerns}/m_listing_change.rb | 0 app/models/document/proposal.rb | 2 +- .../document/review_of_significant_trade.rb | 2 +- app/models/{ => eu_decisions}/eu_opinion.rb | 0 .../{ => eu_decisions}/eu_suspension.rb | 0 app/models/{ => events}/cites_ac.rb | 0 app/models/{ => events}/cites_cop.rb | 0 app/models/{ => events}/cites_cop_observer.rb | 0 .../cites_extraordinary_meeting.rb | 0 app/models/{ => events}/cites_pc.rb | 0 .../cites_suspension_notification.rb | 0 .../cites_suspension_notification_observer.rb | 0 app/models/{ => events}/cites_tc.rb | 0 app/models/{ => events}/ec_srg.rb | 0 .../{ => events}/eu_council_regulation.rb | 0 app/models/{ => events}/eu_event_observer.rb | 0 .../eu_implementing_regulation.rb | 0 app/models/{ => events}/eu_regulation.rb | 0 .../{ => events}/eu_regulation_observer.rb | 0 .../{ => events}/eu_suspension_regulation.rb | 0 .../eu_suspension_regulation_observer.rb | 0 app/models/{ => events}/id_materials.rb | 0 app/models/{document => }/proposal_details.rb | 2 +- app/models/{document => }/review_details.rb | 2 +- app/models/{ => trade_codes}/purpose.rb | 0 app/models/{ => trade_codes}/source.rb | 0 app/models/{ => trade_codes}/term.rb | 0 app/models/{ => trade_codes}/unit.rb | 0 .../cites_suspension.rb | 0 .../cites_suspension_observer.rb | 0 app/models/{ => trade_restrictions}/quota.rb | 0 .../quota_observer.rb | 0 ...pendix_year_validation_rule_serializer.rb} | 0 .../checklist/checklist.rb | 0 .../checklist/checklist_params.rb | 0 .../checklist/column_display_name_mapping.rb | 0 .../checklist/csv/document.rb | 0 .../checklist/csv/history.rb | 0 .../checklist/csv/history_content.rb | 0 .../checklist/csv/index.rb | 0 .../checklist/csv/index_content.rb | 0 .../checklist/higher_taxa_injector.rb | 0 .../checklist/higher_taxa_item.rb | 0 app/{models => services}/checklist/history.rb | 0 .../checklist/history_fetcher.rb | 0 app/{models => services}/checklist/index.rb | 0 .../checklist/index_fetcher.rb | 0 .../checklist/json/document.rb | 0 .../checklist/json/history.rb | 0 .../checklist/json/history_content.rb | 0 .../checklist/json/index.rb | 0 .../checklist/json/index_content.rb | 0 .../checklist/pdf/document.rb | 0 .../checklist/pdf/helpers.rb | 0 .../checklist/pdf/history.rb | 0 .../checklist/pdf/history_annotations_key.rb | 0 .../checklist/pdf/history_content.rb | 0 .../checklist/pdf/index.rb | 0 .../checklist/pdf/index_annotations_key.rb | 0 .../checklist/pdf/index_content.rb | 0 .../checklist/pdf/index_fetcher.rb | 0 .../checklist/pdf/index_query.rb | 0 .../checklist/timeline.rb | 0 .../checklist/timeline_event.rb | 0 .../checklist/timeline_interval.rb | 0 .../checklist/timeline_year.rb | 0 .../checklist/timelines_for_taxon_concept.rb | 0 .../cms_mapping_manager.rb | 0 .../country_dictionary.rb | 0 app/{models => services}/dashboard_stats.rb | 0 app/{models => services}/document_batch.rb | 0 .../document_collection_order.rb | 0 app/{models => services}/document_search.rb | 0 .../document_search_params.rb | 0 .../events_by_type_stats.rb | 0 app/{models => services}/geo_entity_search.rb | 0 .../iucn_mapping_manager.rb | 0 ...ept_filter_by_appendix_population_query.rb | 0 ..._taxon_concept_filter_by_appendix_query.rb | 0 ...n_concept_filter_by_id_with_descendants.rb | 0 ...ter_by_scientific_name_with_descendants.rb | 0 .../cascading_notes_processor.rb | 0 .../delete_unreassigned_processor.rb | 0 .../nomenclature_change/full_reassignment.rb | 0 .../input_taxon_concept_processor.rb | 0 .../nomenclature_change/lump/constructor.rb | 0 .../nomenclature_change/lump/processor.rb | 0 .../output_taxon_concept_processor.rb | 0 .../nomenclature_change/processor.rb | 0 .../reassignment_copy_processor.rb | 0 .../reassignment_processor.rb | 0 .../reassignment_summarizer.rb | 0 .../reassignment_transfer_processor.rb | 0 .../nomenclature_change/split/constructor.rb | 0 .../nomenclature_change/split/processor.rb | 0 .../status_change/constructor_helpers.rb | 0 .../status_change/processor_helpers.rb | 0 .../status_change_processor.rb | 0 .../status_downgrade_processor.rb | 0 .../status_swap/constructor.rb | 0 .../status_swap/processor.rb | 0 .../status_to_accepted/constructor.rb | 0 .../status_to_accepted/processor.rb | 0 .../status_to_synonym/constructor.rb | 0 .../status_to_synonym/processor.rb | 0 .../status_upgrade_processor.rb | 0 .../taxonomic_tree_name_resolver.rb | 0 .../to_accepted_name_transformation.rb | 0 .../to_synonym_transformation.rb | 0 .../trade_shipments_resolver.rb | 0 app/{models => services}/search_params.rb | 0 .../species/cites_listings_export.rb | 0 .../species/cites_processes_export.rb | 0 .../species/cms_listings_export.rb | 0 .../species/cms_mappings_export.rb | 0 .../species/common_names_export.rb | 0 .../species/csv_copy_export.rb | 0 .../species/documents_export.rb | 0 .../species/eu_decisions_export.rb | 0 .../species/eu_listings_export.rb | 0 .../species/id_manual_documents_export.rb | 0 .../species/iucn_mappings_export.rb | 0 .../species/listings_export.rb | 0 .../species/listings_export_factory.rb | 0 .../species/orphaned_taxon_concepts_export.rb | 0 .../species/restrictions_export.rb | 0 app/{models => services}/species/search.rb | 0 .../species/search_params.rb | 0 .../species_reference_output_export.rb | 0 .../standard_reference_output_export.rb | 0 .../synonyms_and_trade_names_export.rb | 0 .../species/taxon_concept_prefix_matcher.rb | 0 .../taxon_concepts_distributions_export.rb | 0 .../species/taxon_concepts_names_export.rb | 0 .../taxon_concept_data.rb | 0 .../taxon_concept_matcher.rb | 0 .../taxon_concept_prefix_matcher.rb | 0 .../taxon_concept_view_stats.rb | 0 .../trade/batch_update.rb | 0 app/{models => services}/trade/filter.rb | 0 .../trade/permit_matcher.rb | 0 .../trade/reported_taxon_concept_resolver.rb | 0 app/{models => services}/trade/sandbox.rb | 0 .../trade/sandbox_filter.rb | 0 .../trade/sandbox_search_params.rb | 0 .../trade/search_params.rb | 0 .../trade/shipment_report_queries.rb | 0 .../trade/shipments_comptab_export.rb | 0 .../trade/shipments_export.rb | 0 .../trade/shipments_export_factory.rb | 0 .../trade/shipments_gross_exports_export.rb | 0 .../trade/shipments_gross_imports_export.rb | 0 .../trade/shipments_net_exports_export.rb | 0 .../trade/shipments_net_imports_export.rb | 0 .../trade/trade_data_downloads_export.rb | 0 .../trade/csv_source_file_uploader.rb | 2 - app/workers/manual_download_worker.rb | 2 - config/application.rb | 4 +- config/initializers/acts_as_taggable_on.rb | 16 +++++ config/initializers/ahoy.rb | 2 +- config/initializers/preload_sti_models.rb | 32 +++++++--- config/initializers/sidekiq.rb | 2 +- lib/modules/hash_deep_slice.rb | 28 --------- lib/modules/import/rst/formatter.rb | 29 ++++++++- lib/modules/sapi/geo_i_p.rb | 61 +++++++++++++++++++ lib/modules/sapi/geoip.rb | 59 ------------------ lib/tasks/elibrary/document_files_importer.rb | 1 - lib/tasks/elibrary/import.rake | 4 +- .../manual_document_files_importer.rb | 1 - lib/tasks/export_trade_db.rake | 2 - spec/factories/documents.rb | 4 +- .../cites_captivity_process_spec.rb | 0 .../{ => eu_decisions}/eu_opinion_spec.rb | 0 .../{ => eu_decisions}/eu_suspension_spec.rb | 0 spec/models/{ => events}/cites_cop_spec.rb | 0 .../cites_suspension_notification_spec.rb | 0 .../models/{ => events}/eu_regulation_spec.rb | 0 spec/models/html_to_latex_spec.rb | 2 - spec/models/{ => trade_codes}/purpose_spec.rb | 0 spec/models/{ => trade_codes}/source_spec.rb | 0 spec/models/{ => trade_codes}/term_spec.rb | 0 spec/models/{ => trade_codes}/unit_spec.rb | 0 .../cites_suspension_spec.rb | 0 .../{ => trade_restrictions}/quota_spec.rb | 0 spec/models/zeitwerk_compliance_spec.rb | 8 +++ .../checklist/annotations_spec.rb | 0 .../appendix_population_and_region_spec.rb | 0 .../checklist/appendix_population_spec.rb | 0 .../checklist/appendix_spec.rb | 0 .../checklist/checklist_spec.rb | 0 .../checklist/common_names_spec.rb | 0 .../checklist/higher_taxa_injector_spec.rb | 0 .../checklist/higher_taxa_item_spec.rb | 0 .../checklist/order_spec.rb | 0 .../pdf/history_annotations_key_spec.rb | 0 .../checklist/pdf/history_spec.rb | 0 .../pdf/index_annotations_key_spec.rb | 0 .../checklist/pdf/index_fetcher_spec.rb | 0 .../checklist/scientific_name_spec.rb | 0 .../checklist/synonyms_spec.rb | 0 .../taxon_concept_prefix_matcher_spec.rb | 0 .../checklist/timeline_spec.rb | 0 .../timelines_for_taxon_concept_spec.rb | 0 .../dashboard_stats_species_spec.rb | 0 .../dashboard_stats_trade_spec.rb | 0 .../document_batch_spec.rb | 0 .../document_search_spec.rb | 0 .../geo_entity_search_spec.rb | 0 .../delete_unreassigned_processor_spec.rb | 0 .../full_reassignment_spec.rb | 0 .../lump/constructor_spec.rb | 0 .../output_taxon_concept_processor_spec.rb | 0 .../lump/processor_spec.rb | 0 .../reassignment_copy_processor_spec.rb | 0 .../reassignment_transfer_processor_spec.rb | 0 .../split/constructor_spec.rb | 0 .../output_taxon_concept_processor_spec.rb | 0 .../split/processor_spec.rb | 0 .../status_swap/constructor_spec.rb | 0 .../status_swap/processor_spec.rb | 0 .../status_to_accepted/processor_spec.rb | 0 .../status_to_synonym/constructor_spec.rb | 0 .../output_taxon_concept_processor_spec.rb | 0 .../status_to_synonym/processor_spec.rb | 0 .../species/common_names_export_spec.rb | 0 .../species/documents_export_spec.rb | 0 .../species/hybrid_prefix_matcher_spec.rb | 0 .../invisible_subspecies_search_spec.rb | 0 .../species/listings_export_spec.rb | 0 .../orphaned_taxon_concepts_export_spec.rb | 0 .../species/search_spec.rb | 0 .../species/species_reference_output_spec.rb | 0 .../species/standard_reference_output_spec.rb | 0 .../synonyms_and_trade_names_export_spec.rb | 0 .../taxon_concept_prefix_matcher_spec.rb | 0 .../species/taxon_concepts_export_spec.rb | 0 .../species/trade_name_prefix_matcher_spec.rb | 0 .../species/visible_subspecies_search_spec.rb | 0 .../taxon_concept_data_spec.rb | 0 .../taxon_concept_prefix_matcher_spec.rb | 0 .../{models => services}/trade/filter_spec.rb | 0 .../trade/permit_matcher_spec.rb | 0 .../reported_taxon_concept_resolver_spec.rb | 0 .../trade/sandbox_filter_spec.rb | 0 .../trade/sandbox_spec.rb | 0 .../trade/shipments_comptab_export_spec.rb | 0 .../trade/shipments_export_spec.rb | 0 .../shipments_gross_exports_export_spec.rb | 0 .../shipments_gross_imports_export_spec.rb | 0 .../shipments_net_exports_export_spec.rb | 0 .../shipments_net_imports_export_spec.rb | 0 259 files changed, 153 insertions(+), 131 deletions(-) rename app/{models => controllers/concerns}/trade/trade_data_download_logger.rb (98%) delete mode 100644 app/models/admin.rb rename app/models/{ => cites_processes}/cites_captivity_process.rb (100%) rename app/models/{ => cites_processes}/cites_rst_process.rb (100%) rename app/models/{ => concerns}/m_listing_change.rb (100%) rename app/models/{ => eu_decisions}/eu_opinion.rb (100%) rename app/models/{ => eu_decisions}/eu_suspension.rb (100%) rename app/models/{ => events}/cites_ac.rb (100%) rename app/models/{ => events}/cites_cop.rb (100%) rename app/models/{ => events}/cites_cop_observer.rb (100%) rename app/models/{ => events}/cites_extraordinary_meeting.rb (100%) rename app/models/{ => events}/cites_pc.rb (100%) rename app/models/{ => events}/cites_suspension_notification.rb (100%) rename app/models/{ => events}/cites_suspension_notification_observer.rb (100%) rename app/models/{ => events}/cites_tc.rb (100%) rename app/models/{ => events}/ec_srg.rb (100%) rename app/models/{ => events}/eu_council_regulation.rb (100%) rename app/models/{ => events}/eu_event_observer.rb (100%) rename app/models/{ => events}/eu_implementing_regulation.rb (100%) rename app/models/{ => events}/eu_regulation.rb (100%) rename app/models/{ => events}/eu_regulation_observer.rb (100%) rename app/models/{ => events}/eu_suspension_regulation.rb (100%) rename app/models/{ => events}/eu_suspension_regulation_observer.rb (100%) rename app/models/{ => events}/id_materials.rb (100%) rename app/models/{document => }/proposal_details.rb (92%) rename app/models/{document => }/review_details.rb (92%) rename app/models/{ => trade_codes}/purpose.rb (100%) rename app/models/{ => trade_codes}/source.rb (100%) rename app/models/{ => trade_codes}/term.rb (100%) rename app/models/{ => trade_codes}/unit.rb (100%) rename app/models/{ => trade_restrictions}/cites_suspension.rb (100%) rename app/models/{ => trade_restrictions}/cites_suspension_observer.rb (100%) rename app/models/{ => trade_restrictions}/quota.rb (100%) rename app/models/{ => trade_restrictions}/quota_observer.rb (100%) rename app/serializers/trade/{species_name_appendix_year_validation_rule_serializer.rb => taxon_concept_appendix_year_validation_rule_serializer.rb} (100%) rename app/{models => services}/checklist/checklist.rb (100%) rename app/{models => services}/checklist/checklist_params.rb (100%) rename app/{models => services}/checklist/column_display_name_mapping.rb (100%) rename app/{models => services}/checklist/csv/document.rb (100%) rename app/{models => services}/checklist/csv/history.rb (100%) rename app/{models => services}/checklist/csv/history_content.rb (100%) rename app/{models => services}/checklist/csv/index.rb (100%) rename app/{models => services}/checklist/csv/index_content.rb (100%) rename app/{models => services}/checklist/higher_taxa_injector.rb (100%) rename app/{models => services}/checklist/higher_taxa_item.rb (100%) rename app/{models => services}/checklist/history.rb (100%) rename app/{models => services}/checklist/history_fetcher.rb (100%) rename app/{models => services}/checklist/index.rb (100%) rename app/{models => services}/checklist/index_fetcher.rb (100%) rename app/{models => services}/checklist/json/document.rb (100%) rename app/{models => services}/checklist/json/history.rb (100%) rename app/{models => services}/checklist/json/history_content.rb (100%) rename app/{models => services}/checklist/json/index.rb (100%) rename app/{models => services}/checklist/json/index_content.rb (100%) rename app/{models => services}/checklist/pdf/document.rb (100%) rename app/{models => services}/checklist/pdf/helpers.rb (100%) rename app/{models => services}/checklist/pdf/history.rb (100%) rename app/{models => services}/checklist/pdf/history_annotations_key.rb (100%) rename app/{models => services}/checklist/pdf/history_content.rb (100%) rename app/{models => services}/checklist/pdf/index.rb (100%) rename app/{models => services}/checklist/pdf/index_annotations_key.rb (100%) rename app/{models => services}/checklist/pdf/index_content.rb (100%) rename app/{models => services}/checklist/pdf/index_fetcher.rb (100%) rename app/{models => services}/checklist/pdf/index_query.rb (100%) rename app/{models => services}/checklist/timeline.rb (100%) rename app/{models => services}/checklist/timeline_event.rb (100%) rename app/{models => services}/checklist/timeline_interval.rb (100%) rename app/{models => services}/checklist/timeline_year.rb (100%) rename app/{models => services}/checklist/timelines_for_taxon_concept.rb (100%) rename app/{models => services}/cms_mapping_manager.rb (100%) rename app/{models => services}/country_dictionary.rb (100%) rename app/{models => services}/dashboard_stats.rb (100%) rename app/{models => services}/document_batch.rb (100%) rename app/{models => services}/document_collection_order.rb (100%) rename app/{models => services}/document_search.rb (100%) rename app/{models => services}/document_search_params.rb (100%) rename app/{models => services}/events_by_type_stats.rb (100%) rename app/{models => services}/geo_entity_search.rb (100%) rename app/{models => services}/iucn_mapping_manager.rb (100%) rename app/{models => services}/m_taxon_concept_filter_by_appendix_population_query.rb (100%) rename app/{models => services}/m_taxon_concept_filter_by_appendix_query.rb (100%) rename app/{models => services}/m_taxon_concept_filter_by_id_with_descendants.rb (100%) rename app/{models => services}/m_taxon_concept_filter_by_scientific_name_with_descendants.rb (100%) rename app/{models => services}/nomenclature_change/cascading_notes_processor.rb (100%) rename app/{models => services}/nomenclature_change/delete_unreassigned_processor.rb (100%) rename app/{models => services}/nomenclature_change/full_reassignment.rb (100%) rename app/{models => services}/nomenclature_change/input_taxon_concept_processor.rb (100%) rename app/{models => services}/nomenclature_change/lump/constructor.rb (100%) rename app/{models => services}/nomenclature_change/lump/processor.rb (100%) rename app/{models => services}/nomenclature_change/output_taxon_concept_processor.rb (100%) rename app/{models => services}/nomenclature_change/processor.rb (100%) rename app/{models => services}/nomenclature_change/reassignment_copy_processor.rb (100%) rename app/{models => services}/nomenclature_change/reassignment_processor.rb (100%) rename app/{models => services}/nomenclature_change/reassignment_summarizer.rb (100%) rename app/{models => services}/nomenclature_change/reassignment_transfer_processor.rb (100%) rename app/{models => services}/nomenclature_change/split/constructor.rb (100%) rename app/{models => services}/nomenclature_change/split/processor.rb (100%) rename app/{models => services}/nomenclature_change/status_change/constructor_helpers.rb (100%) rename app/{models => services}/nomenclature_change/status_change/processor_helpers.rb (100%) rename app/{models => services}/nomenclature_change/status_change_processor.rb (100%) rename app/{models => services}/nomenclature_change/status_downgrade_processor.rb (100%) rename app/{models => services}/nomenclature_change/status_swap/constructor.rb (100%) rename app/{models => services}/nomenclature_change/status_swap/processor.rb (100%) rename app/{models => services}/nomenclature_change/status_to_accepted/constructor.rb (100%) rename app/{models => services}/nomenclature_change/status_to_accepted/processor.rb (100%) rename app/{models => services}/nomenclature_change/status_to_synonym/constructor.rb (100%) rename app/{models => services}/nomenclature_change/status_to_synonym/processor.rb (100%) rename app/{models => services}/nomenclature_change/status_upgrade_processor.rb (100%) rename app/{models => services}/nomenclature_change/taxonomic_tree_name_resolver.rb (100%) rename app/{models => services}/nomenclature_change/to_accepted_name_transformation.rb (100%) rename app/{models => services}/nomenclature_change/to_synonym_transformation.rb (100%) rename app/{models => services}/nomenclature_change/trade_shipments_resolver.rb (100%) rename app/{models => services}/search_params.rb (100%) rename app/{models => services}/species/cites_listings_export.rb (100%) rename app/{models => services}/species/cites_processes_export.rb (100%) rename app/{models => services}/species/cms_listings_export.rb (100%) rename app/{models => services}/species/cms_mappings_export.rb (100%) rename app/{models => services}/species/common_names_export.rb (100%) rename app/{models => services}/species/csv_copy_export.rb (100%) rename app/{models => services}/species/documents_export.rb (100%) rename app/{models => services}/species/eu_decisions_export.rb (100%) rename app/{models => services}/species/eu_listings_export.rb (100%) rename app/{models => services}/species/id_manual_documents_export.rb (100%) rename app/{models => services}/species/iucn_mappings_export.rb (100%) rename app/{models => services}/species/listings_export.rb (100%) rename app/{models => services}/species/listings_export_factory.rb (100%) rename app/{models => services}/species/orphaned_taxon_concepts_export.rb (100%) rename app/{models => services}/species/restrictions_export.rb (100%) rename app/{models => services}/species/search.rb (100%) rename app/{models => services}/species/search_params.rb (100%) rename app/{models => services}/species/species_reference_output_export.rb (100%) rename app/{models => services}/species/standard_reference_output_export.rb (100%) rename app/{models => services}/species/synonyms_and_trade_names_export.rb (100%) rename app/{models => services}/species/taxon_concept_prefix_matcher.rb (100%) rename app/{models => services}/species/taxon_concepts_distributions_export.rb (100%) rename app/{models => services}/species/taxon_concepts_names_export.rb (100%) rename app/{models => services}/taxon_concept_data.rb (100%) rename app/{models => services}/taxon_concept_matcher.rb (100%) rename app/{models => services}/taxon_concept_prefix_matcher.rb (100%) rename app/{models => services}/taxon_concept_view_stats.rb (100%) rename app/{models => services}/trade/batch_update.rb (100%) rename app/{models => services}/trade/filter.rb (100%) rename app/{models => services}/trade/permit_matcher.rb (100%) rename app/{models => services}/trade/reported_taxon_concept_resolver.rb (100%) rename app/{models => services}/trade/sandbox.rb (100%) rename app/{models => services}/trade/sandbox_filter.rb (100%) rename app/{models => services}/trade/sandbox_search_params.rb (100%) rename app/{models => services}/trade/search_params.rb (100%) rename app/{models => services}/trade/shipment_report_queries.rb (100%) rename app/{models => services}/trade/shipments_comptab_export.rb (100%) rename app/{models => services}/trade/shipments_export.rb (100%) rename app/{models => services}/trade/shipments_export_factory.rb (100%) rename app/{models => services}/trade/shipments_gross_exports_export.rb (100%) rename app/{models => services}/trade/shipments_gross_imports_export.rb (100%) rename app/{models => services}/trade/shipments_net_exports_export.rb (100%) rename app/{models => services}/trade/shipments_net_imports_export.rb (100%) rename app/{models => services}/trade/trade_data_downloads_export.rb (100%) delete mode 100644 lib/modules/hash_deep_slice.rb create mode 100644 lib/modules/sapi/geo_i_p.rb delete mode 100644 lib/modules/sapi/geoip.rb rename spec/models/{ => cites_processes}/cites_captivity_process_spec.rb (100%) rename spec/models/{ => eu_decisions}/eu_opinion_spec.rb (100%) rename spec/models/{ => eu_decisions}/eu_suspension_spec.rb (100%) rename spec/models/{ => events}/cites_cop_spec.rb (100%) rename spec/models/{ => events}/cites_suspension_notification_spec.rb (100%) rename spec/models/{ => events}/eu_regulation_spec.rb (100%) rename spec/models/{ => trade_codes}/purpose_spec.rb (100%) rename spec/models/{ => trade_codes}/source_spec.rb (100%) rename spec/models/{ => trade_codes}/term_spec.rb (100%) rename spec/models/{ => trade_codes}/unit_spec.rb (100%) rename spec/models/{ => trade_restrictions}/cites_suspension_spec.rb (100%) rename spec/models/{ => trade_restrictions}/quota_spec.rb (100%) create mode 100644 spec/models/zeitwerk_compliance_spec.rb rename spec/{models => services}/checklist/annotations_spec.rb (100%) rename spec/{models => services}/checklist/appendix_population_and_region_spec.rb (100%) rename spec/{models => services}/checklist/appendix_population_spec.rb (100%) rename spec/{models => services}/checklist/appendix_spec.rb (100%) rename spec/{models => services}/checklist/checklist_spec.rb (100%) rename spec/{models => services}/checklist/common_names_spec.rb (100%) rename spec/{models => services}/checklist/higher_taxa_injector_spec.rb (100%) rename spec/{models => services}/checklist/higher_taxa_item_spec.rb (100%) rename spec/{models => services}/checklist/order_spec.rb (100%) rename spec/{models => services}/checklist/pdf/history_annotations_key_spec.rb (100%) rename spec/{models => services}/checklist/pdf/history_spec.rb (100%) rename spec/{models => services}/checklist/pdf/index_annotations_key_spec.rb (100%) rename spec/{models => services}/checklist/pdf/index_fetcher_spec.rb (100%) rename spec/{models => services}/checklist/scientific_name_spec.rb (100%) rename spec/{models => services}/checklist/synonyms_spec.rb (100%) rename spec/{models => services}/checklist/taxon_concept_prefix_matcher_spec.rb (100%) rename spec/{models => services}/checklist/timeline_spec.rb (100%) rename spec/{models => services}/checklist/timelines_for_taxon_concept_spec.rb (100%) rename spec/{models => services}/dashboard_stats_species_spec.rb (100%) rename spec/{models => services}/dashboard_stats_trade_spec.rb (100%) rename spec/{models => services}/document_batch_spec.rb (100%) rename spec/{models => services}/document_search_spec.rb (100%) rename spec/{models => services}/geo_entity_search_spec.rb (100%) rename spec/{models => services}/nomenclature_change/delete_unreassigned_processor_spec.rb (100%) rename spec/{models => services}/nomenclature_change/full_reassignment_spec.rb (100%) rename spec/{models => services}/nomenclature_change/lump/constructor_spec.rb (100%) rename spec/{models => services}/nomenclature_change/lump/output_taxon_concept_processor_spec.rb (100%) rename spec/{models => services}/nomenclature_change/lump/processor_spec.rb (100%) rename spec/{models => services}/nomenclature_change/reassignment_copy_processor_spec.rb (100%) rename spec/{models => services}/nomenclature_change/reassignment_transfer_processor_spec.rb (100%) rename spec/{models => services}/nomenclature_change/split/constructor_spec.rb (100%) rename spec/{models => services}/nomenclature_change/split/output_taxon_concept_processor_spec.rb (100%) rename spec/{models => services}/nomenclature_change/split/processor_spec.rb (100%) rename spec/{models => services}/nomenclature_change/status_swap/constructor_spec.rb (100%) rename spec/{models => services}/nomenclature_change/status_swap/processor_spec.rb (100%) rename spec/{models => services}/nomenclature_change/status_to_accepted/processor_spec.rb (100%) rename spec/{models => services}/nomenclature_change/status_to_synonym/constructor_spec.rb (100%) rename spec/{models => services}/nomenclature_change/status_to_synonym/output_taxon_concept_processor_spec.rb (100%) rename spec/{models => services}/nomenclature_change/status_to_synonym/processor_spec.rb (100%) rename spec/{models => services}/species/common_names_export_spec.rb (100%) rename spec/{models => services}/species/documents_export_spec.rb (100%) rename spec/{models => services}/species/hybrid_prefix_matcher_spec.rb (100%) rename spec/{models => services}/species/invisible_subspecies_search_spec.rb (100%) rename spec/{models => services}/species/listings_export_spec.rb (100%) rename spec/{models => services}/species/orphaned_taxon_concepts_export_spec.rb (100%) rename spec/{models => services}/species/search_spec.rb (100%) rename spec/{models => services}/species/species_reference_output_spec.rb (100%) rename spec/{models => services}/species/standard_reference_output_spec.rb (100%) rename spec/{models => services}/species/synonyms_and_trade_names_export_spec.rb (100%) rename spec/{models => services}/species/taxon_concept_prefix_matcher_spec.rb (100%) rename spec/{models => services}/species/taxon_concepts_export_spec.rb (100%) rename spec/{models => services}/species/trade_name_prefix_matcher_spec.rb (100%) rename spec/{models => services}/species/visible_subspecies_search_spec.rb (100%) rename spec/{models => services}/taxon_concept_data_spec.rb (100%) rename spec/{models => services}/taxon_concept_prefix_matcher_spec.rb (100%) rename spec/{models => services}/trade/filter_spec.rb (100%) rename spec/{models => services}/trade/permit_matcher_spec.rb (100%) rename spec/{models => services}/trade/reported_taxon_concept_resolver_spec.rb (100%) rename spec/{models => services}/trade/sandbox_filter_spec.rb (100%) rename spec/{models => services}/trade/sandbox_spec.rb (100%) rename spec/{models => services}/trade/shipments_comptab_export_spec.rb (100%) rename spec/{models => services}/trade/shipments_export_spec.rb (100%) rename spec/{models => services}/trade/shipments_gross_exports_export_spec.rb (100%) rename spec/{models => services}/trade/shipments_gross_imports_export_spec.rb (100%) rename spec/{models => services}/trade/shipments_net_exports_export_spec.rb (100%) rename spec/{models => services}/trade/shipments_net_imports_export_spec.rb (100%) diff --git a/Capfile b/Capfile index 666fbafec..7699784be 100644 --- a/Capfile +++ b/Capfile @@ -9,7 +9,7 @@ require 'capistrano/setup' require 'capistrano/deploy' -require "capistrano/scm/git" +require 'capistrano/scm/git' install_plugin Capistrano::SCM::Git diff --git a/app/controllers/admin/documents_controller.rb b/app/controllers/admin/documents_controller.rb index 7c49a0d4a..9867fcd28 100644 --- a/app/controllers/admin/documents_controller.rb +++ b/app/controllers/admin/documents_controller.rb @@ -21,9 +21,9 @@ def edit edit! do |format| load_associations if @document.is_a?(Document::ReviewOfSignificantTrade) - @document.review_details ||= Document::ReviewDetails.new + @document.review_details ||= ReviewDetails.new elsif @document.is_a?(Document::Proposal) - @document.proposal_details ||= Document::ProposalDetails.new + @document.proposal_details ||= ProposalDetails.new end @document.citations.build format.html { render 'new' } diff --git a/app/controllers/api/v1/documents_controller.rb b/app/controllers/api/v1/documents_controller.rb index 6a209523e..72530d605 100644 --- a/app/controllers/api/v1/documents_controller.rb +++ b/app/controllers/api/v1/documents_controller.rb @@ -84,8 +84,6 @@ def show end def download_zip - require 'zip' - @documents = Document.find(params[:ids].split(',')) t = Tempfile.new('tmp-zip-' + request.remote_ip) diff --git a/app/controllers/checklist/documents_controller.rb b/app/controllers/checklist/documents_controller.rb index 0217d9da1..be3d57fd2 100644 --- a/app/controllers/checklist/documents_controller.rb +++ b/app/controllers/checklist/documents_controller.rb @@ -77,8 +77,6 @@ def render_403 end def full_volume_downloader - require 'zip' - t = Tempfile.new('tmp-zip-' + request.remote_ip) missing_files = [] vol_path = [Rails.root, '/public/ID_manual_volumes/', params['locale'], '/'].join diff --git a/app/models/trade/trade_data_download_logger.rb b/app/controllers/concerns/trade/trade_data_download_logger.rb similarity index 98% rename from app/models/trade/trade_data_download_logger.rb rename to app/controllers/concerns/trade/trade_data_download_logger.rb index 11ebae4cc..ed2c46e35 100644 --- a/app/models/trade/trade_data_download_logger.rb +++ b/app/controllers/concerns/trade/trade_data_download_logger.rb @@ -1,4 +1,4 @@ -require 'sapi/geoip' +require 'sapi/geo_i_p' module Trade::TradeDataDownloadLogger module_function diff --git a/app/models/admin.rb b/app/models/admin.rb deleted file mode 100644 index a38e3f484..000000000 --- a/app/models/admin.rb +++ /dev/null @@ -1,5 +0,0 @@ -module Admin - def self.table_name_prefix - 'admin_' - end -end diff --git a/app/models/cites_captivity_process.rb b/app/models/cites_processes/cites_captivity_process.rb similarity index 100% rename from app/models/cites_captivity_process.rb rename to app/models/cites_processes/cites_captivity_process.rb diff --git a/app/models/cites_rst_process.rb b/app/models/cites_processes/cites_rst_process.rb similarity index 100% rename from app/models/cites_rst_process.rb rename to app/models/cites_processes/cites_rst_process.rb diff --git a/app/models/m_listing_change.rb b/app/models/concerns/m_listing_change.rb similarity index 100% rename from app/models/m_listing_change.rb rename to app/models/concerns/m_listing_change.rb diff --git a/app/models/document/proposal.rb b/app/models/document/proposal.rb index 49ec4dc02..100a7c14c 100644 --- a/app/models/document/proposal.rb +++ b/app/models/document/proposal.rb @@ -33,7 +33,7 @@ def self.display_name # attr_accessible :proposal_details_attributes has_one :proposal_details, - :class_name => 'Document::ProposalDetails', + :class_name => 'ProposalDetails', :foreign_key => 'document_id', dependent: :destroy accepts_nested_attributes_for :proposal_details, :allow_destroy => true diff --git a/app/models/document/review_of_significant_trade.rb b/app/models/document/review_of_significant_trade.rb index f834612f6..09b5e43a5 100644 --- a/app/models/document/review_of_significant_trade.rb +++ b/app/models/document/review_of_significant_trade.rb @@ -33,7 +33,7 @@ def self.display_name # attr_accessible :review_details_attributes has_one :review_details, - :class_name => 'Document::ReviewDetails', + :class_name => 'ReviewDetails', :foreign_key => 'document_id', dependent: :destroy accepts_nested_attributes_for :review_details, :allow_destroy => true diff --git a/app/models/eu_opinion.rb b/app/models/eu_decisions/eu_opinion.rb similarity index 100% rename from app/models/eu_opinion.rb rename to app/models/eu_decisions/eu_opinion.rb diff --git a/app/models/eu_suspension.rb b/app/models/eu_decisions/eu_suspension.rb similarity index 100% rename from app/models/eu_suspension.rb rename to app/models/eu_decisions/eu_suspension.rb diff --git a/app/models/cites_ac.rb b/app/models/events/cites_ac.rb similarity index 100% rename from app/models/cites_ac.rb rename to app/models/events/cites_ac.rb diff --git a/app/models/cites_cop.rb b/app/models/events/cites_cop.rb similarity index 100% rename from app/models/cites_cop.rb rename to app/models/events/cites_cop.rb diff --git a/app/models/cites_cop_observer.rb b/app/models/events/cites_cop_observer.rb similarity index 100% rename from app/models/cites_cop_observer.rb rename to app/models/events/cites_cop_observer.rb diff --git a/app/models/cites_extraordinary_meeting.rb b/app/models/events/cites_extraordinary_meeting.rb similarity index 100% rename from app/models/cites_extraordinary_meeting.rb rename to app/models/events/cites_extraordinary_meeting.rb diff --git a/app/models/cites_pc.rb b/app/models/events/cites_pc.rb similarity index 100% rename from app/models/cites_pc.rb rename to app/models/events/cites_pc.rb diff --git a/app/models/cites_suspension_notification.rb b/app/models/events/cites_suspension_notification.rb similarity index 100% rename from app/models/cites_suspension_notification.rb rename to app/models/events/cites_suspension_notification.rb diff --git a/app/models/cites_suspension_notification_observer.rb b/app/models/events/cites_suspension_notification_observer.rb similarity index 100% rename from app/models/cites_suspension_notification_observer.rb rename to app/models/events/cites_suspension_notification_observer.rb diff --git a/app/models/cites_tc.rb b/app/models/events/cites_tc.rb similarity index 100% rename from app/models/cites_tc.rb rename to app/models/events/cites_tc.rb diff --git a/app/models/ec_srg.rb b/app/models/events/ec_srg.rb similarity index 100% rename from app/models/ec_srg.rb rename to app/models/events/ec_srg.rb diff --git a/app/models/eu_council_regulation.rb b/app/models/events/eu_council_regulation.rb similarity index 100% rename from app/models/eu_council_regulation.rb rename to app/models/events/eu_council_regulation.rb diff --git a/app/models/eu_event_observer.rb b/app/models/events/eu_event_observer.rb similarity index 100% rename from app/models/eu_event_observer.rb rename to app/models/events/eu_event_observer.rb diff --git a/app/models/eu_implementing_regulation.rb b/app/models/events/eu_implementing_regulation.rb similarity index 100% rename from app/models/eu_implementing_regulation.rb rename to app/models/events/eu_implementing_regulation.rb diff --git a/app/models/eu_regulation.rb b/app/models/events/eu_regulation.rb similarity index 100% rename from app/models/eu_regulation.rb rename to app/models/events/eu_regulation.rb diff --git a/app/models/eu_regulation_observer.rb b/app/models/events/eu_regulation_observer.rb similarity index 100% rename from app/models/eu_regulation_observer.rb rename to app/models/events/eu_regulation_observer.rb diff --git a/app/models/eu_suspension_regulation.rb b/app/models/events/eu_suspension_regulation.rb similarity index 100% rename from app/models/eu_suspension_regulation.rb rename to app/models/events/eu_suspension_regulation.rb diff --git a/app/models/eu_suspension_regulation_observer.rb b/app/models/events/eu_suspension_regulation_observer.rb similarity index 100% rename from app/models/eu_suspension_regulation_observer.rb rename to app/models/events/eu_suspension_regulation_observer.rb diff --git a/app/models/id_materials.rb b/app/models/events/id_materials.rb similarity index 100% rename from app/models/id_materials.rb rename to app/models/events/id_materials.rb diff --git a/app/models/document/proposal_details.rb b/app/models/proposal_details.rb similarity index 92% rename from app/models/document/proposal_details.rb rename to app/models/proposal_details.rb index f95e7373f..af43f21bc 100644 --- a/app/models/document/proposal_details.rb +++ b/app/models/proposal_details.rb @@ -12,7 +12,7 @@ # proposal_number :text # -class Document::ProposalDetails < ApplicationRecord +class ProposalDetails < ApplicationRecord # Used in DocumentsController # attr_accessible :document_id, :proposal_nature, :proposal_outcome_id, # :representation, :proposal_number diff --git a/app/models/document/review_details.rb b/app/models/review_details.rb similarity index 92% rename from app/models/document/review_details.rb rename to app/models/review_details.rb index eb9e62d26..f12e1d8ab 100644 --- a/app/models/document/review_details.rb +++ b/app/models/review_details.rb @@ -11,7 +11,7 @@ # updated_at :datetime not null # -class Document::ReviewDetails < ApplicationRecord +class ReviewDetails < ApplicationRecord # Used by DocumentController. # attr_accessible :document_id, :review_phase_id, :process_stage_id, :recommended_category self.table_name = 'review_details' diff --git a/app/models/purpose.rb b/app/models/trade_codes/purpose.rb similarity index 100% rename from app/models/purpose.rb rename to app/models/trade_codes/purpose.rb diff --git a/app/models/source.rb b/app/models/trade_codes/source.rb similarity index 100% rename from app/models/source.rb rename to app/models/trade_codes/source.rb diff --git a/app/models/term.rb b/app/models/trade_codes/term.rb similarity index 100% rename from app/models/term.rb rename to app/models/trade_codes/term.rb diff --git a/app/models/unit.rb b/app/models/trade_codes/unit.rb similarity index 100% rename from app/models/unit.rb rename to app/models/trade_codes/unit.rb diff --git a/app/models/cites_suspension.rb b/app/models/trade_restrictions/cites_suspension.rb similarity index 100% rename from app/models/cites_suspension.rb rename to app/models/trade_restrictions/cites_suspension.rb diff --git a/app/models/cites_suspension_observer.rb b/app/models/trade_restrictions/cites_suspension_observer.rb similarity index 100% rename from app/models/cites_suspension_observer.rb rename to app/models/trade_restrictions/cites_suspension_observer.rb diff --git a/app/models/quota.rb b/app/models/trade_restrictions/quota.rb similarity index 100% rename from app/models/quota.rb rename to app/models/trade_restrictions/quota.rb diff --git a/app/models/quota_observer.rb b/app/models/trade_restrictions/quota_observer.rb similarity index 100% rename from app/models/quota_observer.rb rename to app/models/trade_restrictions/quota_observer.rb diff --git a/app/serializers/trade/species_name_appendix_year_validation_rule_serializer.rb b/app/serializers/trade/taxon_concept_appendix_year_validation_rule_serializer.rb similarity index 100% rename from app/serializers/trade/species_name_appendix_year_validation_rule_serializer.rb rename to app/serializers/trade/taxon_concept_appendix_year_validation_rule_serializer.rb diff --git a/app/models/checklist/checklist.rb b/app/services/checklist/checklist.rb similarity index 100% rename from app/models/checklist/checklist.rb rename to app/services/checklist/checklist.rb diff --git a/app/models/checklist/checklist_params.rb b/app/services/checklist/checklist_params.rb similarity index 100% rename from app/models/checklist/checklist_params.rb rename to app/services/checklist/checklist_params.rb diff --git a/app/models/checklist/column_display_name_mapping.rb b/app/services/checklist/column_display_name_mapping.rb similarity index 100% rename from app/models/checklist/column_display_name_mapping.rb rename to app/services/checklist/column_display_name_mapping.rb diff --git a/app/models/checklist/csv/document.rb b/app/services/checklist/csv/document.rb similarity index 100% rename from app/models/checklist/csv/document.rb rename to app/services/checklist/csv/document.rb diff --git a/app/models/checklist/csv/history.rb b/app/services/checklist/csv/history.rb similarity index 100% rename from app/models/checklist/csv/history.rb rename to app/services/checklist/csv/history.rb diff --git a/app/models/checklist/csv/history_content.rb b/app/services/checklist/csv/history_content.rb similarity index 100% rename from app/models/checklist/csv/history_content.rb rename to app/services/checklist/csv/history_content.rb diff --git a/app/models/checklist/csv/index.rb b/app/services/checklist/csv/index.rb similarity index 100% rename from app/models/checklist/csv/index.rb rename to app/services/checklist/csv/index.rb diff --git a/app/models/checklist/csv/index_content.rb b/app/services/checklist/csv/index_content.rb similarity index 100% rename from app/models/checklist/csv/index_content.rb rename to app/services/checklist/csv/index_content.rb diff --git a/app/models/checklist/higher_taxa_injector.rb b/app/services/checklist/higher_taxa_injector.rb similarity index 100% rename from app/models/checklist/higher_taxa_injector.rb rename to app/services/checklist/higher_taxa_injector.rb diff --git a/app/models/checklist/higher_taxa_item.rb b/app/services/checklist/higher_taxa_item.rb similarity index 100% rename from app/models/checklist/higher_taxa_item.rb rename to app/services/checklist/higher_taxa_item.rb diff --git a/app/models/checklist/history.rb b/app/services/checklist/history.rb similarity index 100% rename from app/models/checklist/history.rb rename to app/services/checklist/history.rb diff --git a/app/models/checklist/history_fetcher.rb b/app/services/checklist/history_fetcher.rb similarity index 100% rename from app/models/checklist/history_fetcher.rb rename to app/services/checklist/history_fetcher.rb diff --git a/app/models/checklist/index.rb b/app/services/checklist/index.rb similarity index 100% rename from app/models/checklist/index.rb rename to app/services/checklist/index.rb diff --git a/app/models/checklist/index_fetcher.rb b/app/services/checklist/index_fetcher.rb similarity index 100% rename from app/models/checklist/index_fetcher.rb rename to app/services/checklist/index_fetcher.rb diff --git a/app/models/checklist/json/document.rb b/app/services/checklist/json/document.rb similarity index 100% rename from app/models/checklist/json/document.rb rename to app/services/checklist/json/document.rb diff --git a/app/models/checklist/json/history.rb b/app/services/checklist/json/history.rb similarity index 100% rename from app/models/checklist/json/history.rb rename to app/services/checklist/json/history.rb diff --git a/app/models/checklist/json/history_content.rb b/app/services/checklist/json/history_content.rb similarity index 100% rename from app/models/checklist/json/history_content.rb rename to app/services/checklist/json/history_content.rb diff --git a/app/models/checklist/json/index.rb b/app/services/checklist/json/index.rb similarity index 100% rename from app/models/checklist/json/index.rb rename to app/services/checklist/json/index.rb diff --git a/app/models/checklist/json/index_content.rb b/app/services/checklist/json/index_content.rb similarity index 100% rename from app/models/checklist/json/index_content.rb rename to app/services/checklist/json/index_content.rb diff --git a/app/models/checklist/pdf/document.rb b/app/services/checklist/pdf/document.rb similarity index 100% rename from app/models/checklist/pdf/document.rb rename to app/services/checklist/pdf/document.rb diff --git a/app/models/checklist/pdf/helpers.rb b/app/services/checklist/pdf/helpers.rb similarity index 100% rename from app/models/checklist/pdf/helpers.rb rename to app/services/checklist/pdf/helpers.rb diff --git a/app/models/checklist/pdf/history.rb b/app/services/checklist/pdf/history.rb similarity index 100% rename from app/models/checklist/pdf/history.rb rename to app/services/checklist/pdf/history.rb diff --git a/app/models/checklist/pdf/history_annotations_key.rb b/app/services/checklist/pdf/history_annotations_key.rb similarity index 100% rename from app/models/checklist/pdf/history_annotations_key.rb rename to app/services/checklist/pdf/history_annotations_key.rb diff --git a/app/models/checklist/pdf/history_content.rb b/app/services/checklist/pdf/history_content.rb similarity index 100% rename from app/models/checklist/pdf/history_content.rb rename to app/services/checklist/pdf/history_content.rb diff --git a/app/models/checklist/pdf/index.rb b/app/services/checklist/pdf/index.rb similarity index 100% rename from app/models/checklist/pdf/index.rb rename to app/services/checklist/pdf/index.rb diff --git a/app/models/checklist/pdf/index_annotations_key.rb b/app/services/checklist/pdf/index_annotations_key.rb similarity index 100% rename from app/models/checklist/pdf/index_annotations_key.rb rename to app/services/checklist/pdf/index_annotations_key.rb diff --git a/app/models/checklist/pdf/index_content.rb b/app/services/checklist/pdf/index_content.rb similarity index 100% rename from app/models/checklist/pdf/index_content.rb rename to app/services/checklist/pdf/index_content.rb diff --git a/app/models/checklist/pdf/index_fetcher.rb b/app/services/checklist/pdf/index_fetcher.rb similarity index 100% rename from app/models/checklist/pdf/index_fetcher.rb rename to app/services/checklist/pdf/index_fetcher.rb diff --git a/app/models/checklist/pdf/index_query.rb b/app/services/checklist/pdf/index_query.rb similarity index 100% rename from app/models/checklist/pdf/index_query.rb rename to app/services/checklist/pdf/index_query.rb diff --git a/app/models/checklist/timeline.rb b/app/services/checklist/timeline.rb similarity index 100% rename from app/models/checklist/timeline.rb rename to app/services/checklist/timeline.rb diff --git a/app/models/checklist/timeline_event.rb b/app/services/checklist/timeline_event.rb similarity index 100% rename from app/models/checklist/timeline_event.rb rename to app/services/checklist/timeline_event.rb diff --git a/app/models/checklist/timeline_interval.rb b/app/services/checklist/timeline_interval.rb similarity index 100% rename from app/models/checklist/timeline_interval.rb rename to app/services/checklist/timeline_interval.rb diff --git a/app/models/checklist/timeline_year.rb b/app/services/checklist/timeline_year.rb similarity index 100% rename from app/models/checklist/timeline_year.rb rename to app/services/checklist/timeline_year.rb diff --git a/app/models/checklist/timelines_for_taxon_concept.rb b/app/services/checklist/timelines_for_taxon_concept.rb similarity index 100% rename from app/models/checklist/timelines_for_taxon_concept.rb rename to app/services/checklist/timelines_for_taxon_concept.rb diff --git a/app/models/cms_mapping_manager.rb b/app/services/cms_mapping_manager.rb similarity index 100% rename from app/models/cms_mapping_manager.rb rename to app/services/cms_mapping_manager.rb diff --git a/app/models/country_dictionary.rb b/app/services/country_dictionary.rb similarity index 100% rename from app/models/country_dictionary.rb rename to app/services/country_dictionary.rb diff --git a/app/models/dashboard_stats.rb b/app/services/dashboard_stats.rb similarity index 100% rename from app/models/dashboard_stats.rb rename to app/services/dashboard_stats.rb diff --git a/app/models/document_batch.rb b/app/services/document_batch.rb similarity index 100% rename from app/models/document_batch.rb rename to app/services/document_batch.rb diff --git a/app/models/document_collection_order.rb b/app/services/document_collection_order.rb similarity index 100% rename from app/models/document_collection_order.rb rename to app/services/document_collection_order.rb diff --git a/app/models/document_search.rb b/app/services/document_search.rb similarity index 100% rename from app/models/document_search.rb rename to app/services/document_search.rb diff --git a/app/models/document_search_params.rb b/app/services/document_search_params.rb similarity index 100% rename from app/models/document_search_params.rb rename to app/services/document_search_params.rb diff --git a/app/models/events_by_type_stats.rb b/app/services/events_by_type_stats.rb similarity index 100% rename from app/models/events_by_type_stats.rb rename to app/services/events_by_type_stats.rb diff --git a/app/models/geo_entity_search.rb b/app/services/geo_entity_search.rb similarity index 100% rename from app/models/geo_entity_search.rb rename to app/services/geo_entity_search.rb diff --git a/app/models/iucn_mapping_manager.rb b/app/services/iucn_mapping_manager.rb similarity index 100% rename from app/models/iucn_mapping_manager.rb rename to app/services/iucn_mapping_manager.rb diff --git a/app/models/m_taxon_concept_filter_by_appendix_population_query.rb b/app/services/m_taxon_concept_filter_by_appendix_population_query.rb similarity index 100% rename from app/models/m_taxon_concept_filter_by_appendix_population_query.rb rename to app/services/m_taxon_concept_filter_by_appendix_population_query.rb diff --git a/app/models/m_taxon_concept_filter_by_appendix_query.rb b/app/services/m_taxon_concept_filter_by_appendix_query.rb similarity index 100% rename from app/models/m_taxon_concept_filter_by_appendix_query.rb rename to app/services/m_taxon_concept_filter_by_appendix_query.rb diff --git a/app/models/m_taxon_concept_filter_by_id_with_descendants.rb b/app/services/m_taxon_concept_filter_by_id_with_descendants.rb similarity index 100% rename from app/models/m_taxon_concept_filter_by_id_with_descendants.rb rename to app/services/m_taxon_concept_filter_by_id_with_descendants.rb diff --git a/app/models/m_taxon_concept_filter_by_scientific_name_with_descendants.rb b/app/services/m_taxon_concept_filter_by_scientific_name_with_descendants.rb similarity index 100% rename from app/models/m_taxon_concept_filter_by_scientific_name_with_descendants.rb rename to app/services/m_taxon_concept_filter_by_scientific_name_with_descendants.rb diff --git a/app/models/nomenclature_change/cascading_notes_processor.rb b/app/services/nomenclature_change/cascading_notes_processor.rb similarity index 100% rename from app/models/nomenclature_change/cascading_notes_processor.rb rename to app/services/nomenclature_change/cascading_notes_processor.rb diff --git a/app/models/nomenclature_change/delete_unreassigned_processor.rb b/app/services/nomenclature_change/delete_unreassigned_processor.rb similarity index 100% rename from app/models/nomenclature_change/delete_unreassigned_processor.rb rename to app/services/nomenclature_change/delete_unreassigned_processor.rb diff --git a/app/models/nomenclature_change/full_reassignment.rb b/app/services/nomenclature_change/full_reassignment.rb similarity index 100% rename from app/models/nomenclature_change/full_reassignment.rb rename to app/services/nomenclature_change/full_reassignment.rb diff --git a/app/models/nomenclature_change/input_taxon_concept_processor.rb b/app/services/nomenclature_change/input_taxon_concept_processor.rb similarity index 100% rename from app/models/nomenclature_change/input_taxon_concept_processor.rb rename to app/services/nomenclature_change/input_taxon_concept_processor.rb diff --git a/app/models/nomenclature_change/lump/constructor.rb b/app/services/nomenclature_change/lump/constructor.rb similarity index 100% rename from app/models/nomenclature_change/lump/constructor.rb rename to app/services/nomenclature_change/lump/constructor.rb diff --git a/app/models/nomenclature_change/lump/processor.rb b/app/services/nomenclature_change/lump/processor.rb similarity index 100% rename from app/models/nomenclature_change/lump/processor.rb rename to app/services/nomenclature_change/lump/processor.rb diff --git a/app/models/nomenclature_change/output_taxon_concept_processor.rb b/app/services/nomenclature_change/output_taxon_concept_processor.rb similarity index 100% rename from app/models/nomenclature_change/output_taxon_concept_processor.rb rename to app/services/nomenclature_change/output_taxon_concept_processor.rb diff --git a/app/models/nomenclature_change/processor.rb b/app/services/nomenclature_change/processor.rb similarity index 100% rename from app/models/nomenclature_change/processor.rb rename to app/services/nomenclature_change/processor.rb diff --git a/app/models/nomenclature_change/reassignment_copy_processor.rb b/app/services/nomenclature_change/reassignment_copy_processor.rb similarity index 100% rename from app/models/nomenclature_change/reassignment_copy_processor.rb rename to app/services/nomenclature_change/reassignment_copy_processor.rb diff --git a/app/models/nomenclature_change/reassignment_processor.rb b/app/services/nomenclature_change/reassignment_processor.rb similarity index 100% rename from app/models/nomenclature_change/reassignment_processor.rb rename to app/services/nomenclature_change/reassignment_processor.rb diff --git a/app/models/nomenclature_change/reassignment_summarizer.rb b/app/services/nomenclature_change/reassignment_summarizer.rb similarity index 100% rename from app/models/nomenclature_change/reassignment_summarizer.rb rename to app/services/nomenclature_change/reassignment_summarizer.rb diff --git a/app/models/nomenclature_change/reassignment_transfer_processor.rb b/app/services/nomenclature_change/reassignment_transfer_processor.rb similarity index 100% rename from app/models/nomenclature_change/reassignment_transfer_processor.rb rename to app/services/nomenclature_change/reassignment_transfer_processor.rb diff --git a/app/models/nomenclature_change/split/constructor.rb b/app/services/nomenclature_change/split/constructor.rb similarity index 100% rename from app/models/nomenclature_change/split/constructor.rb rename to app/services/nomenclature_change/split/constructor.rb diff --git a/app/models/nomenclature_change/split/processor.rb b/app/services/nomenclature_change/split/processor.rb similarity index 100% rename from app/models/nomenclature_change/split/processor.rb rename to app/services/nomenclature_change/split/processor.rb diff --git a/app/models/nomenclature_change/status_change/constructor_helpers.rb b/app/services/nomenclature_change/status_change/constructor_helpers.rb similarity index 100% rename from app/models/nomenclature_change/status_change/constructor_helpers.rb rename to app/services/nomenclature_change/status_change/constructor_helpers.rb diff --git a/app/models/nomenclature_change/status_change/processor_helpers.rb b/app/services/nomenclature_change/status_change/processor_helpers.rb similarity index 100% rename from app/models/nomenclature_change/status_change/processor_helpers.rb rename to app/services/nomenclature_change/status_change/processor_helpers.rb diff --git a/app/models/nomenclature_change/status_change_processor.rb b/app/services/nomenclature_change/status_change_processor.rb similarity index 100% rename from app/models/nomenclature_change/status_change_processor.rb rename to app/services/nomenclature_change/status_change_processor.rb diff --git a/app/models/nomenclature_change/status_downgrade_processor.rb b/app/services/nomenclature_change/status_downgrade_processor.rb similarity index 100% rename from app/models/nomenclature_change/status_downgrade_processor.rb rename to app/services/nomenclature_change/status_downgrade_processor.rb diff --git a/app/models/nomenclature_change/status_swap/constructor.rb b/app/services/nomenclature_change/status_swap/constructor.rb similarity index 100% rename from app/models/nomenclature_change/status_swap/constructor.rb rename to app/services/nomenclature_change/status_swap/constructor.rb diff --git a/app/models/nomenclature_change/status_swap/processor.rb b/app/services/nomenclature_change/status_swap/processor.rb similarity index 100% rename from app/models/nomenclature_change/status_swap/processor.rb rename to app/services/nomenclature_change/status_swap/processor.rb diff --git a/app/models/nomenclature_change/status_to_accepted/constructor.rb b/app/services/nomenclature_change/status_to_accepted/constructor.rb similarity index 100% rename from app/models/nomenclature_change/status_to_accepted/constructor.rb rename to app/services/nomenclature_change/status_to_accepted/constructor.rb diff --git a/app/models/nomenclature_change/status_to_accepted/processor.rb b/app/services/nomenclature_change/status_to_accepted/processor.rb similarity index 100% rename from app/models/nomenclature_change/status_to_accepted/processor.rb rename to app/services/nomenclature_change/status_to_accepted/processor.rb diff --git a/app/models/nomenclature_change/status_to_synonym/constructor.rb b/app/services/nomenclature_change/status_to_synonym/constructor.rb similarity index 100% rename from app/models/nomenclature_change/status_to_synonym/constructor.rb rename to app/services/nomenclature_change/status_to_synonym/constructor.rb diff --git a/app/models/nomenclature_change/status_to_synonym/processor.rb b/app/services/nomenclature_change/status_to_synonym/processor.rb similarity index 100% rename from app/models/nomenclature_change/status_to_synonym/processor.rb rename to app/services/nomenclature_change/status_to_synonym/processor.rb diff --git a/app/models/nomenclature_change/status_upgrade_processor.rb b/app/services/nomenclature_change/status_upgrade_processor.rb similarity index 100% rename from app/models/nomenclature_change/status_upgrade_processor.rb rename to app/services/nomenclature_change/status_upgrade_processor.rb diff --git a/app/models/nomenclature_change/taxonomic_tree_name_resolver.rb b/app/services/nomenclature_change/taxonomic_tree_name_resolver.rb similarity index 100% rename from app/models/nomenclature_change/taxonomic_tree_name_resolver.rb rename to app/services/nomenclature_change/taxonomic_tree_name_resolver.rb diff --git a/app/models/nomenclature_change/to_accepted_name_transformation.rb b/app/services/nomenclature_change/to_accepted_name_transformation.rb similarity index 100% rename from app/models/nomenclature_change/to_accepted_name_transformation.rb rename to app/services/nomenclature_change/to_accepted_name_transformation.rb diff --git a/app/models/nomenclature_change/to_synonym_transformation.rb b/app/services/nomenclature_change/to_synonym_transformation.rb similarity index 100% rename from app/models/nomenclature_change/to_synonym_transformation.rb rename to app/services/nomenclature_change/to_synonym_transformation.rb diff --git a/app/models/nomenclature_change/trade_shipments_resolver.rb b/app/services/nomenclature_change/trade_shipments_resolver.rb similarity index 100% rename from app/models/nomenclature_change/trade_shipments_resolver.rb rename to app/services/nomenclature_change/trade_shipments_resolver.rb diff --git a/app/models/search_params.rb b/app/services/search_params.rb similarity index 100% rename from app/models/search_params.rb rename to app/services/search_params.rb diff --git a/app/models/species/cites_listings_export.rb b/app/services/species/cites_listings_export.rb similarity index 100% rename from app/models/species/cites_listings_export.rb rename to app/services/species/cites_listings_export.rb diff --git a/app/models/species/cites_processes_export.rb b/app/services/species/cites_processes_export.rb similarity index 100% rename from app/models/species/cites_processes_export.rb rename to app/services/species/cites_processes_export.rb diff --git a/app/models/species/cms_listings_export.rb b/app/services/species/cms_listings_export.rb similarity index 100% rename from app/models/species/cms_listings_export.rb rename to app/services/species/cms_listings_export.rb diff --git a/app/models/species/cms_mappings_export.rb b/app/services/species/cms_mappings_export.rb similarity index 100% rename from app/models/species/cms_mappings_export.rb rename to app/services/species/cms_mappings_export.rb diff --git a/app/models/species/common_names_export.rb b/app/services/species/common_names_export.rb similarity index 100% rename from app/models/species/common_names_export.rb rename to app/services/species/common_names_export.rb diff --git a/app/models/species/csv_copy_export.rb b/app/services/species/csv_copy_export.rb similarity index 100% rename from app/models/species/csv_copy_export.rb rename to app/services/species/csv_copy_export.rb diff --git a/app/models/species/documents_export.rb b/app/services/species/documents_export.rb similarity index 100% rename from app/models/species/documents_export.rb rename to app/services/species/documents_export.rb diff --git a/app/models/species/eu_decisions_export.rb b/app/services/species/eu_decisions_export.rb similarity index 100% rename from app/models/species/eu_decisions_export.rb rename to app/services/species/eu_decisions_export.rb diff --git a/app/models/species/eu_listings_export.rb b/app/services/species/eu_listings_export.rb similarity index 100% rename from app/models/species/eu_listings_export.rb rename to app/services/species/eu_listings_export.rb diff --git a/app/models/species/id_manual_documents_export.rb b/app/services/species/id_manual_documents_export.rb similarity index 100% rename from app/models/species/id_manual_documents_export.rb rename to app/services/species/id_manual_documents_export.rb diff --git a/app/models/species/iucn_mappings_export.rb b/app/services/species/iucn_mappings_export.rb similarity index 100% rename from app/models/species/iucn_mappings_export.rb rename to app/services/species/iucn_mappings_export.rb diff --git a/app/models/species/listings_export.rb b/app/services/species/listings_export.rb similarity index 100% rename from app/models/species/listings_export.rb rename to app/services/species/listings_export.rb diff --git a/app/models/species/listings_export_factory.rb b/app/services/species/listings_export_factory.rb similarity index 100% rename from app/models/species/listings_export_factory.rb rename to app/services/species/listings_export_factory.rb diff --git a/app/models/species/orphaned_taxon_concepts_export.rb b/app/services/species/orphaned_taxon_concepts_export.rb similarity index 100% rename from app/models/species/orphaned_taxon_concepts_export.rb rename to app/services/species/orphaned_taxon_concepts_export.rb diff --git a/app/models/species/restrictions_export.rb b/app/services/species/restrictions_export.rb similarity index 100% rename from app/models/species/restrictions_export.rb rename to app/services/species/restrictions_export.rb diff --git a/app/models/species/search.rb b/app/services/species/search.rb similarity index 100% rename from app/models/species/search.rb rename to app/services/species/search.rb diff --git a/app/models/species/search_params.rb b/app/services/species/search_params.rb similarity index 100% rename from app/models/species/search_params.rb rename to app/services/species/search_params.rb diff --git a/app/models/species/species_reference_output_export.rb b/app/services/species/species_reference_output_export.rb similarity index 100% rename from app/models/species/species_reference_output_export.rb rename to app/services/species/species_reference_output_export.rb diff --git a/app/models/species/standard_reference_output_export.rb b/app/services/species/standard_reference_output_export.rb similarity index 100% rename from app/models/species/standard_reference_output_export.rb rename to app/services/species/standard_reference_output_export.rb diff --git a/app/models/species/synonyms_and_trade_names_export.rb b/app/services/species/synonyms_and_trade_names_export.rb similarity index 100% rename from app/models/species/synonyms_and_trade_names_export.rb rename to app/services/species/synonyms_and_trade_names_export.rb diff --git a/app/models/species/taxon_concept_prefix_matcher.rb b/app/services/species/taxon_concept_prefix_matcher.rb similarity index 100% rename from app/models/species/taxon_concept_prefix_matcher.rb rename to app/services/species/taxon_concept_prefix_matcher.rb diff --git a/app/models/species/taxon_concepts_distributions_export.rb b/app/services/species/taxon_concepts_distributions_export.rb similarity index 100% rename from app/models/species/taxon_concepts_distributions_export.rb rename to app/services/species/taxon_concepts_distributions_export.rb diff --git a/app/models/species/taxon_concepts_names_export.rb b/app/services/species/taxon_concepts_names_export.rb similarity index 100% rename from app/models/species/taxon_concepts_names_export.rb rename to app/services/species/taxon_concepts_names_export.rb diff --git a/app/models/taxon_concept_data.rb b/app/services/taxon_concept_data.rb similarity index 100% rename from app/models/taxon_concept_data.rb rename to app/services/taxon_concept_data.rb diff --git a/app/models/taxon_concept_matcher.rb b/app/services/taxon_concept_matcher.rb similarity index 100% rename from app/models/taxon_concept_matcher.rb rename to app/services/taxon_concept_matcher.rb diff --git a/app/models/taxon_concept_prefix_matcher.rb b/app/services/taxon_concept_prefix_matcher.rb similarity index 100% rename from app/models/taxon_concept_prefix_matcher.rb rename to app/services/taxon_concept_prefix_matcher.rb diff --git a/app/models/taxon_concept_view_stats.rb b/app/services/taxon_concept_view_stats.rb similarity index 100% rename from app/models/taxon_concept_view_stats.rb rename to app/services/taxon_concept_view_stats.rb diff --git a/app/models/trade/batch_update.rb b/app/services/trade/batch_update.rb similarity index 100% rename from app/models/trade/batch_update.rb rename to app/services/trade/batch_update.rb diff --git a/app/models/trade/filter.rb b/app/services/trade/filter.rb similarity index 100% rename from app/models/trade/filter.rb rename to app/services/trade/filter.rb diff --git a/app/models/trade/permit_matcher.rb b/app/services/trade/permit_matcher.rb similarity index 100% rename from app/models/trade/permit_matcher.rb rename to app/services/trade/permit_matcher.rb diff --git a/app/models/trade/reported_taxon_concept_resolver.rb b/app/services/trade/reported_taxon_concept_resolver.rb similarity index 100% rename from app/models/trade/reported_taxon_concept_resolver.rb rename to app/services/trade/reported_taxon_concept_resolver.rb diff --git a/app/models/trade/sandbox.rb b/app/services/trade/sandbox.rb similarity index 100% rename from app/models/trade/sandbox.rb rename to app/services/trade/sandbox.rb diff --git a/app/models/trade/sandbox_filter.rb b/app/services/trade/sandbox_filter.rb similarity index 100% rename from app/models/trade/sandbox_filter.rb rename to app/services/trade/sandbox_filter.rb diff --git a/app/models/trade/sandbox_search_params.rb b/app/services/trade/sandbox_search_params.rb similarity index 100% rename from app/models/trade/sandbox_search_params.rb rename to app/services/trade/sandbox_search_params.rb diff --git a/app/models/trade/search_params.rb b/app/services/trade/search_params.rb similarity index 100% rename from app/models/trade/search_params.rb rename to app/services/trade/search_params.rb diff --git a/app/models/trade/shipment_report_queries.rb b/app/services/trade/shipment_report_queries.rb similarity index 100% rename from app/models/trade/shipment_report_queries.rb rename to app/services/trade/shipment_report_queries.rb diff --git a/app/models/trade/shipments_comptab_export.rb b/app/services/trade/shipments_comptab_export.rb similarity index 100% rename from app/models/trade/shipments_comptab_export.rb rename to app/services/trade/shipments_comptab_export.rb diff --git a/app/models/trade/shipments_export.rb b/app/services/trade/shipments_export.rb similarity index 100% rename from app/models/trade/shipments_export.rb rename to app/services/trade/shipments_export.rb diff --git a/app/models/trade/shipments_export_factory.rb b/app/services/trade/shipments_export_factory.rb similarity index 100% rename from app/models/trade/shipments_export_factory.rb rename to app/services/trade/shipments_export_factory.rb diff --git a/app/models/trade/shipments_gross_exports_export.rb b/app/services/trade/shipments_gross_exports_export.rb similarity index 100% rename from app/models/trade/shipments_gross_exports_export.rb rename to app/services/trade/shipments_gross_exports_export.rb diff --git a/app/models/trade/shipments_gross_imports_export.rb b/app/services/trade/shipments_gross_imports_export.rb similarity index 100% rename from app/models/trade/shipments_gross_imports_export.rb rename to app/services/trade/shipments_gross_imports_export.rb diff --git a/app/models/trade/shipments_net_exports_export.rb b/app/services/trade/shipments_net_exports_export.rb similarity index 100% rename from app/models/trade/shipments_net_exports_export.rb rename to app/services/trade/shipments_net_exports_export.rb diff --git a/app/models/trade/shipments_net_imports_export.rb b/app/services/trade/shipments_net_imports_export.rb similarity index 100% rename from app/models/trade/shipments_net_imports_export.rb rename to app/services/trade/shipments_net_imports_export.rb diff --git a/app/models/trade/trade_data_downloads_export.rb b/app/services/trade/trade_data_downloads_export.rb similarity index 100% rename from app/models/trade/trade_data_downloads_export.rb rename to app/services/trade/trade_data_downloads_export.rb diff --git a/app/uploaders/trade/csv_source_file_uploader.rb b/app/uploaders/trade/csv_source_file_uploader.rb index 1d92b664d..f33eb07bb 100644 --- a/app/uploaders/trade/csv_source_file_uploader.rb +++ b/app/uploaders/trade/csv_source_file_uploader.rb @@ -1,6 +1,4 @@ # encoding: utf-8 -require 'fileutils' - class Trade::CsvSourceFileUploader < CarrierWave::Uploader::Base # Include RMagick or MiniMagick support: diff --git a/app/workers/manual_download_worker.rb b/app/workers/manual_download_worker.rb index 35aea3ca4..5ca21d137 100644 --- a/app/workers/manual_download_worker.rb +++ b/app/workers/manual_download_worker.rb @@ -54,8 +54,6 @@ def download_location(ext, params) end def zip_file_generator - require 'zip' - missing_files = [] pdf_file_paths = [] tmp_dir_path = [Rails.root, "/tmp/", SecureRandom.hex(8)].join diff --git a/config/application.rb b/config/application.rb index 16faccc5a..5f5670c07 100644 --- a/config/application.rb +++ b/config/application.rb @@ -1,7 +1,8 @@ require_relative 'boot' require 'rails/all' -require "susy" +require 'zip' +require 'susy' # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. @@ -11,7 +12,6 @@ module SAPI class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 6.0 - config.autoloader = :classic # TODO https://guides.rubyonrails.org/classic_to_zeitwerk_howto.html # Settings in config/environments/* take precedence over those specified here. # Application configuration can go into files in config/initializers diff --git a/config/initializers/acts_as_taggable_on.rb b/config/initializers/acts_as_taggable_on.rb index bd6585dcd..b00cc59c5 100644 --- a/config/initializers/acts_as_taggable_on.rb +++ b/config/initializers/acts_as_taggable_on.rb @@ -7,4 +7,20 @@ # attr_accessible :tag_id, :context, :taggable, :taggable_id, :taggable_type, :tagger_id, :tagger_type # end + + +# TODO +# DEPRECATION WARNING: Initialization autoloaded the constant ComparisonAttributes. +# +# Being able to do this is deprecated. Autoloading during initialization is going +# to be an error condition in future versions of Rails. +# +# Reloading does not reboot the application, and therefore code executed during +# initialization does not run again. So, if you reload ComparisonAttributes, for example, +# the expected changes won't be reflected in that stale Module object. +# +# `config.autoloader` is set to `classic`. This autoloaded constant would have been unloaded if `config.autoloader` had been set to `:zeitwerk`. +# +# Please, check the "Autoloading and Reloading Constants" guide for solutions. +# (called from at /SAPI/config/environment.rb:5) ActsAsTaggableOn::Tagging.send :include, ComparisonAttributes diff --git a/config/initializers/ahoy.rb b/config/initializers/ahoy.rb index 41e76e86d..ebac64204 100644 --- a/config/initializers/ahoy.rb +++ b/config/initializers/ahoy.rb @@ -1,4 +1,4 @@ -require 'sapi/geoip' +require 'sapi/geo_i_p' class Ahoy::Store < Ahoy::DatabaseStore UUID_NAMESPACE = UUIDTools::UUID.parse("dcd74c26-8fc9-453a-a9c2-afc445c3258d") diff --git a/config/initializers/preload_sti_models.rb b/config/initializers/preload_sti_models.rb index 2a6a892a4..5ef919421 100644 --- a/config/initializers/preload_sti_models.rb +++ b/config/initializers/preload_sti_models.rb @@ -1,10 +1,28 @@ -if Rails.env.development? +# https://guides.rubyonrails.org/autoloading_and_reloading_constants.html#single-table-inheritance - require_dependency File.join("app", "models", "document.rb") - Dir.glob(Rails.root.join("app/models/document/*.rb")).each do |path| - require_dependency path - end - %w[cites_cop cites_ac cites_pc cites_tc cites_extraordinary_meeting ec_srg].each do |c| - require_dependency File.join("app", "models", "#{c}.rb") +# https://guides.rubyonrails.org/autoloading_and_reloading_constants.html#option-2-preload-a-collapsed-directory +events = "#{Rails.root}/app/models/events" +Rails.autoloaders.main.collapse(events) +trade_codes = "#{Rails.root}/app/models/trade_codes" +Rails.autoloaders.main.collapse(trade_codes) +trade_restrictions = "#{Rails.root}/app/models/trade_restrictions" +Rails.autoloaders.main.collapse(trade_restrictions) +eu_decisions = "#{Rails.root}/app/models/eu_decisions" +Rails.autoloaders.main.collapse(eu_decisions) +cites_processes = "#{Rails.root}/app/models/cites_processes" +Rails.autoloaders.main.collapse(cites_processes) + +unless Rails.application.config.eager_load + Rails.application.config.to_prepare do + # https://guides.rubyonrails.org/autoloading_and_reloading_constants.html#option-2-preload-a-collapsed-directory + Rails.autoloaders.main.eager_load_dir(events) + Rails.autoloaders.main.eager_load_dir(trade_codes) + Rails.autoloaders.main.eager_load_dir(trade_restrictions) + Rails.autoloaders.main.eager_load_dir(eu_decisions) + Rails.autoloaders.main.eager_load_dir(cites_processes) + # https://guides.rubyonrails.org/autoloading_and_reloading_constants.html#option-3-preload-a-regular-directory + Rails.autoloaders.main.eager_load_dir("#{Rails.root}/app/models/document") + Rails.autoloaders.main.eager_load_dir("#{Rails.root}/app/models/document_tag") + Rails.autoloaders.main.eager_load_dir("#{Rails.root}/app/models/nomenclature_change") end end diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index f62ab9ee2..75dbad6cb 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -1,6 +1,6 @@ require 'sidekiq' require 'sidekiq-status' -require "sidekiq-unique-jobs" +require 'sidekiq-unique-jobs' Sidekiq.configure_client do |config| config.client_middleware do |chain| diff --git a/lib/modules/hash_deep_slice.rb b/lib/modules/hash_deep_slice.rb deleted file mode 100644 index 78410bb63..000000000 --- a/lib/modules/hash_deep_slice.rb +++ /dev/null @@ -1,28 +0,0 @@ -Hash.class_eval do - - def deep_slice(*allowed_keys) - sliced = {} - allowed_keys.each do |allowed_key| - if allowed_key.is_a?(Hash) - allowed_key.each do |allowed_subkey, allowed_subkey_values| - if has_key?(allowed_subkey) - value = self[allowed_subkey] - if value.is_a?(Hash) - sliced[allowed_subkey] = value.deep_slice(*Array.wrap(allowed_subkey_values)) - elsif value.nil? - sliced[allowed_subkey] = '' - else - raise ArgumentError, "can only deep-slice hash values, but value for #{allowed_subkey.inspect} was of type #{value.class.name}" - end - end - end - else - if has_key?(allowed_key) - sliced[allowed_key] = self[allowed_key] - end - end - end - sliced - end - -end diff --git a/lib/modules/import/rst/formatter.rb b/lib/modules/import/rst/formatter.rb index 88b7517c7..1fa029fd9 100644 --- a/lib/modules/import/rst/formatter.rb +++ b/lib/modules/import/rst/formatter.rb @@ -1,5 +1,3 @@ -require 'hash_deep_slice' - module Import::Rst::Formatter class << self def format_data(data) @@ -7,5 +5,32 @@ def format_data(data) item.deep_slice('id', 'countryId', 'status', 'startDate' ,'species' => 'name', 'meeting' => 'name') end end + + private + + def deep_slice(*allowed_keys) + sliced = {} + allowed_keys.each do |allowed_key| + if allowed_key.is_a?(Hash) + allowed_key.each do |allowed_subkey, allowed_subkey_values| + if has_key?(allowed_subkey) + value = self[allowed_subkey] + if value.is_a?(Hash) + sliced[allowed_subkey] = value.deep_slice(*Array.wrap(allowed_subkey_values)) + elsif value.nil? + sliced[allowed_subkey] = '' + else + raise ArgumentError, "can only deep-slice hash values, but value for #{allowed_subkey.inspect} was of type #{value.class.name}" + end + end + end + else + if has_key?(allowed_key) + sliced[allowed_key] = self[allowed_key] + end + end + end + sliced + end end end diff --git a/lib/modules/sapi/geo_i_p.rb b/lib/modules/sapi/geo_i_p.rb new file mode 100644 index 000000000..039272220 --- /dev/null +++ b/lib/modules/sapi/geo_i_p.rb @@ -0,0 +1,61 @@ +# Returns UTF8 +module Sapi + class GeoIP + include Singleton + + def initialize + begin + @city_db = GEO_IP_CONFIG['city_db'] && ::GeoIP.new(GEO_IP_CONFIG['city_db']) + rescue Errno::ENOENT + @city_db = nil + end + begin + @org_db = GEO_IP_CONFIG['org_db'] && ::GeoIP.new(GEO_IP_CONFIG['org_db']) + rescue Errno::ENOENT + @org_db = nil + end + end + + def resolve(ip) + result = country_and_city(ip).merge(organisation(ip)) + result.each do |k, v| + result[k] = + if v.nil? + 'Unknown' + else + v.force_encoding("ISO-8859-1").encode("UTF-8") + end + end + end + + def country_and_city(ip) + cdb_names = @city_db && @city_db.city(ip) + country = cdb_names.try(:country_code2) + city = cdb_names.try(:city_name) + { + country: country, + city: city + } + end + + def organisation(ip) + org_names = @org_db && @org_db.organization(ip) + org = org_names.try(:isp) + { organization: org } + end + + def default_separator(ip) + invalid_addresses = ['127.0.0.1', nil, 'localhost', 'nil', '', 'unknown'] + + if invalid_addresses.include?(ip) + :comma + else + ip_data = country_and_city(ip) + country = ip_data[:country] + separator_char = (country ? DEFAULT_COUNTRY_SEPARATORS[country.to_sym] : ',') + (separator_char == ';' ? :semicolon : :comma) + end + end + + end +end diff --git a/lib/modules/sapi/geoip.rb b/lib/modules/sapi/geoip.rb deleted file mode 100644 index d171d3d4f..000000000 --- a/lib/modules/sapi/geoip.rb +++ /dev/null @@ -1,59 +0,0 @@ -# Returns UTF8 -class Sapi::GeoIP - include Singleton - - def initialize - begin - @city_db = GEO_IP_CONFIG['city_db'] && ::GeoIP.new(GEO_IP_CONFIG['city_db']) - rescue Errno::ENOENT - @city_db = nil - end - begin - @org_db = GEO_IP_CONFIG['org_db'] && ::GeoIP.new(GEO_IP_CONFIG['org_db']) - rescue Errno::ENOENT - @org_db = nil - end - end - - def resolve(ip) - result = country_and_city(ip).merge(organisation(ip)) - result.each do |k, v| - result[k] = - if v.nil? - 'Unknown' - else - v.force_encoding("ISO-8859-1").encode("UTF-8") - end - end - end - - def country_and_city(ip) - cdb_names = @city_db && @city_db.city(ip) - country = cdb_names.try(:country_code2) - city = cdb_names.try(:city_name) - { - country: country, - city: city - } - end - - def organisation(ip) - org_names = @org_db && @org_db.organization(ip) - org = org_names.try(:isp) - { organization: org } - end - - def default_separator(ip) - invalid_addresses = ['127.0.0.1', nil, 'localhost', 'nil', '', 'unknown'] - - if invalid_addresses.include?(ip) - :comma - else - ip_data = country_and_city(ip) - country = ip_data[:country] - separator_char = (country ? DEFAULT_COUNTRY_SEPARATORS[country.to_sym] : ',') - (separator_char == ';' ? :semicolon : :comma) - end - end - -end diff --git a/lib/tasks/elibrary/document_files_importer.rb b/lib/tasks/elibrary/document_files_importer.rb index b9df41c48..ee7871632 100644 --- a/lib/tasks/elibrary/document_files_importer.rb +++ b/lib/tasks/elibrary/document_files_importer.rb @@ -1,4 +1,3 @@ -require 'fileutils' # The purpose of this is to go over all document records and match them # with respective files. # Where files are present, create the expected directory structure, e.g. diff --git a/lib/tasks/elibrary/import.rake b/lib/tasks/elibrary/import.rake index d23670f70..8a70dd9e6 100644 --- a/lib/tasks/elibrary/import.rake +++ b/lib/tasks/elibrary/import.rake @@ -12,8 +12,8 @@ namespace :elibrary do DocumentCitationTaxonConcept.delete_all DocumentCitation.delete_all puts "Deleting documents" - Document::ProposalDetails.delete_all - Document::ReviewDetails.delete_all + ProposalDetails.delete_all + ReviewDetails.delete_all Document.delete_all DocumentTag.delete_all end diff --git a/lib/tasks/elibrary/manual_document_files_importer.rb b/lib/tasks/elibrary/manual_document_files_importer.rb index 08edd1767..a5e3d3f62 100644 --- a/lib/tasks/elibrary/manual_document_files_importer.rb +++ b/lib/tasks/elibrary/manual_document_files_importer.rb @@ -1,4 +1,3 @@ -require 'fileutils' # The purpose of this is to go over all document records and match them # with respective files. # Where files are present, create the expected directory structure, e.g. diff --git a/lib/tasks/export_trade_db.rake b/lib/tasks/export_trade_db.rake index 545d5473a..c4c9281ef 100644 --- a/lib/tasks/export_trade_db.rake +++ b/lib/tasks/export_trade_db.rake @@ -1,5 +1,3 @@ -require 'zip' -require 'fileutils' namespace :export do task :trade_db => :environment do RECORDS_PER_FILE = 500000 diff --git a/spec/factories/documents.rb b/spec/factories/documents.rb index 91831d343..6726954b0 100644 --- a/spec/factories/documents.rb +++ b/spec/factories/documents.rb @@ -42,12 +42,12 @@ geo_entity end - factory :proposal_details, class: Document::ProposalDetails do + factory :proposal_details, class: ProposalDetails do document proposal_outcome end - factory :review_details, class: Document::ReviewDetails do + factory :review_details, class: ReviewDetails do document review_phase end diff --git a/spec/models/cites_captivity_process_spec.rb b/spec/models/cites_processes/cites_captivity_process_spec.rb similarity index 100% rename from spec/models/cites_captivity_process_spec.rb rename to spec/models/cites_processes/cites_captivity_process_spec.rb diff --git a/spec/models/eu_opinion_spec.rb b/spec/models/eu_decisions/eu_opinion_spec.rb similarity index 100% rename from spec/models/eu_opinion_spec.rb rename to spec/models/eu_decisions/eu_opinion_spec.rb diff --git a/spec/models/eu_suspension_spec.rb b/spec/models/eu_decisions/eu_suspension_spec.rb similarity index 100% rename from spec/models/eu_suspension_spec.rb rename to spec/models/eu_decisions/eu_suspension_spec.rb diff --git a/spec/models/cites_cop_spec.rb b/spec/models/events/cites_cop_spec.rb similarity index 100% rename from spec/models/cites_cop_spec.rb rename to spec/models/events/cites_cop_spec.rb diff --git a/spec/models/cites_suspension_notification_spec.rb b/spec/models/events/cites_suspension_notification_spec.rb similarity index 100% rename from spec/models/cites_suspension_notification_spec.rb rename to spec/models/events/cites_suspension_notification_spec.rb diff --git a/spec/models/eu_regulation_spec.rb b/spec/models/events/eu_regulation_spec.rb similarity index 100% rename from spec/models/eu_regulation_spec.rb rename to spec/models/events/eu_regulation_spec.rb diff --git a/spec/models/html_to_latex_spec.rb b/spec/models/html_to_latex_spec.rb index d58f7fe45..5595d7909 100644 --- a/spec/models/html_to_latex_spec.rb +++ b/spec/models/html_to_latex_spec.rb @@ -1,6 +1,4 @@ require 'spec_helper' -# https://stackoverflow.com/a/46904732/556780 -require_dependency Rails.root.join('lib', 'modules', 'html_to_latex') describe HtmlToLatex do describe :convert do diff --git a/spec/models/purpose_spec.rb b/spec/models/trade_codes/purpose_spec.rb similarity index 100% rename from spec/models/purpose_spec.rb rename to spec/models/trade_codes/purpose_spec.rb diff --git a/spec/models/source_spec.rb b/spec/models/trade_codes/source_spec.rb similarity index 100% rename from spec/models/source_spec.rb rename to spec/models/trade_codes/source_spec.rb diff --git a/spec/models/term_spec.rb b/spec/models/trade_codes/term_spec.rb similarity index 100% rename from spec/models/term_spec.rb rename to spec/models/trade_codes/term_spec.rb diff --git a/spec/models/unit_spec.rb b/spec/models/trade_codes/unit_spec.rb similarity index 100% rename from spec/models/unit_spec.rb rename to spec/models/trade_codes/unit_spec.rb diff --git a/spec/models/cites_suspension_spec.rb b/spec/models/trade_restrictions/cites_suspension_spec.rb similarity index 100% rename from spec/models/cites_suspension_spec.rb rename to spec/models/trade_restrictions/cites_suspension_spec.rb diff --git a/spec/models/quota_spec.rb b/spec/models/trade_restrictions/quota_spec.rb similarity index 100% rename from spec/models/quota_spec.rb rename to spec/models/trade_restrictions/quota_spec.rb diff --git a/spec/models/zeitwerk_compliance_spec.rb b/spec/models/zeitwerk_compliance_spec.rb new file mode 100644 index 000000000..0cd4e3aac --- /dev/null +++ b/spec/models/zeitwerk_compliance_spec.rb @@ -0,0 +1,8 @@ +require "spec_helper" + +# https://guides.rubyonrails.org/classic_to_zeitwerk_howto.html#rspec +RSpec.describe "Zeitwerk compliance" do + it "eager loads all files without errors" do + expect { Rails.application.eager_load! }.not_to raise_error + end +end diff --git a/spec/models/checklist/annotations_spec.rb b/spec/services/checklist/annotations_spec.rb similarity index 100% rename from spec/models/checklist/annotations_spec.rb rename to spec/services/checklist/annotations_spec.rb diff --git a/spec/models/checklist/appendix_population_and_region_spec.rb b/spec/services/checklist/appendix_population_and_region_spec.rb similarity index 100% rename from spec/models/checklist/appendix_population_and_region_spec.rb rename to spec/services/checklist/appendix_population_and_region_spec.rb diff --git a/spec/models/checklist/appendix_population_spec.rb b/spec/services/checklist/appendix_population_spec.rb similarity index 100% rename from spec/models/checklist/appendix_population_spec.rb rename to spec/services/checklist/appendix_population_spec.rb diff --git a/spec/models/checklist/appendix_spec.rb b/spec/services/checklist/appendix_spec.rb similarity index 100% rename from spec/models/checklist/appendix_spec.rb rename to spec/services/checklist/appendix_spec.rb diff --git a/spec/models/checklist/checklist_spec.rb b/spec/services/checklist/checklist_spec.rb similarity index 100% rename from spec/models/checklist/checklist_spec.rb rename to spec/services/checklist/checklist_spec.rb diff --git a/spec/models/checklist/common_names_spec.rb b/spec/services/checklist/common_names_spec.rb similarity index 100% rename from spec/models/checklist/common_names_spec.rb rename to spec/services/checklist/common_names_spec.rb diff --git a/spec/models/checklist/higher_taxa_injector_spec.rb b/spec/services/checklist/higher_taxa_injector_spec.rb similarity index 100% rename from spec/models/checklist/higher_taxa_injector_spec.rb rename to spec/services/checklist/higher_taxa_injector_spec.rb diff --git a/spec/models/checklist/higher_taxa_item_spec.rb b/spec/services/checklist/higher_taxa_item_spec.rb similarity index 100% rename from spec/models/checklist/higher_taxa_item_spec.rb rename to spec/services/checklist/higher_taxa_item_spec.rb diff --git a/spec/models/checklist/order_spec.rb b/spec/services/checklist/order_spec.rb similarity index 100% rename from spec/models/checklist/order_spec.rb rename to spec/services/checklist/order_spec.rb diff --git a/spec/models/checklist/pdf/history_annotations_key_spec.rb b/spec/services/checklist/pdf/history_annotations_key_spec.rb similarity index 100% rename from spec/models/checklist/pdf/history_annotations_key_spec.rb rename to spec/services/checklist/pdf/history_annotations_key_spec.rb diff --git a/spec/models/checklist/pdf/history_spec.rb b/spec/services/checklist/pdf/history_spec.rb similarity index 100% rename from spec/models/checklist/pdf/history_spec.rb rename to spec/services/checklist/pdf/history_spec.rb diff --git a/spec/models/checklist/pdf/index_annotations_key_spec.rb b/spec/services/checklist/pdf/index_annotations_key_spec.rb similarity index 100% rename from spec/models/checklist/pdf/index_annotations_key_spec.rb rename to spec/services/checklist/pdf/index_annotations_key_spec.rb diff --git a/spec/models/checklist/pdf/index_fetcher_spec.rb b/spec/services/checklist/pdf/index_fetcher_spec.rb similarity index 100% rename from spec/models/checklist/pdf/index_fetcher_spec.rb rename to spec/services/checklist/pdf/index_fetcher_spec.rb diff --git a/spec/models/checklist/scientific_name_spec.rb b/spec/services/checklist/scientific_name_spec.rb similarity index 100% rename from spec/models/checklist/scientific_name_spec.rb rename to spec/services/checklist/scientific_name_spec.rb diff --git a/spec/models/checklist/synonyms_spec.rb b/spec/services/checklist/synonyms_spec.rb similarity index 100% rename from spec/models/checklist/synonyms_spec.rb rename to spec/services/checklist/synonyms_spec.rb diff --git a/spec/models/checklist/taxon_concept_prefix_matcher_spec.rb b/spec/services/checklist/taxon_concept_prefix_matcher_spec.rb similarity index 100% rename from spec/models/checklist/taxon_concept_prefix_matcher_spec.rb rename to spec/services/checklist/taxon_concept_prefix_matcher_spec.rb diff --git a/spec/models/checklist/timeline_spec.rb b/spec/services/checklist/timeline_spec.rb similarity index 100% rename from spec/models/checklist/timeline_spec.rb rename to spec/services/checklist/timeline_spec.rb diff --git a/spec/models/checklist/timelines_for_taxon_concept_spec.rb b/spec/services/checklist/timelines_for_taxon_concept_spec.rb similarity index 100% rename from spec/models/checklist/timelines_for_taxon_concept_spec.rb rename to spec/services/checklist/timelines_for_taxon_concept_spec.rb diff --git a/spec/models/dashboard_stats_species_spec.rb b/spec/services/dashboard_stats_species_spec.rb similarity index 100% rename from spec/models/dashboard_stats_species_spec.rb rename to spec/services/dashboard_stats_species_spec.rb diff --git a/spec/models/dashboard_stats_trade_spec.rb b/spec/services/dashboard_stats_trade_spec.rb similarity index 100% rename from spec/models/dashboard_stats_trade_spec.rb rename to spec/services/dashboard_stats_trade_spec.rb diff --git a/spec/models/document_batch_spec.rb b/spec/services/document_batch_spec.rb similarity index 100% rename from spec/models/document_batch_spec.rb rename to spec/services/document_batch_spec.rb diff --git a/spec/models/document_search_spec.rb b/spec/services/document_search_spec.rb similarity index 100% rename from spec/models/document_search_spec.rb rename to spec/services/document_search_spec.rb diff --git a/spec/models/geo_entity_search_spec.rb b/spec/services/geo_entity_search_spec.rb similarity index 100% rename from spec/models/geo_entity_search_spec.rb rename to spec/services/geo_entity_search_spec.rb diff --git a/spec/models/nomenclature_change/delete_unreassigned_processor_spec.rb b/spec/services/nomenclature_change/delete_unreassigned_processor_spec.rb similarity index 100% rename from spec/models/nomenclature_change/delete_unreassigned_processor_spec.rb rename to spec/services/nomenclature_change/delete_unreassigned_processor_spec.rb diff --git a/spec/models/nomenclature_change/full_reassignment_spec.rb b/spec/services/nomenclature_change/full_reassignment_spec.rb similarity index 100% rename from spec/models/nomenclature_change/full_reassignment_spec.rb rename to spec/services/nomenclature_change/full_reassignment_spec.rb diff --git a/spec/models/nomenclature_change/lump/constructor_spec.rb b/spec/services/nomenclature_change/lump/constructor_spec.rb similarity index 100% rename from spec/models/nomenclature_change/lump/constructor_spec.rb rename to spec/services/nomenclature_change/lump/constructor_spec.rb diff --git a/spec/models/nomenclature_change/lump/output_taxon_concept_processor_spec.rb b/spec/services/nomenclature_change/lump/output_taxon_concept_processor_spec.rb similarity index 100% rename from spec/models/nomenclature_change/lump/output_taxon_concept_processor_spec.rb rename to spec/services/nomenclature_change/lump/output_taxon_concept_processor_spec.rb diff --git a/spec/models/nomenclature_change/lump/processor_spec.rb b/spec/services/nomenclature_change/lump/processor_spec.rb similarity index 100% rename from spec/models/nomenclature_change/lump/processor_spec.rb rename to spec/services/nomenclature_change/lump/processor_spec.rb diff --git a/spec/models/nomenclature_change/reassignment_copy_processor_spec.rb b/spec/services/nomenclature_change/reassignment_copy_processor_spec.rb similarity index 100% rename from spec/models/nomenclature_change/reassignment_copy_processor_spec.rb rename to spec/services/nomenclature_change/reassignment_copy_processor_spec.rb diff --git a/spec/models/nomenclature_change/reassignment_transfer_processor_spec.rb b/spec/services/nomenclature_change/reassignment_transfer_processor_spec.rb similarity index 100% rename from spec/models/nomenclature_change/reassignment_transfer_processor_spec.rb rename to spec/services/nomenclature_change/reassignment_transfer_processor_spec.rb diff --git a/spec/models/nomenclature_change/split/constructor_spec.rb b/spec/services/nomenclature_change/split/constructor_spec.rb similarity index 100% rename from spec/models/nomenclature_change/split/constructor_spec.rb rename to spec/services/nomenclature_change/split/constructor_spec.rb diff --git a/spec/models/nomenclature_change/split/output_taxon_concept_processor_spec.rb b/spec/services/nomenclature_change/split/output_taxon_concept_processor_spec.rb similarity index 100% rename from spec/models/nomenclature_change/split/output_taxon_concept_processor_spec.rb rename to spec/services/nomenclature_change/split/output_taxon_concept_processor_spec.rb diff --git a/spec/models/nomenclature_change/split/processor_spec.rb b/spec/services/nomenclature_change/split/processor_spec.rb similarity index 100% rename from spec/models/nomenclature_change/split/processor_spec.rb rename to spec/services/nomenclature_change/split/processor_spec.rb diff --git a/spec/models/nomenclature_change/status_swap/constructor_spec.rb b/spec/services/nomenclature_change/status_swap/constructor_spec.rb similarity index 100% rename from spec/models/nomenclature_change/status_swap/constructor_spec.rb rename to spec/services/nomenclature_change/status_swap/constructor_spec.rb diff --git a/spec/models/nomenclature_change/status_swap/processor_spec.rb b/spec/services/nomenclature_change/status_swap/processor_spec.rb similarity index 100% rename from spec/models/nomenclature_change/status_swap/processor_spec.rb rename to spec/services/nomenclature_change/status_swap/processor_spec.rb diff --git a/spec/models/nomenclature_change/status_to_accepted/processor_spec.rb b/spec/services/nomenclature_change/status_to_accepted/processor_spec.rb similarity index 100% rename from spec/models/nomenclature_change/status_to_accepted/processor_spec.rb rename to spec/services/nomenclature_change/status_to_accepted/processor_spec.rb diff --git a/spec/models/nomenclature_change/status_to_synonym/constructor_spec.rb b/spec/services/nomenclature_change/status_to_synonym/constructor_spec.rb similarity index 100% rename from spec/models/nomenclature_change/status_to_synonym/constructor_spec.rb rename to spec/services/nomenclature_change/status_to_synonym/constructor_spec.rb diff --git a/spec/models/nomenclature_change/status_to_synonym/output_taxon_concept_processor_spec.rb b/spec/services/nomenclature_change/status_to_synonym/output_taxon_concept_processor_spec.rb similarity index 100% rename from spec/models/nomenclature_change/status_to_synonym/output_taxon_concept_processor_spec.rb rename to spec/services/nomenclature_change/status_to_synonym/output_taxon_concept_processor_spec.rb diff --git a/spec/models/nomenclature_change/status_to_synonym/processor_spec.rb b/spec/services/nomenclature_change/status_to_synonym/processor_spec.rb similarity index 100% rename from spec/models/nomenclature_change/status_to_synonym/processor_spec.rb rename to spec/services/nomenclature_change/status_to_synonym/processor_spec.rb diff --git a/spec/models/species/common_names_export_spec.rb b/spec/services/species/common_names_export_spec.rb similarity index 100% rename from spec/models/species/common_names_export_spec.rb rename to spec/services/species/common_names_export_spec.rb diff --git a/spec/models/species/documents_export_spec.rb b/spec/services/species/documents_export_spec.rb similarity index 100% rename from spec/models/species/documents_export_spec.rb rename to spec/services/species/documents_export_spec.rb diff --git a/spec/models/species/hybrid_prefix_matcher_spec.rb b/spec/services/species/hybrid_prefix_matcher_spec.rb similarity index 100% rename from spec/models/species/hybrid_prefix_matcher_spec.rb rename to spec/services/species/hybrid_prefix_matcher_spec.rb diff --git a/spec/models/species/invisible_subspecies_search_spec.rb b/spec/services/species/invisible_subspecies_search_spec.rb similarity index 100% rename from spec/models/species/invisible_subspecies_search_spec.rb rename to spec/services/species/invisible_subspecies_search_spec.rb diff --git a/spec/models/species/listings_export_spec.rb b/spec/services/species/listings_export_spec.rb similarity index 100% rename from spec/models/species/listings_export_spec.rb rename to spec/services/species/listings_export_spec.rb diff --git a/spec/models/species/orphaned_taxon_concepts_export_spec.rb b/spec/services/species/orphaned_taxon_concepts_export_spec.rb similarity index 100% rename from spec/models/species/orphaned_taxon_concepts_export_spec.rb rename to spec/services/species/orphaned_taxon_concepts_export_spec.rb diff --git a/spec/models/species/search_spec.rb b/spec/services/species/search_spec.rb similarity index 100% rename from spec/models/species/search_spec.rb rename to spec/services/species/search_spec.rb diff --git a/spec/models/species/species_reference_output_spec.rb b/spec/services/species/species_reference_output_spec.rb similarity index 100% rename from spec/models/species/species_reference_output_spec.rb rename to spec/services/species/species_reference_output_spec.rb diff --git a/spec/models/species/standard_reference_output_spec.rb b/spec/services/species/standard_reference_output_spec.rb similarity index 100% rename from spec/models/species/standard_reference_output_spec.rb rename to spec/services/species/standard_reference_output_spec.rb diff --git a/spec/models/species/synonyms_and_trade_names_export_spec.rb b/spec/services/species/synonyms_and_trade_names_export_spec.rb similarity index 100% rename from spec/models/species/synonyms_and_trade_names_export_spec.rb rename to spec/services/species/synonyms_and_trade_names_export_spec.rb diff --git a/spec/models/species/taxon_concept_prefix_matcher_spec.rb b/spec/services/species/taxon_concept_prefix_matcher_spec.rb similarity index 100% rename from spec/models/species/taxon_concept_prefix_matcher_spec.rb rename to spec/services/species/taxon_concept_prefix_matcher_spec.rb diff --git a/spec/models/species/taxon_concepts_export_spec.rb b/spec/services/species/taxon_concepts_export_spec.rb similarity index 100% rename from spec/models/species/taxon_concepts_export_spec.rb rename to spec/services/species/taxon_concepts_export_spec.rb diff --git a/spec/models/species/trade_name_prefix_matcher_spec.rb b/spec/services/species/trade_name_prefix_matcher_spec.rb similarity index 100% rename from spec/models/species/trade_name_prefix_matcher_spec.rb rename to spec/services/species/trade_name_prefix_matcher_spec.rb diff --git a/spec/models/species/visible_subspecies_search_spec.rb b/spec/services/species/visible_subspecies_search_spec.rb similarity index 100% rename from spec/models/species/visible_subspecies_search_spec.rb rename to spec/services/species/visible_subspecies_search_spec.rb diff --git a/spec/models/taxon_concept_data_spec.rb b/spec/services/taxon_concept_data_spec.rb similarity index 100% rename from spec/models/taxon_concept_data_spec.rb rename to spec/services/taxon_concept_data_spec.rb diff --git a/spec/models/taxon_concept_prefix_matcher_spec.rb b/spec/services/taxon_concept_prefix_matcher_spec.rb similarity index 100% rename from spec/models/taxon_concept_prefix_matcher_spec.rb rename to spec/services/taxon_concept_prefix_matcher_spec.rb diff --git a/spec/models/trade/filter_spec.rb b/spec/services/trade/filter_spec.rb similarity index 100% rename from spec/models/trade/filter_spec.rb rename to spec/services/trade/filter_spec.rb diff --git a/spec/models/trade/permit_matcher_spec.rb b/spec/services/trade/permit_matcher_spec.rb similarity index 100% rename from spec/models/trade/permit_matcher_spec.rb rename to spec/services/trade/permit_matcher_spec.rb diff --git a/spec/models/trade/reported_taxon_concept_resolver_spec.rb b/spec/services/trade/reported_taxon_concept_resolver_spec.rb similarity index 100% rename from spec/models/trade/reported_taxon_concept_resolver_spec.rb rename to spec/services/trade/reported_taxon_concept_resolver_spec.rb diff --git a/spec/models/trade/sandbox_filter_spec.rb b/spec/services/trade/sandbox_filter_spec.rb similarity index 100% rename from spec/models/trade/sandbox_filter_spec.rb rename to spec/services/trade/sandbox_filter_spec.rb diff --git a/spec/models/trade/sandbox_spec.rb b/spec/services/trade/sandbox_spec.rb similarity index 100% rename from spec/models/trade/sandbox_spec.rb rename to spec/services/trade/sandbox_spec.rb diff --git a/spec/models/trade/shipments_comptab_export_spec.rb b/spec/services/trade/shipments_comptab_export_spec.rb similarity index 100% rename from spec/models/trade/shipments_comptab_export_spec.rb rename to spec/services/trade/shipments_comptab_export_spec.rb diff --git a/spec/models/trade/shipments_export_spec.rb b/spec/services/trade/shipments_export_spec.rb similarity index 100% rename from spec/models/trade/shipments_export_spec.rb rename to spec/services/trade/shipments_export_spec.rb diff --git a/spec/models/trade/shipments_gross_exports_export_spec.rb b/spec/services/trade/shipments_gross_exports_export_spec.rb similarity index 100% rename from spec/models/trade/shipments_gross_exports_export_spec.rb rename to spec/services/trade/shipments_gross_exports_export_spec.rb diff --git a/spec/models/trade/shipments_gross_imports_export_spec.rb b/spec/services/trade/shipments_gross_imports_export_spec.rb similarity index 100% rename from spec/models/trade/shipments_gross_imports_export_spec.rb rename to spec/services/trade/shipments_gross_imports_export_spec.rb diff --git a/spec/models/trade/shipments_net_exports_export_spec.rb b/spec/services/trade/shipments_net_exports_export_spec.rb similarity index 100% rename from spec/models/trade/shipments_net_exports_export_spec.rb rename to spec/services/trade/shipments_net_exports_export_spec.rb diff --git a/spec/models/trade/shipments_net_imports_export_spec.rb b/spec/services/trade/shipments_net_imports_export_spec.rb similarity index 100% rename from spec/models/trade/shipments_net_imports_export_spec.rb rename to spec/services/trade/shipments_net_imports_export_spec.rb From 5b8b820c91d22a2bfba73285eb5229479e33f62f Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Thu, 1 Feb 2024 16:51:58 +0000 Subject: [PATCH 139/241] add Service/Serializers group in coverage page. --- spec/spec_helper.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 56a0d9f0f..bf9260178 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -8,7 +8,10 @@ formatters.push CodeClimate::TestReporter::Formatter if ENV['CI'] SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[*formatters] -SimpleCov.start 'rails' +SimpleCov.start 'rails' do + add_group "Services", "app/services" + add_group "Serializers", "app/serializers" +end # This file is copied to spec/ when you run 'rails generate rspec:install' ENV["RAILS_ENV"] ||= 'test' From 0bcd5a81d60549943f35f3ec8c52d6c27d09cc99 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Thu, 1 Feb 2024 17:58:36 +0000 Subject: [PATCH 140/241] Upgrade gem to remove DEPRECATION WARNING --- Gemfile | 2 +- Gemfile.lock | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index d88be10a4..8d193e05f 100644 --- a/Gemfile +++ b/Gemfile @@ -153,7 +153,7 @@ gem 'dotenv-rails', '2.0.1' gem 'sitemap_generator', '~> 6.3' -gem 'appsignal', '1.3.3' # TODO: should upgrade to latest after all upgrade. +gem 'appsignal', '~> 3.5', '>= 3.5.5' gem 'test-unit', '3.1.5' # annoyingly, rails console won't start without it in staging / production ### GEM for frontend ### diff --git a/Gemfile.lock b/Gemfile.lock index 4cfdd64d8..344cc962d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -78,9 +78,8 @@ GEM sshkit (>= 1.6.1, != 1.7.0) annotate (2.5.0) rake - appsignal (1.3.3) + appsignal (3.5.5) rack - thread_safe ast (2.4.2) aws-eventstream (1.3.0) aws-sdk (2.11.632) @@ -599,7 +598,7 @@ DEPENDENCIES acts-as-taggable-on (= 9.0.1) ahoy_matey (= 4.2.1) annotate (= 2.5.0) - appsignal (= 1.3.3) + appsignal (~> 3.5, >= 3.5.5) aws-sdk (~> 2) bcrypt_pbkdf (= 1.1.0) bootsnap (>= 1.4.2) From f5aede0996294cbfac56161491a4854a7b5c6653 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Thu, 1 Feb 2024 18:08:09 +0000 Subject: [PATCH 141/241] https://thoughtbot.com/blog/rails-6-warning-message-upgrade --- config/initializers/acts_as_taggable_on.rb | 30 +++------------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/config/initializers/acts_as_taggable_on.rb b/config/initializers/acts_as_taggable_on.rb index b00cc59c5..7d43529f1 100644 --- a/config/initializers/acts_as_taggable_on.rb +++ b/config/initializers/acts_as_taggable_on.rb @@ -1,26 +1,4 @@ -# Migrated to Strong Parameters -# ActsAsTaggableOn::Tag.class_eval do -# attr_accessible :name -# end - -# ActsAsTaggableOn::Tagging.class_eval do -# attr_accessible :tag_id, :context, :taggable, :taggable_id, :taggable_type, :tagger_id, :tagger_type -# end - - - -# TODO -# DEPRECATION WARNING: Initialization autoloaded the constant ComparisonAttributes. -# -# Being able to do this is deprecated. Autoloading during initialization is going -# to be an error condition in future versions of Rails. -# -# Reloading does not reboot the application, and therefore code executed during -# initialization does not run again. So, if you reload ComparisonAttributes, for example, -# the expected changes won't be reflected in that stale Module object. -# -# `config.autoloader` is set to `classic`. This autoloaded constant would have been unloaded if `config.autoloader` had been set to `:zeitwerk`. -# -# Please, check the "Autoloading and Reloading Constants" guide for solutions. -# (called from at /SAPI/config/environment.rb:5) -ActsAsTaggableOn::Tagging.send :include, ComparisonAttributes +# https://thoughtbot.com/blog/rails-6-warning-message-upgrade +Rails.application.reloader.to_prepare do + ActsAsTaggableOn::Tagging.send :include, ComparisonAttributes +end From 95606b97174b85e09496204db709cbcc524c771b Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Thu, 1 Feb 2024 21:10:29 +0000 Subject: [PATCH 142/241] Ruby 3.0.6 --- .ruby-version | 2 +- .travis.yml | 2 +- Dockerfile | 2 +- Dockerfile.cap-deploy | 16 ++- Gemfile | 8 +- Gemfile.lock | 27 ++--- app/models/trade/sandbox_template.rb | 4 +- app/models/trade_restriction.rb | 2 +- app/services/cms_mapping_manager.rb | 162 +++++++++++++-------------- app/services/iucn_mapping_manager.rb | 146 ++++++++++++------------ config/deploy.rb | 2 +- spec/spec_helper.rb | 2 +- 12 files changed, 185 insertions(+), 190 deletions(-) diff --git a/.ruby-version b/.ruby-version index 6a81b4c83..818bd47ab 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.7.8 +3.0.6 diff --git a/.travis.yml b/.travis.yml index 6ef32e0d6..48ddd864b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ cache: bundler bundler_args: --without development production staging sudo: false rvm: - - 2.7.8 + - 3.0.6 addons: postgresql: 9.4 code_climate: diff --git a/Dockerfile b/Dockerfile index 633fa1e8a..c8b325004 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Dockerfile -FROM ruby:2.7.8 +FROM ruby:3.0.6 # Rails and SAPI has some additional dependencies, e.g. rake requires a JS # runtime, so attempt to get these from apt, where possible diff --git a/Dockerfile.cap-deploy b/Dockerfile.cap-deploy index 3ea5832e8..d88a2d0b6 100644 --- a/Dockerfile.cap-deploy +++ b/Dockerfile.cap-deploy @@ -1,11 +1,12 @@ # Dockerfile -# FROM --platform=linux/amd64 debian -FROM ruby:2.7.8 +FROM ubuntu:jammy ENV DEBIAN_FRONTEND=noninteractive # Rails and SAPI has some additional dependencies, e.g. rake requires a JS # runtime, so attempt to get these from apt, where possible RUN apt-get update && apt-get install -y --force-yes \ + # SSH + openssh-client \ # For ruby? libsodium-dev libgmp3-dev \ # For RVM @@ -21,11 +22,19 @@ WORKDIR /SAPI # https://stackoverflow.com/questions/43612927/how-to-correctly-install-rvm-in-docker RUN gpg --keyserver keyserver.ubuntu.com --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB RUN curl -sSL https://get.rvm.io | bash -s -RUN /bin/bash -l -c ". /etc/profile.d/rvm.sh && rvm install 2.7.8" +RUN /bin/bash -l -c ". /etc/profile.d/rvm.sh && rvm install 3.0.6" # RVM installed in multi-user mode. However cap assume rvm is installed in single user mode. # Create a soft link to fake it. RUN mkdir -p ~/.rvm/bin && ln -s /usr/local/rvm/bin/rvm ~/.rvm/bin/rvm +COPY Gemfile /SAPI/Gemfile +COPY Gemfile.lock /SAPI/Gemfile.lock + +ENV BUNDLE_SILENCE_ROOT_WARNING=1 +RUN /bin/bash -c "source /etc/profile.d/rvm.sh \ + && gem install bundler:1.17.3 \ + && bundle" + # Host SSH key/config RUN mkdir -p ~/.ssh && ln -s /run/secrets/host_ssh_key ~/.ssh/id_ed25519 && ln -s /run/secrets/host_ssh_config ~/.ssh/config @@ -36,7 +45,6 @@ ENTRYPOINT ["/bin/bash", "-l"] ########################################## # /bin/bash # source /etc/profile.d/rvm.sh -# bundle # eval "$(ssh-agent -s)" # ssh-add # cap staging deploy diff --git a/Gemfile b/Gemfile index 8d193e05f..696100fdf 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } -ruby '2.7.8' +ruby '3.0.6' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' gem 'rails', '6.0.6.1' @@ -85,8 +85,6 @@ gem 'bootsnap', '>= 1.4.2', require: false # To use Jbuilder templates for JSON # gem 'jbuilder', '~> 2.7' -gem 'rest-client', '1.8.0', require: false # TODO, should upgrade for better compatibility with newer Ruby but breaking change. Seems not many place using it, worth a try. - group :development do # Access an interactive console on exception pages or by calling 'console' anywhere in the code. gem 'web-console', '>= 3.3.0' @@ -140,8 +138,8 @@ group :test do gem 'rails-controller-testing' gem "codeclimate-test-reporter", '0.1.1', require: nil # TODO, should be removed gem 'factory_bot_rails', '5.2.0' - gem 'simplecov', '0.22.0', :require => false # TODO: latest - gem 'coveralls', '0.7.1', :require => false + gem 'simplecov', '~> 0.22.0', :require => false + gem 'coveralls_reborn', '~> 0.28.0', require: false end gem 'geoip', '1.3.5' # TODO: no change logs, no idea if safe to update. Latest version is 1.6.4 @ 2018 diff --git a/Gemfile.lock b/Gemfile.lock index 344cc962d..3b7ba8ed9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -168,12 +168,11 @@ GEM coffee-script-source (1.12.2) concurrent-ruby (1.2.3) connection_pool (2.4.1) - coveralls (0.7.1) - multi_json (~> 1.3) - rest-client - simplecov (>= 0.7) - term-ansicolor - thor + coveralls_reborn (0.28.0) + simplecov (~> 0.22.0) + term-ansicolor (~> 1.7) + thor (~> 1.2) + tins (~> 1.32) crass (1.0.6) dalli (2.7.10) database_cleaner (2.0.2) @@ -192,7 +191,6 @@ GEM warden (~> 1.2.3) diff-lcs (1.5.0) docile (1.4.0) - domain_name (0.6.20240107) dotenv (2.0.1) dotenv-rails (2.0.1) dotenv (= 2.0.1) @@ -247,8 +245,6 @@ GEM actionpack (>= 5.2) activesupport (>= 5.2) hashery (2.1.2) - http-cookie (1.0.5) - domain_name (~> 0.5) httparty (0.21.0) mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) @@ -301,7 +297,6 @@ GEM marcel (1.0.2) matrix (0.4.2) method_source (1.0.0) - mime-types (2.99.3) mini_magick (4.12.0) mini_mime (1.1.5) mini_portile2 (2.8.5) @@ -331,7 +326,6 @@ GEM net-smtp (0.4.0.1) net-protocol net-ssh (7.2.1) - netrc (0.11.0) nio4r (2.7.0) nokogiri (1.15.5) mini_portile2 (~> 2.8.2) @@ -423,10 +417,6 @@ GEM responders (3.1.1) actionpack (>= 5.2) railties (>= 5.2) - rest-client (1.8.0) - http-cookie (>= 1.0.2, < 2.0) - mime-types (>= 1.16, < 3.0) - netrc (~> 0.7) rexml (3.2.6) rspec (3.12.0) rspec-core (~> 3.12.0) @@ -619,7 +609,7 @@ DEPENDENCIES chartkick (~> 5.0, >= 5.0.5) codeclimate-test-reporter (= 0.1.1) coffee-rails (~> 5.0) - coveralls (= 0.7.1) + coveralls_reborn (~> 0.28.0) dalli (= 2.7.10) database_cleaner (~> 2.0, >= 2.0.2) devise (= 4.7.3) @@ -660,7 +650,6 @@ DEPENDENCIES rbnacl-libsodium (= 1.0.16) request_store (~> 1.5, >= 1.5.1) responders (~> 3.1, >= 3.1.1) - rest-client (= 1.8.0) rspec-collection_matchers (~> 1.2, >= 1.2.1) rspec-rails (= 5.1.2) rubocop-rails @@ -672,7 +661,7 @@ DEPENDENCIES sidekiq-cron (~> 1.12) sidekiq-status (~> 3.0, >= 3.0.3) sidekiq-unique-jobs (= 7.1.31) - simplecov (= 0.22.0) + simplecov (~> 0.22.0) sitemap_generator (~> 6.3) slackistrano (= 0.1.9) spring @@ -690,7 +679,7 @@ DEPENDENCIES wkhtmltopdf-binary (~> 0.12.6.6) RUBY VERSION - ruby 2.7.8p225 + ruby 3.0.6p216 BUNDLED WITH 1.17.3 diff --git a/app/models/trade/sandbox_template.rb b/app/models/trade/sandbox_template.rb index e58c7f2ba..3aa616909 100644 --- a/app/models/trade/sandbox_template.rb +++ b/app/models/trade/sandbox_template.rb @@ -92,8 +92,8 @@ def self.sanitize(id = nil) ) end - def save(attributes = {}) - super(attributes) + def save(*args, **options, &block) + super(*args, **options, &block) sanitize end diff --git a/app/models/trade_restriction.rb b/app/models/trade_restriction.rb index 06a4cb3f3..763000242 100644 --- a/app/models/trade_restriction.rb +++ b/app/models/trade_restriction.rb @@ -139,7 +139,7 @@ def self.to_csv(file_path, filters) when :semicolon then ';' else ',' end - CSV.open(file_path, 'wb', { :col_sep => csv_separator_char }) do |csv| + CSV.open(file_path, 'wb', col_sep: csv_separator_char) do |csv| csv << Species::RestrictionsExport::TAXONOMY_COLUMN_NAMES + ['Remarks'] + self.csv_columns_headers ids = [] diff --git a/app/services/cms_mapping_manager.rb b/app/services/cms_mapping_manager.rb index df966f3b5..045368fdd 100644 --- a/app/services/cms_mapping_manager.rb +++ b/app/services/cms_mapping_manager.rb @@ -1,87 +1,87 @@ class CmsMappingManager + # Broken and not in-use (https://unep-wcmc.slack.com/archives/GR2EA1B0C/p1706268513001039) + # class << self - class << self + # def sync + # config_location ||= Rails.root.join('config/secrets.yml') + # @config = YAML.load_file(config_location)[Rails.env] + # index_url = @config['cms']['index'] + # @show_url = @config['cms']['show'] + # puts "#{index_url}" + # url = URI.escape(index_url) + # cms = Taxonomy.where(:name => Taxonomy::CMS).first + # species = JSON.parse(RestClient.get(url)) + # species.each do |sp| + # taxon_concept = TaxonConcept.where(:full_name => sp["scientific_name"], + # :taxonomy_id => cms.id).first + # mapping = CmsMapping.find_or_create_by( + # taxon_concept_id: taxon_concept.try(:id), cms_taxon_name: sp["scientific_name"], cms_uuid: sp["node_uuid"]) - def sync - config_location ||= Rails.root.join('config/secrets.yml') - @config = YAML.load_file(config_location)[Rails.env] - index_url = @config['cms']['index'] - @show_url = @config['cms']['show'] - puts "#{index_url}" - url = URI.escape(index_url) - cms = Taxonomy.where(:name => Taxonomy::CMS).first - species = JSON.parse(RestClient.get(url)) - species.each do |sp| - taxon_concept = TaxonConcept.where(:full_name => sp["scientific_name"], - :taxonomy_id => cms.id).first - mapping = CmsMapping.find_or_create_by( - taxon_concept_id: taxon_concept.try(:id), cms_taxon_name: sp["scientific_name"], cms_uuid: sp["node_uuid"]) + # analyse mapping + # end + # end - analyse mapping - end - end + # def analyse(mapping) + # url = URI.escape(@show_url + mapping.cms_taxon_name) + # species = JSON.parse(RestClient.get(url)).first + # if species + # puts "setting mapping details for #{mapping.cms_taxon_name}" + # mapping.cms_author = species["taxonomy"]["author"] + # taxon_concept = mapping.taxon_concept + # mapping.details = { + # 'distributions_splus' => taxon_concept && taxon_concept.distributions.size, + # 'distributions_cms' => species["geographic_range"]["range_states"].size, + # 'instruments_splus' => taxon_concept && taxon_concept.instruments.map(&:name).join(", "), + # 'instruments_cms' => species["assessment_information"].map do |ai| + # ai["instrument"] && ai["instrument"]["instrument"] + # end.join(", "), + # 'listing_splus' => taxon_concept && taxon_concept.listing_changes.first && + # "#{taxon_concept.listing_changes.first.species_listing.name}: #{taxon_concept. + # listing_changes.first.effective_at.strftime("%d/%m/%Y")}", + # 'listing_cms' => + # if species["appendix_1_date"] + # "Appendix I: #{Date.parse(species["appendix_1_date"]).strftime("%d/%m/%Y")}" + # elsif species["appendix_2_date"] + # "Appendix II: #{Date.parse(species["appendix_2_date"]).strftime("%d/%m/%Y")}" + # else + # nil + # end + # } + # mapping.save + # end + # end - def analyse(mapping) - url = URI.escape(@show_url + mapping.cms_taxon_name) - species = JSON.parse(RestClient.get(url)).first - if species - puts "setting mapping details for #{mapping.cms_taxon_name}" - mapping.cms_author = species["taxonomy"]["author"] - taxon_concept = mapping.taxon_concept - mapping.details = { - 'distributions_splus' => taxon_concept && taxon_concept.distributions.size, - 'distributions_cms' => species["geographic_range"]["range_states"].size, - 'instruments_splus' => taxon_concept && taxon_concept.instruments.map(&:name).join(", "), - 'instruments_cms' => species["assessment_information"].map do |ai| - ai["instrument"] && ai["instrument"]["instrument"] - end.join(", "), - 'listing_splus' => taxon_concept && taxon_concept.listing_changes.first && - "#{taxon_concept.listing_changes.first.species_listing.name}: #{taxon_concept. - listing_changes.first.effective_at.strftime("%d/%m/%Y")}", - 'listing_cms' => - if species["appendix_1_date"] - "Appendix I: #{Date.parse(species["appendix_1_date"]).strftime("%d/%m/%Y")}" - elsif species["appendix_2_date"] - "Appendix II: #{Date.parse(species["appendix_2_date"]).strftime("%d/%m/%Y")}" - else - nil - end - } - mapping.save - end - end - - # Finds CITES' taxon concepts that match the CMS Species or Subspecies and - # copies the distributions and distribution references from the CITES to the CMS taxon concept. - # It also creates a taxon_relationship of EQUAL_TO between both taxon concepts - def fill_cms_distributions - species = Rank.where(:name => Rank::SPECIES).first - subspecies = Rank.where(:name => Rank::SUBSPECIES).first - cms = Taxonomy.where(:name => Taxonomy::CMS).first - cites = Taxonomy.where(:name => Taxonomy::CITES_EU).first - equal_to = TaxonRelationshipType.where(:name => TaxonRelationshipType::EQUAL_TO).first - TaxonConcept.where(:rank_id => [species.id, subspecies.id], - :taxonomy_id => cms.id).each do |taxon| - matching_cites_taxon = TaxonConcept.where(:rank_id => taxon.rank_id, - :full_name => taxon.full_name, - :taxonomy_id => cites.id).first - next unless matching_cites_taxon - puts "found a match for #{taxon.full_name} #{taxon.id} matches #{matching_cites_taxon.id}" - matching_cites_taxon.distributions.each do |dist| - distribution = Distribution.find_or_initialize_by( - taxon_concept_id: taxon.id, geo_entity_id: ist.geo_entity_id) - distribution.tag_list = dist.tag_list - distribution.save - dist.distribution_references.each do |reference| - DistributionReference.find_or_create_by( - distribution_id: distribution.id, reference_id: reference.reference_id) - end - end - puts "creating taxon relationship" - TaxonRelationship.create(:taxon_concept_id => matching_cites_taxon.id, - :other_taxon_concept_id => taxon.id, - :taxon_relationship_type_id => equal_to.id) - end - end - end + # # Finds CITES' taxon concepts that match the CMS Species or Subspecies and + # # copies the distributions and distribution references from the CITES to the CMS taxon concept. + # # It also creates a taxon_relationship of EQUAL_TO between both taxon concepts + # def fill_cms_distributions + # species = Rank.where(:name => Rank::SPECIES).first + # subspecies = Rank.where(:name => Rank::SUBSPECIES).first + # cms = Taxonomy.where(:name => Taxonomy::CMS).first + # cites = Taxonomy.where(:name => Taxonomy::CITES_EU).first + # equal_to = TaxonRelationshipType.where(:name => TaxonRelationshipType::EQUAL_TO).first + # TaxonConcept.where(:rank_id => [species.id, subspecies.id], + # :taxonomy_id => cms.id).each do |taxon| + # matching_cites_taxon = TaxonConcept.where(:rank_id => taxon.rank_id, + # :full_name => taxon.full_name, + # :taxonomy_id => cites.id).first + # next unless matching_cites_taxon + # puts "found a match for #{taxon.full_name} #{taxon.id} matches #{matching_cites_taxon.id}" + # matching_cites_taxon.distributions.each do |dist| + # distribution = Distribution.find_or_initialize_by( + # taxon_concept_id: taxon.id, geo_entity_id: ist.geo_entity_id) + # distribution.tag_list = dist.tag_list + # distribution.save + # dist.distribution_references.each do |reference| + # DistributionReference.find_or_create_by( + # distribution_id: distribution.id, reference_id: reference.reference_id) + # end + # end + # puts "creating taxon relationship" + # TaxonRelationship.create(:taxon_concept_id => matching_cites_taxon.id, + # :other_taxon_concept_id => taxon.id, + # :taxon_relationship_type_id => equal_to.id) + # end + # end + # end end diff --git a/app/services/iucn_mapping_manager.rb b/app/services/iucn_mapping_manager.rb index 9e3506750..150e9b0fb 100644 --- a/app/services/iucn_mapping_manager.rb +++ b/app/services/iucn_mapping_manager.rb @@ -1,82 +1,82 @@ class IucnMappingManager + # Broken and not in-use (https://unep-wcmc.slack.com/archives/GR2EA1B0C/p1706268513001039) + # class << self - class << self + # def sync + # species = Rank.where(:name => Rank::SPECIES).first + # @subspecies = Rank.where(:name => Rank::SUBSPECIES).first + # taxonomy = Taxonomy.where(:name => Taxonomy::CITES_EU).first - def sync - species = Rank.where(:name => Rank::SPECIES).first - @subspecies = Rank.where(:name => Rank::SUBSPECIES).first - taxonomy = Taxonomy.where(:name => Taxonomy::CITES_EU).first + # TaxonConcept.where(:rank_id => [species.id, @subspecies.id], :name_status => ['A', 'S'], + # :taxonomy_id => taxonomy.id).each do |taxon_concept| + # sync_taxon_concept taxon_concept + # end + # end - TaxonConcept.where(:rank_id => [species.id, @subspecies.id], :name_status => ['A', 'S'], - :taxonomy_id => taxonomy.id).each do |taxon_concept| - sync_taxon_concept taxon_concept - end - end + # def sync_taxon_concept(taxon_concept) + # mapping = IucnMapping.find_or_create_by(taxon_concept_id: taxon_concept.id) + # full_name = if taxon_concept.rank_id == @subspecies.id + # taxon_concept.full_name.insert(taxon_concept.full_name.rindex(/ /), " ssp.") + # else + # taxon_concept.full_name + # end + # data = fetch_data_for_name full_name + # if !data || !data["result"] || data["result"].empty? + # puts "#{taxon_concept.full_name} NO MATCH" + # else + # map_taxon_concept taxon_concept, mapping, data + # end + # end - def sync_taxon_concept(taxon_concept) - mapping = IucnMapping.find_or_create_by(taxon_concept_id: taxon_concept.id) - full_name = if taxon_concept.rank_id == @subspecies.id - taxon_concept.full_name.insert(taxon_concept.full_name.rindex(/ /), " ssp.") - else - taxon_concept.full_name - end - data = fetch_data_for_name full_name - if !data || !data["result"] || data["result"].empty? - puts "#{taxon_concept.full_name} NO MATCH" - else - map_taxon_concept taxon_concept, mapping, data - end - end + # def fetch_data_for_name(full_name) + # @config_location ||= Rails.root.join('config/secrets.yml') + # @config ||= YAML.load_file(@config_location)[Rails.env] + # @token ||= @config['iucn_redlist']['token'] + # @url ||= @config['iucn_redlist']['url'] + # puts "#{@url}#{full_name.downcase}?token=#{@token}" + # url = URI.escape("#{@url}#{full_name.downcase}?token=#{@token}") + # JSON.parse(RestClient.get(url)) + # end - def fetch_data_for_name(full_name) - @config_location ||= Rails.root.join('config/secrets.yml') - @config ||= YAML.load_file(@config_location)[Rails.env] - @token ||= @config['iucn_redlist']['token'] - @url ||= @config['iucn_redlist']['url'] - puts "#{@url}#{full_name.downcase}?token=#{@token}" - url = URI.escape("#{@url}#{full_name.downcase}?token=#{@token}") - JSON.parse(RestClient.get(url)) - end + # def map_taxon_concept(taxon_concept, map, data) + # begin + # match = data["result"].first + # puts "#{taxon_concept.full_name} #{taxon_concept.author_year} <=> #{match["scientific_name"]} #{match["authority"]}" + # map.update( + # :iucn_taxon_name => match['scientific_name'], + # :iucn_taxon_id => match['taxonid'], + # :iucn_author => match['authority'], + # :iucn_category => match['category'], + # :details => { + # :match => type_of_match(taxon_concept, match), + # :no_matches => data["result"].size + # }, + # :accepted_name_id => taxon_concept.name_status == 'S' ? taxon_concept.accepted_names.first.try(:id) : nil + # ) + # rescue Exception => e + # puts "#######################################################################" + # puts "########################## EXCEPTION Taxon Concept #{taxon_concept.id} ###########" + # puts e.message + # end + # end - def map_taxon_concept(taxon_concept, map, data) - begin - match = data["result"].first - puts "#{taxon_concept.full_name} #{taxon_concept.author_year} <=> #{match["scientific_name"]} #{match["authority"]}" - map.update( - :iucn_taxon_name => match['scientific_name'], - :iucn_taxon_id => match['taxonid'], - :iucn_author => match['authority'], - :iucn_category => match['category'], - :details => { - :match => type_of_match(taxon_concept, match), - :no_matches => data["result"].size - }, - :accepted_name_id => taxon_concept.name_status == 'S' ? taxon_concept.accepted_names.first.try(:id) : nil - ) - rescue Exception => e - puts "#######################################################################" - puts "########################## EXCEPTION Taxon Concept #{taxon_concept.id} ###########" - puts e.message - end - end + # def type_of_match(tc, match) + # if tc.full_name == match["scientific_name"] + # if strip_authors(tc.author_year) == strip_authors(match["authority"]) + # puts "FULL_MATCH!" + # "FULL_MATCH" + # else + # puts "NAME_MATCH" + # "NAME_MATCH" + # end + # end + # end - def type_of_match(tc, match) - if tc.full_name == match["scientific_name"] - if strip_authors(tc.author_year) == strip_authors(match["authority"]) - puts "FULL_MATCH!" - "FULL_MATCH" - else - puts "NAME_MATCH" - "NAME_MATCH" - end - end - end - - def strip_authors(author) - return '' unless author - author.split(" "). - reject { |p| ["and", "&", "&", ","].include?(p) }. - join(" ") - end - end + # def strip_authors(author) + # return '' unless author + # author.split(" "). + # reject { |p| ["and", "&", "&", ","].include?(p) }. + # join(" ") + # end + # end end diff --git a/config/deploy.rb b/config/deploy.rb index fd9e5ebaf..3ece01f92 100644 --- a/config/deploy.rb +++ b/config/deploy.rb @@ -20,7 +20,7 @@ # set :format, :pretty set :rvm_type, :user -set :rvm_ruby_version, '2.7.8' +set :rvm_ruby_version, '3.0.6' # Sidekiq config set :sidekiq_service_unit_user, :system diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index bf9260178..5f795b2c5 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -7,7 +7,7 @@ formatters = [Coveralls::SimpleCov::Formatter, SimpleCov::Formatter::HTMLFormatter] formatters.push CodeClimate::TestReporter::Formatter if ENV['CI'] -SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[*formatters] +SimpleCov.formatters = formatters SimpleCov.start 'rails' do add_group "Services", "app/services" add_group "Serializers", "app/serializers" From bc7aa8e651092b1ded23e98fc018d27eee919f23 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 2 Feb 2024 09:49:39 +0000 Subject: [PATCH 143/241] update cap to support ruby 3 --- Gemfile | 6 +++--- Gemfile.lock | 14 +++++++------- config/deploy.rb | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Gemfile b/Gemfile index 696100fdf..3532dbf33 100644 --- a/Gemfile +++ b/Gemfile @@ -96,9 +96,9 @@ group :development do gem 'annotate', "2.5.0" # gem 'sextant' # Deploy with Capistrano - gem 'capistrano', '3.11.0', require: false - gem 'capistrano-rails', '1.4.0', require: false - gem 'capistrano-bundler', '1.5.0', require: false + gem 'capistrano', '3.18.0', require: false + gem 'capistrano-rails', '1.6.3', require: false + gem 'capistrano-bundler', '1.6.0', require: false gem 'capistrano-rvm', '0.1.2', require: false gem 'capistrano-maintenance', '1.0.0', require: false gem 'capistrano-passenger', '0.2.0', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 3b7ba8ed9..f3e8e21cb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -114,12 +114,12 @@ GEM builder (3.2.4) byebug (11.1.3) cancancan (3.5.0) - capistrano (3.11.0) + capistrano (3.18.0) airbrussh (>= 1.0.0) i18n rake (>= 10.0.0) sshkit (>= 1.9.0) - capistrano-bundler (1.5.0) + capistrano-bundler (1.6.0) capistrano (~> 3.1) capistrano-local-precompile (1.2.0) capistrano (>= 3.8) @@ -127,9 +127,9 @@ GEM capistrano (>= 3.0) capistrano-passenger (0.2.0) capistrano (~> 3.0) - capistrano-rails (1.4.0) + capistrano-rails (1.6.3) capistrano (~> 3.1) - capistrano-bundler (~> 1.1) + capistrano-bundler (>= 1.1, < 3) capistrano-rvm (0.1.2) capistrano (~> 3.0) sshkit (~> 1.2) @@ -596,12 +596,12 @@ DEPENDENCIES brightbox (= 2.3.9) byebug cancancan (~> 3.5) - capistrano (= 3.11.0) - capistrano-bundler (= 1.5.0) + capistrano (= 3.18.0) + capistrano-bundler (= 1.6.0) capistrano-local-precompile (= 1.2.0) capistrano-maintenance (= 1.0.0) capistrano-passenger (= 0.2.0) - capistrano-rails (= 1.4.0) + capistrano-rails (= 1.6.3) capistrano-rvm (= 0.1.2) capistrano-sidekiq (~> 2.3, >= 2.3.1) capybara (>= 2.15) diff --git a/config/deploy.rb b/config/deploy.rb index 3ece01f92..a8056ce7f 100644 --- a/config/deploy.rb +++ b/config/deploy.rb @@ -1,5 +1,5 @@ # config valid only for current version of Capistrano -lock '3.11.0' +lock '3.18.0' set :application, 'sapi' set :repo_url, 'git@github.com:unepwcmc/SAPI.git' From cc96ac6b04507dd7c7f27de2900707b208d23af5 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 2 Feb 2024 10:20:34 +0000 Subject: [PATCH 144/241] Rails 6.1; app:update; rails diff --- .gitattributes | 10 ++ Gemfile | 18 ++- Gemfile.lock | 152 +++++++++--------- bin/rails | 5 +- bin/rake | 5 +- bin/setup | 6 +- bin/spring | 14 ++ bin/yarn | 12 +- config.ru | 3 +- config/application.rb | 13 +- config/boot.rb | 4 +- config/environment.rb | 2 +- config/environments/development.rb | 20 ++- config/environments/production.rb | 17 +- config/environments/staging.rb | 13 +- config/environments/test.rb | 13 +- .../0_new_framework_defaults_6_0.rb | 45 ------ .../0_new_framework_defaults_6_1.rb | 67 ++++++++ config/initializers/backtrace_silencers.rb | 7 +- .../initializers/filter_parameter_logging.rb | 4 +- config/initializers/permissions_policy.rb | 11 ++ config/puma.rb | 7 +- 22 files changed, 286 insertions(+), 162 deletions(-) create mode 100644 .gitattributes create mode 100755 bin/spring delete mode 100644 config/initializers/0_new_framework_defaults_6_0.rb create mode 100644 config/initializers/0_new_framework_defaults_6_1.rb create mode 100644 config/initializers/permissions_policy.rb diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..516857104 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +# See https://git-scm.com/docs/gitattributes for more about git attribute files. + +# Mark the database schema as having been generated. +db/schema.rb linguist-generated + +# Mark the yarn lockfile as having been generated. +yarn.lock linguist-generated + +# Mark any vendored files as having been vendored. +vendor/* linguist-vendored diff --git a/Gemfile b/Gemfile index 3532dbf33..d9221351c 100644 --- a/Gemfile +++ b/Gemfile @@ -4,11 +4,11 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby '3.0.6' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' -gem 'rails', '6.0.6.1' +gem 'rails', '6.1.7.6' # Use sqlite3 as the database for Active Record # gem 'sqlite3' # Use Puma as the app server -gem 'puma', '~> 4.1' +gem 'puma', '~> 5.0' # Use SCSS for stylesheets gem 'sass-rails', '>= 6' # https://stackoverflow.com/questions/55213868/rails-6-how-to-disable-webpack-and-use-sprockets-instead @@ -80,18 +80,20 @@ gem 'strong_migrations', '~> 1.7' # gem 'mini_magick', '~> 4.8' # Reduces boot times through caching; required in config/boot.rb -gem 'bootsnap', '>= 1.4.2', require: false +gem 'bootsnap', '>= 1.4.4', require: false # To use Jbuilder templates for JSON # gem 'jbuilder', '~> 2.7' group :development do # Access an interactive console on exception pages or by calling 'console' anywhere in the code. - gem 'web-console', '>= 3.3.0' - gem 'listen', '~> 3.2' + gem 'web-console', '>= 4.1.0' + # Display performance information such as SQL time and flame graphs for each request in your browser. + # Can be configured to work on production as well see: https://github.com/MiniProfiler/rack-mini-profiler/blob/master/README.md + gem 'rack-mini-profiler', '~> 2.0' + gem 'listen', '~> 3.3' # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'spring' - gem 'spring-watcher-listen', '~> 2.0.0' gem 'annotate', "2.5.0" # gem 'sextant' @@ -130,8 +132,8 @@ end group :test do # Adds support for Capybara system testing and selenium driver - gem 'capybara', '>= 2.15' - gem 'selenium-webdriver' + gem 'capybara', '>= 3.26' + gem 'selenium-webdriver', '>= 4.0.0.rc1' # Easy installation and use of web drivers to run system tests with browsers gem 'webdrivers' diff --git a/Gemfile.lock b/Gemfile.lock index f3e8e21cb..201596d00 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,27 +2,29 @@ GEM remote: https://rubygems.org/ specs: Ascii85 (1.0.3) - actioncable (6.0.6.1) - actionpack (= 6.0.6.1) + actioncable (6.1.7.6) + actionpack (= 6.1.7.6) + activesupport (= 6.1.7.6) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.0.6.1) - actionpack (= 6.0.6.1) - activejob (= 6.0.6.1) - activerecord (= 6.0.6.1) - activestorage (= 6.0.6.1) - activesupport (= 6.0.6.1) + actionmailbox (6.1.7.6) + actionpack (= 6.1.7.6) + activejob (= 6.1.7.6) + activerecord (= 6.1.7.6) + activestorage (= 6.1.7.6) + activesupport (= 6.1.7.6) mail (>= 2.7.1) - actionmailer (6.0.6.1) - actionpack (= 6.0.6.1) - actionview (= 6.0.6.1) - activejob (= 6.0.6.1) + actionmailer (6.1.7.6) + actionpack (= 6.1.7.6) + actionview (= 6.1.7.6) + activejob (= 6.1.7.6) + activesupport (= 6.1.7.6) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.0.6.1) - actionview (= 6.0.6.1) - activesupport (= 6.0.6.1) - rack (~> 2.0, >= 2.0.8) + actionpack (6.1.7.6) + actionview (= 6.1.7.6) + activesupport (= 6.1.7.6) + rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) @@ -30,14 +32,14 @@ GEM actionpack (>= 4.0.0) actionpack-page_caching (1.2.4) actionpack (>= 4.0.0) - actiontext (6.0.6.1) - actionpack (= 6.0.6.1) - activerecord (= 6.0.6.1) - activestorage (= 6.0.6.1) - activesupport (= 6.0.6.1) + actiontext (6.1.7.6) + actionpack (= 6.1.7.6) + activerecord (= 6.1.7.6) + activestorage (= 6.1.7.6) + activesupport (= 6.1.7.6) nokogiri (>= 1.8.5) - actionview (6.0.6.1) - activesupport (= 6.0.6.1) + actionview (6.1.7.6) + activesupport (= 6.1.7.6) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -46,25 +48,27 @@ GEM ember-data-source (>= 1.13, < 3.0) active_model_serializers (0.8.4) activemodel (>= 3.0) - activejob (6.0.6.1) - activesupport (= 6.0.6.1) + activejob (6.1.7.6) + activesupport (= 6.1.7.6) globalid (>= 0.3.6) - activemodel (6.0.6.1) - activesupport (= 6.0.6.1) - activerecord (6.0.6.1) - activemodel (= 6.0.6.1) - activesupport (= 6.0.6.1) - activestorage (6.0.6.1) - actionpack (= 6.0.6.1) - activejob (= 6.0.6.1) - activerecord (= 6.0.6.1) + activemodel (6.1.7.6) + activesupport (= 6.1.7.6) + activerecord (6.1.7.6) + activemodel (= 6.1.7.6) + activesupport (= 6.1.7.6) + activestorage (6.1.7.6) + actionpack (= 6.1.7.6) + activejob (= 6.1.7.6) + activerecord (= 6.1.7.6) + activesupport (= 6.1.7.6) marcel (~> 1.0) - activesupport (6.0.6.1) + mini_mime (>= 1.1.0) + activesupport (6.1.7.6) concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) - zeitwerk (~> 2.2, >= 2.2.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + zeitwerk (~> 2.3) acts-as-taggable-on (9.0.1) activerecord (>= 6.0, < 7.1) addressable (2.8.6) @@ -231,8 +235,8 @@ GEM et-orbi (~> 1, >= 1.2.7) raabro (~> 1.4) geoip (1.3.5) - globalid (1.1.0) - activesupport (>= 5.0) + globalid (1.2.1) + activesupport (>= 6.1) gon (6.4.0) actionpack (>= 3.0.20) i18n (>= 0.7) @@ -359,28 +363,30 @@ GEM ruby-rc4 ttfunk (~> 1.0.3) public_suffix (5.0.4) - puma (4.3.12) + puma (5.6.8) nio4r (~> 2.0) raabro (1.4.0) racc (1.7.3) rack (2.2.8) rack-cors (0.3.0) + rack-mini-profiler (2.3.4) + rack (>= 1.2.0) rack-test (2.1.0) rack (>= 1.3) - rails (6.0.6.1) - actioncable (= 6.0.6.1) - actionmailbox (= 6.0.6.1) - actionmailer (= 6.0.6.1) - actionpack (= 6.0.6.1) - actiontext (= 6.0.6.1) - actionview (= 6.0.6.1) - activejob (= 6.0.6.1) - activemodel (= 6.0.6.1) - activerecord (= 6.0.6.1) - activestorage (= 6.0.6.1) - activesupport (= 6.0.6.1) - bundler (>= 1.3.0) - railties (= 6.0.6.1) + rails (6.1.7.6) + actioncable (= 6.1.7.6) + actionmailbox (= 6.1.7.6) + actionmailer (= 6.1.7.6) + actionpack (= 6.1.7.6) + actiontext (= 6.1.7.6) + actionview (= 6.1.7.6) + activejob (= 6.1.7.6) + activemodel (= 6.1.7.6) + activerecord (= 6.1.7.6) + activestorage (= 6.1.7.6) + activesupport (= 6.1.7.6) + bundler (>= 1.15.0) + railties (= 6.1.7.6) sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) @@ -395,12 +401,12 @@ GEM nokogiri (~> 1.14) rails-observers (0.1.5) activemodel (>= 4.0) - railties (6.0.6.1) - actionpack (= 6.0.6.1) - activesupport (= 6.0.6.1) + railties (6.1.7.6) + actionpack (= 6.1.7.6) + activesupport (= 6.1.7.6) method_source - rake (>= 0.8.7) - thor (>= 0.20.3, < 2.0) + rake (>= 12.2) + thor (~> 1.0) rainbow (3.1.1) rake (13.1.0) rb-fsevent (0.11.2) @@ -518,9 +524,6 @@ GEM capistrano (>= 3.0.1) json spring (2.1.1) - spring-watcher-listen (2.0.1) - listen (>= 2.7, < 4.0) - spring (>= 1.2, < 3.0) sprockets (3.7.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) @@ -544,14 +547,13 @@ GEM test-unit (3.1.5) power_assert thor (1.3.0) - thread_safe (0.3.6) tilt (2.3.0) timeout (0.4.1) tins (1.32.1) sync ttfunk (1.0.3) - tzinfo (1.2.11) - thread_safe (~> 0.1) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) uglifier (4.2.0) execjs (>= 0.3.0, < 3) unicode-display_width (2.5.0) @@ -591,7 +593,7 @@ DEPENDENCIES appsignal (~> 3.5, >= 3.5.5) aws-sdk (~> 2) bcrypt_pbkdf (= 1.1.0) - bootsnap (>= 1.4.2) + bootsnap (>= 1.4.4) bootstrap-sass (= 2.3.2.2) brightbox (= 2.3.9) byebug @@ -604,7 +606,7 @@ DEPENDENCIES capistrano-rails (= 1.6.3) capistrano-rvm (= 0.1.2) capistrano-sidekiq (~> 2.3, >= 2.3.1) - capybara (>= 2.15) + capybara (>= 3.26) carrierwave (= 2.2.5) chartkick (~> 5.0, >= 5.0.5) codeclimate-test-reporter (= 0.1.1) @@ -629,7 +631,7 @@ DEPENDENCIES json_spec (~> 1.1, >= 1.1.5) kaminari (~> 1.2, >= 1.2.2) launchy (= 2.4.3) - listen (~> 3.2) + listen (~> 3.3) mobility (~> 1.2, >= 1.2.9) nested-hstore (~> 0.1.2) nested_form (~> 0.3.2) @@ -641,9 +643,10 @@ DEPENDENCIES pg_array_parser (~> 0.0.9) pg_search (~> 2.3, >= 2.3.6) prawn (= 0.13.2) - puma (~> 4.1) + puma (~> 5.0) rack-cors (= 0.3.0) - rails (= 6.0.6.1) + rack-mini-profiler (~> 2.0) + rails (= 6.1.7.6) rails-controller-testing rails-observers (~> 0.1.5) rbnacl (= 4.0.2) @@ -656,7 +659,7 @@ DEPENDENCIES rubocop-rspec rubyzip (~> 2.3, >= 2.3.2) sass-rails (>= 6) - selenium-webdriver + selenium-webdriver (>= 4.0.0.rc1) sidekiq (< 7) sidekiq-cron (~> 1.12) sidekiq-status (~> 3.0, >= 3.0.3) @@ -665,7 +668,6 @@ DEPENDENCIES sitemap_generator (~> 6.3) slackistrano (= 0.1.9) spring - spring-watcher-listen (~> 2.0.0) sprockets (= 3.7.2) sprockets-rails strong_migrations (~> 1.7) @@ -673,7 +675,7 @@ DEPENDENCIES test-unit (= 3.1.5) uglifier (>= 1.3.0) uuidtools (~> 2.2) - web-console (>= 3.3.0) + web-console (>= 4.1.0) webdrivers wicked (= 1.3.4) wkhtmltopdf-binary (~> 0.12.6.6) diff --git a/bin/rails b/bin/rails index 073966023..21d3e02d8 100755 --- a/bin/rails +++ b/bin/rails @@ -1,4 +1,5 @@ #!/usr/bin/env ruby +load File.expand_path("spring", __dir__) APP_PATH = File.expand_path('../config/application', __dir__) -require_relative '../config/boot' -require 'rails/commands' +require_relative "../config/boot" +require "rails/commands" diff --git a/bin/rake b/bin/rake index 17240489f..7327f471e 100755 --- a/bin/rake +++ b/bin/rake @@ -1,4 +1,5 @@ #!/usr/bin/env ruby -require_relative '../config/boot' -require 'rake' +load File.expand_path("spring", __dir__) +require_relative "../config/boot" +require "rake" Rake.application.run diff --git a/bin/setup b/bin/setup index 5853b5ea8..2f3d36d37 100755 --- a/bin/setup +++ b/bin/setup @@ -1,5 +1,5 @@ #!/usr/bin/env ruby -require 'fileutils' +require "fileutils" # path to your application root. APP_ROOT = File.expand_path('..', __dir__) @@ -9,8 +9,8 @@ def system!(*args) end FileUtils.chdir APP_ROOT do - # This script is a way to setup or update your development environment automatically. - # This script is idempotent, so that you can run it at anytime and get an expectable outcome. + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. # Add necessary setup steps to this file. puts '== Installing dependencies ==' diff --git a/bin/spring b/bin/spring new file mode 100755 index 000000000..b4147e843 --- /dev/null +++ b/bin/spring @@ -0,0 +1,14 @@ +#!/usr/bin/env ruby +if !defined?(Spring) && [nil, "development", "test"].include?(ENV["RAILS_ENV"]) + gem "bundler" + require "bundler" + + # Load Spring without loading other gems in the Gemfile, for speed. + Bundler.locked_gems&.specs&.find { |spec| spec.name == "spring" }&.tap do |spring| + Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path + gem "spring", spring.version + require "spring/binstub" + rescue Gem::LoadError + # Ignore when Spring is not installed. + end +end diff --git a/bin/yarn b/bin/yarn index 460dd565b..9fab2c350 100755 --- a/bin/yarn +++ b/bin/yarn @@ -1,9 +1,15 @@ #!/usr/bin/env ruby APP_ROOT = File.expand_path('..', __dir__) Dir.chdir(APP_ROOT) do - begin - exec "yarnpkg", *ARGV - rescue Errno::ENOENT + yarn = ENV["PATH"].split(File::PATH_SEPARATOR). + select { |dir| File.expand_path(dir) != __dir__ }. + product(["yarn", "yarn.cmd", "yarn.ps1"]). + map { |dir, file| File.expand_path(file, dir) }. + find { |file| File.executable?(file) } + + if yarn + exec yarn, *ARGV + else $stderr.puts "Yarn executable was not detected in the system." $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" exit 1 diff --git a/config.ru b/config.ru index f7ba0b527..4a3c09a68 100644 --- a/config.ru +++ b/config.ru @@ -1,5 +1,6 @@ # This file is used by Rack-based servers to start the application. -require_relative 'config/environment' +require_relative "config/environment" run Rails.application +Rails.application.load_server diff --git a/config/application.rb b/config/application.rb index 5f5670c07..0cc5846e0 100644 --- a/config/application.rb +++ b/config/application.rb @@ -11,12 +11,15 @@ module SAPI class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 6.0 + config.load_defaults 6.1 - # Settings in config/environments/* take precedence over those specified here. - # Application configuration can go into files in config/initializers - # -- all .rb files in that directory are automatically loaded after loading - # the framework and any gems in your application. + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + config.time_zone = "London" + # config.eager_load_paths << Rails.root.join("extras") # @see https://gist.github.com/maxivak/381f1e964923f1d469c8d39da8e2522f # TODO: Rails 7.1 https://stackoverflow.com/a/77198784/556780 diff --git a/config/boot.rb b/config/boot.rb index b9e460cef..3cda23b4d 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -1,4 +1,4 @@ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) -require 'bundler/setup' # Set up gems listed in the Gemfile. -require 'bootsnap/setup' # Speed up boot time by caching expensive operations. +require "bundler/setup" # Set up gems listed in the Gemfile. +require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/config/environment.rb b/config/environment.rb index 426333bb4..cac531577 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -1,5 +1,5 @@ # Load the Rails application. -require_relative 'application' +require_relative "application" # Initialize the Rails application. Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb index bd2b4a5de..bf5152379 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,8 +1,10 @@ +require "active_support/core_ext/integer/time" + Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. - # In the development environment your application's code is reloaded on - # every request. This slows down response time but is perfect for development + # In the development environment your application's code is reloaded any time + # it changes. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. config.cache_classes = false @@ -39,6 +41,12 @@ # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load @@ -54,12 +62,18 @@ config.assets.quiet = true # Raises error for missing translations. - # config.action_view.raise_on_missing_translations = true + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true # Use an evented file watcher to asynchronously detect changes in source code, # routes, locales, etc. This feature depends on the listen gem. config.file_watcher = ActiveSupport::EventedFileUpdateChecker + # Uncomment if you wish to allow Action Cable access from any origin. + # config.action_cable.disable_request_forgery_protection = true + # Custom ember settings config.ember.variant = :development diff --git a/config/environments/production.rb b/config/environments/production.rb index 7cd7b0fef..79c3f6a68 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -1,3 +1,5 @@ +require "active_support/core_ext/integer/time" + Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. @@ -31,7 +33,7 @@ config.assets.compile = false # Enable serving of images, stylesheets, and JavaScripts from an asset server. - # config.action_controller.asset_host = 'http://assets.example.com' + # config.asset_host = 'http://assets.example.com' # Specifies the header that your server uses for sending files. # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache @@ -48,8 +50,8 @@ # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # config.force_ssl = true - # Use the lowest log level to ensure availability of diagnostic information - # when problems arise. + # Include generic and useful information about system operation, but avoid logging too much + # information to avoid inadvertent exposure of personally identifiable information (PII). config.log_level = :warn # @see https://github.com/heartcombo/devise#password-reset-tokens-and-rails-logs # Prepend all log lines with the following tags. @@ -61,6 +63,7 @@ # Use a real queuing backend for Active Job (and separate queues per environment). # config.active_job.queue_adapter = :resque # config.active_job.queue_name_prefix = "sapi_production" + config.action_mailer.perform_caching = false # Ignore bad email addresses and do not raise email delivery errors. @@ -75,11 +78,17 @@ # Send deprecation notices to registered listeners. config.active_support.deprecation = :notify + # Log disallowed deprecations. + config.active_support.disallowed_deprecation = :log + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + # Use default logging formatter so that PID and timestamp are not suppressed. config.log_formatter = ::Logger::Formatter.new # Use a different logger for distributed setups. - # require 'syslog/logger' + # require "syslog/logger" # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') if ENV["RAILS_LOG_TO_STDOUT"].present? diff --git a/config/environments/staging.rb b/config/environments/staging.rb index ce7c7de92..fb92d8c2f 100644 --- a/config/environments/staging.rb +++ b/config/environments/staging.rb @@ -1,3 +1,5 @@ +require "active_support/core_ext/integer/time" + Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb @@ -31,7 +33,7 @@ config.assets.compile = false # Enable serving of images, stylesheets, and JavaScripts from an asset server. - # config.action_controller.asset_host = 'http://assets.example.com' + # config.asset_host = 'http://assets.example.com' # Specifies the header that your server uses for sending files. # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache @@ -60,7 +62,8 @@ # Use a real queuing backend for Active Job (and separate queues per environment) # config.active_job.queue_adapter = :resque - # config.active_job.queue_name_prefix = "sapi_#{Rails.env}" + # config.active_job.queue_name_prefix = "sapi_staging" + config.action_mailer.perform_caching = false # Ignore bad email addresses and do not raise email delivery errors. @@ -75,6 +78,12 @@ # Send deprecation notices to registered listeners. config.active_support.deprecation = :notify + # Log disallowed deprecations. + config.active_support.disallowed_deprecation = :log + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + # Use default logging formatter so that PID and timestamp are not suppressed. config.log_formatter = ::Logger::Formatter.new diff --git a/config/environments/test.rb b/config/environments/test.rb index 9579685c1..1607373bb 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,3 +1,5 @@ +require "active_support/core_ext/integer/time" + # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped @@ -45,8 +47,17 @@ # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + # Raises error for missing translations. - # config.action_view.raise_on_missing_translations = true + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true # Custom ember settings config.ember.variant = :development diff --git a/config/initializers/0_new_framework_defaults_6_0.rb b/config/initializers/0_new_framework_defaults_6_0.rb deleted file mode 100644 index 223024573..000000000 --- a/config/initializers/0_new_framework_defaults_6_0.rb +++ /dev/null @@ -1,45 +0,0 @@ -# Be sure to restart your server when you modify this file. -# -# This file contains migration options to ease your Rails 6.0 upgrade. -# -# Once upgraded flip defaults one by one to migrate to the new default. -# -# Read the Guide for Upgrading Ruby on Rails for more info on each option. - -# Don't force requests from old versions of IE to be UTF-8 encoded. -# Rails.application.config.action_view.default_enforce_utf8 = false - -# Embed purpose and expiry metadata inside signed and encrypted -# cookies for increased security. -# -# This option is not backwards compatible with earlier Rails versions. -# It's best enabled when your entire app is migrated and stable on 6.0. -Rails.application.config.action_dispatch.use_cookies_with_metadata = true - -# Change the return value of `ActionDispatch::Response#content_type` to Content-Type header without modification. -Rails.application.config.action_dispatch.return_only_media_type_on_content_type = false - -# Return false instead of self when enqueuing is aborted from a callback. -Rails.application.config.active_job.return_false_on_aborted_enqueue = true - -# Send Active Storage analysis and purge jobs to dedicated queues. -Rails.application.config.active_storage.queues.analysis = :active_storage_analysis -Rails.application.config.active_storage.queues.purge = :active_storage_purge - -# When assigning to a collection of attachments declared via `has_many_attached`, replace existing -# attachments instead of appending. Use #attach to add new attachments without replacing existing ones. -Rails.application.config.active_storage.replace_on_assign_to_many = true - -# Use ActionMailer::MailDeliveryJob for sending parameterized and normal mail. -# -# The default delivery jobs (ActionMailer::Parameterized::DeliveryJob, ActionMailer::DeliveryJob), -# will be removed in Rails 6.1. This setting is not backwards compatible with earlier Rails versions. -# If you send mail in the background, job workers need to have a copy of -# MailDeliveryJob to ensure all delivery jobs are processed properly. -# Make sure your entire app is migrated and stable on 6.0 before using this setting. -Rails.application.config.action_mailer.delivery_job = "ActionMailer::MailDeliveryJob" - -# Enable the same cache key to be reused when the object being cached of type -# `ActiveRecord::Relation` changes by moving the volatile information (max updated at and count) -# of the relation's cache key into the cache version to support recycling cache key. -Rails.application.config.active_record.collection_cache_versioning = true diff --git a/config/initializers/0_new_framework_defaults_6_1.rb b/config/initializers/0_new_framework_defaults_6_1.rb new file mode 100644 index 000000000..9526b835a --- /dev/null +++ b/config/initializers/0_new_framework_defaults_6_1.rb @@ -0,0 +1,67 @@ +# Be sure to restart your server when you modify this file. +# +# This file contains migration options to ease your Rails 6.1 upgrade. +# +# Once upgraded flip defaults one by one to migrate to the new default. +# +# Read the Guide for Upgrading Ruby on Rails for more info on each option. + +# Support for inversing belongs_to -> has_many Active Record associations. +# Rails.application.config.active_record.has_many_inversing = true + +# Track Active Storage variants in the database. +# Rails.application.config.active_storage.track_variants = true + +# Apply random variation to the delay when retrying failed jobs. +# Rails.application.config.active_job.retry_jitter = 0.15 + +# Stop executing `after_enqueue`/`after_perform` callbacks if +# `before_enqueue`/`before_perform` respectively halts with `throw :abort`. +# Rails.application.config.active_job.skip_after_callbacks_if_terminated = true + +# Specify cookies SameSite protection level: either :none, :lax, or :strict. +# +# This change is not backwards compatible with earlier Rails versions. +# It's best enabled when your entire app is migrated and stable on 6.1. +# Rails.application.config.action_dispatch.cookies_same_site_protection = :lax + +# Generate CSRF tokens that are encoded in URL-safe Base64. +# +# This change is not backwards compatible with earlier Rails versions. +# It's best enabled when your entire app is migrated and stable on 6.1. +# Rails.application.config.action_controller.urlsafe_csrf_tokens = true + +# Specify whether `ActiveSupport::TimeZone.utc_to_local` returns a time with an +# UTC offset or a UTC time. +# ActiveSupport.utc_to_local_returns_utc_offset_times = true + +# Change the default HTTP status code to `308` when redirecting non-GET/HEAD +# requests to HTTPS in `ActionDispatch::SSL` middleware. +# Rails.application.config.action_dispatch.ssl_default_redirect_status = 308 + +# Use new connection handling API. For most applications this won't have any +# effect. For applications using multiple databases, this new API provides +# support for granular connection swapping. +# Rails.application.config.active_record.legacy_connection_handling = false + +# Make `form_with` generate non-remote forms by default. +# Rails.application.config.action_view.form_with_generates_remote_forms = false + +# Set the default queue name for the analysis job to the queue adapter default. +# Rails.application.config.active_storage.queues.analysis = nil + +# Set the default queue name for the purge job to the queue adapter default. +# Rails.application.config.active_storage.queues.purge = nil + +# Set the default queue name for the incineration job to the queue adapter default. +# Rails.application.config.action_mailbox.queues.incineration = nil + +# Set the default queue name for the routing job to the queue adapter default. +# Rails.application.config.action_mailbox.queues.routing = nil + +# Set the default queue name for the mail deliver job to the queue adapter default. +# Rails.application.config.action_mailer.deliver_later_queue_name = nil + +# Generate a `Link` header that gives a hint to modern browsers about +# preloading assets when using `javascript_include_tag` and `stylesheet_link_tag`. +# Rails.application.config.action_view.preload_links_header = true diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb index 59385cdf3..33699c309 100644 --- a/config/initializers/backtrace_silencers.rb +++ b/config/initializers/backtrace_silencers.rb @@ -1,7 +1,8 @@ # Be sure to restart your server when you modify this file. # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. -# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } +# Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) } -# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. -# Rails.backtrace_cleaner.remove_silencers! +# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code +# by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'". +Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"] diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb index 4a994e1e7..4b34a0366 100644 --- a/config/initializers/filter_parameter_logging.rb +++ b/config/initializers/filter_parameter_logging.rb @@ -1,4 +1,6 @@ # Be sure to restart your server when you modify this file. # Configure sensitive parameters which will be filtered from the log file. -Rails.application.config.filter_parameters += [:password] +Rails.application.config.filter_parameters += [ + :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn +] diff --git a/config/initializers/permissions_policy.rb b/config/initializers/permissions_policy.rb new file mode 100644 index 000000000..00f64d71b --- /dev/null +++ b/config/initializers/permissions_policy.rb @@ -0,0 +1,11 @@ +# Define an application-wide HTTP permissions policy. For further +# information see https://developers.google.com/web/updates/2018/06/feature-policy +# +# Rails.application.config.permissions_policy do |f| +# f.camera :none +# f.gyroscope :none +# f.microphone :none +# f.usb :none +# f.fullscreen :self +# f.payment :self, "https://secure.example.com" +# end diff --git a/config/puma.rb b/config/puma.rb index 5ed443774..d9b3e836c 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -8,9 +8,14 @@ min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } threads min_threads_count, max_threads_count +# Specifies the `worker_timeout` threshold that Puma will use to wait before +# terminating a worker in development environments. +# +worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" + # Specifies the `port` that Puma will listen on to receive requests; default is 3000. # -port ENV.fetch("PORT") { 3000 } +port ENV.fetch("PORT") { 3000 } # Specifies the `environment` that Puma will run in. # From a191f331e0bbf8295c9e6a1b865b3b4480ec648e Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 2 Feb 2024 10:44:28 +0000 Subject: [PATCH 145/241] Upgrade gems for Rails 6.1 --- Gemfile | 8 ++++---- Gemfile.lock | 36 +++++++++++++++++------------------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/Gemfile b/Gemfile index d9221351c..9b31257ff 100644 --- a/Gemfile +++ b/Gemfile @@ -31,9 +31,9 @@ gem 'nested-hstore', '~> 0.1.2' gem 'pg_search', '~> 2.3', '>= 2.3.6' gem 'oj', '~> 3.16', '>= 3.16.3' # optimised JSON (picked by multi_json) gem 'inherited_resources', '~> 1.14' # Deprecated (https://github.com/activeadmin/inherited_resources#notice) -gem 'nokogiri', '1.15.5' # TODO: New version need Ruby 3 +gem 'nokogiri', '~> 1.16' gem 'mobility', '~> 1.2', '>= 1.2.9' -gem 'devise', '4.7.3' # TODO: need upgrade to 4.8+ when upgrade to rails 6.1 +gem 'devise', '~> 4.9', '>= 4.9.3' gem 'cancancan', '~> 3.5' gem 'ahoy_matey', '4.2.1' # TODO: latest 5.0.2. Can't upgrade to 5.0 until upgrade to Rails 6.1 gem 'uuidtools', '~> 2.2' # For Ahoy. (https://github.com/ankane/ahoy/blob/v2.2.1/docs/Ahoy-2-Upgrade.md#activerecordstore) @@ -43,7 +43,7 @@ gem 'uuidtools', '~> 2.2' # For Ahoy. (https://github.com/ankane/ahoy/blob/v2.2. # rspec ./spec/controllers/admin/nomenclature_changes/split_controller_spec.rb:191 gem 'wicked', '1.3.4' -gem 'groupdate', '6.2.1' # TODO: can upgrade after rails 6.1 and newer ruby 3 +gem 'groupdate', '~> 6.4' gem 'rubyzip', '~> 2.3', '>= 2.3.2' gem 'responders', '~> 3.1', '>= 3.1.1' # https://guides.rubyonrails.org/v4.2/upgrading_ruby_on_rails.html#responders @@ -121,7 +121,7 @@ group :development do end group :test, :development do - gem "rspec-rails", '5.1.2' # TODO: should upgrade once to rails 6.1 + gem 'rspec-rails', '~> 6.1', '>= 6.1.1' gem 'rspec-collection_matchers', '~> 1.2', '>= 1.2.1' gem 'json_spec', '~> 1.1', '>= 1.1.5' gem 'database_cleaner', '~> 2.0', '>= 2.0.2' diff --git a/Gemfile.lock b/Gemfile.lock index 201596d00..e7779cdf4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -187,7 +187,7 @@ GEM database_cleaner-core (2.0.1) date (3.3.4) device_detector (1.1.2) - devise (4.7.3) + devise (4.9.3) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) @@ -220,7 +220,6 @@ GEM railties (>= 4.2) ember-source (1.8.0) handlebars-source (~> 1.0) - errbase (0.2.2) erubi (1.12.0) et-orbi (1.2.7) tzinfo @@ -242,8 +241,8 @@ GEM i18n (>= 0.7) multi_json request_store (>= 1.0) - groupdate (6.2.1) - activesupport (>= 5.2) + groupdate (6.4.0) + activesupport (>= 6.1) handlebars-source (1.0.12) has_scope (0.8.2) actionpack (>= 5.2) @@ -331,7 +330,7 @@ GEM net-protocol net-ssh (7.2.1) nio4r (2.7.0) - nokogiri (1.15.5) + nokogiri (1.16.0) mini_portile2 (~> 2.8.2) racc (~> 1.4) numerizer (0.1.1) @@ -438,14 +437,14 @@ GEM rspec-mocks (3.12.6) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-rails (5.1.2) - actionpack (>= 5.2) - activesupport (>= 5.2) - railties (>= 5.2) - rspec-core (~> 3.10) - rspec-expectations (~> 3.10) - rspec-mocks (~> 3.10) - rspec-support (~> 3.10) + rspec-rails (6.1.1) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.12) + rspec-expectations (~> 3.12) + rspec-mocks (~> 3.12) + rspec-support (~> 3.12) rspec-support (3.12.1) rubocop (1.60.2) json (~> 2.3) @@ -478,8 +477,7 @@ GEM ruby-vips (2.2.0) ffi (~> 1.12) rubyzip (2.3.2) - safely_block (0.3.0) - errbase (>= 0.1.1) + safely_block (0.4.0) sass (3.4.25) sass-rails (6.0.0) sassc-rails (~> 2.1, >= 2.1.1) @@ -614,7 +612,7 @@ DEPENDENCIES coveralls_reborn (~> 0.28.0) dalli (= 2.7.10) database_cleaner (~> 2.0, >= 2.0.2) - devise (= 4.7.3) + devise (~> 4.9, >= 4.9.3) dotenv-rails (= 2.0.1) ed25519 (= 1.2.4) ember-data-source (= 1.13.0) @@ -623,7 +621,7 @@ DEPENDENCIES factory_bot_rails (= 5.2.0) geoip (= 1.3.5) gon (~> 6.4) - groupdate (= 6.2.1) + groupdate (~> 6.4) handlebars-source (= 1.0.12) httparty (~> 0.21.0) inherited_resources (~> 1.14) @@ -635,7 +633,7 @@ DEPENDENCIES mobility (~> 1.2, >= 1.2.9) nested-hstore (~> 0.1.2) nested_form (~> 0.3.2) - nokogiri (= 1.15.5) + nokogiri (~> 1.16) oj (~> 3.16, >= 3.16.3) paper_trail (= 12.3.0) pdfkit (~> 0.8.7.3) @@ -654,7 +652,7 @@ DEPENDENCIES request_store (~> 1.5, >= 1.5.1) responders (~> 3.1, >= 3.1.1) rspec-collection_matchers (~> 1.2, >= 1.2.1) - rspec-rails (= 5.1.2) + rspec-rails (~> 6.1, >= 6.1.1) rubocop-rails rubocop-rspec rubyzip (~> 2.3, >= 2.3.2) From 68d4f49b70b7050636d10406959235f9e3433b8d Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 2 Feb 2024 11:08:35 +0000 Subject: [PATCH 146/241] fix rspec --- lib/csv_column_headers_validator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/csv_column_headers_validator.rb b/lib/csv_column_headers_validator.rb index 77cc783bf..ce8f0303e 100644 --- a/lib/csv_column_headers_validator.rb +++ b/lib/csv_column_headers_validator.rb @@ -28,7 +28,7 @@ def validate_each(record, attribute, value) end rescue => e Rails.logger.error e.inspect - record.errors.add(attribute, "file cannot be processed", {}) + record.errors.add(attribute, "file cannot be processed") end end end From 7b1d8caa7fa4870d75d30ae56160568bf3eb1ba4 Mon Sep 17 00:00:00 2001 From: Daniel Perrett Date: Fri, 12 Jan 2024 17:42:36 +0000 Subject: [PATCH 147/241] chore: begin to test content of taxon_concepts API route --- .../api/taxon_concepts_controller_spec.rb | 90 +++++++++++++++++++ spec/factories.rb | 7 ++ 2 files changed, 97 insertions(+) diff --git a/spec/controllers/api/taxon_concepts_controller_spec.rb b/spec/controllers/api/taxon_concepts_controller_spec.rb index 361a7832a..28aecdec7 100644 --- a/spec/controllers/api/taxon_concepts_controller_spec.rb +++ b/spec/controllers/api/taxon_concepts_controller_spec.rb @@ -14,4 +14,94 @@ expect(@ahoy_event1).to eq(@ahoy_event2) end end + + context "GET show" do + context "Minimal taxon" do + let!(:taxon_concept) { + create(:taxon_concept) + } + let!(:m_taxon_concept) { + taxon_concept.m_taxon_concept + } + + it "Serialises a minimal taxon correctly" do + get :show, params: { :id => taxon_concept.id } + + response_body = parse_json(response.body) + + # Make sure we have the correct taxon + expect( + response_body['taxon_concept']['full_name'] + ).to eq( + taxon_concept.full_name + ) + + # We expect the response to be a superset of the following: + expected = { + "id"=>taxon_concept.id, + "parent_id"=>taxon_concept.parent_id, + "full_name"=>taxon_concept.full_name, + "author_year"=>nil, + "standard_references"=>[], + "common_names"=>[], + "distributions"=>[], + "subspecies"=>[], + "distribution_references"=>[], + "name_status"=>"A", + "nomenclature_note_en"=>nil, + "nomenclature_notification"=>false, + "cites_listing"=>nil, + "eu_listing"=>nil, + "accepted_names"=>[], + "synonyms"=>[], + "references"=>[], + "cites_quotas"=>[], + "cites_suspensions"=>[], + "cites_listings"=>[], + "eu_listings"=>[], + "eu_decisions"=>[], + "cites_processes"=>[] + } + + expect( + response_body['taxon_concept'].slice(*(expected.keys)) + ).to eq(expected) + end + end + + context "Taxon with CITES Processes" do + let!(:taxon_concept) { + create( + :taxon_concept, + ) + } + + let!(:cites_rst_process) { + create( + :cites_rst_process, + taxon_concept: taxon_concept + ) + } + + it "Serialises a minimal taxon correctly" do + get :show, params: { :id => taxon_concept.id } + + response_body = parse_json(response.body) + + # Make sure we have the correct taxon + expect( + response_body['taxon_concept']['full_name'] + ).to eq( + taxon_concept.full_name + ) + + # Check that CITES RST processes are included in the response + expect( + response_body['taxon_concept']['cites_processes'].length + ).to eq( + 1 + ) + end + end + end end diff --git a/spec/factories.rb b/spec/factories.rb index 0060c9fff..c4807d486 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -57,6 +57,13 @@ sequence(:scientific_name) { |n| "Lupus#{n}" } end + factory :cites_rst_process do + taxon_concept + geo_entity + start_date { '2011-01-01' } + status { 'Trade Suspension' } + end + factory :cites_suspension do taxon_concept start_notification From 60c8c96e68006774fd978bb16bbb10ae0af802fa Mon Sep 17 00:00:00 2001 From: Daniel Perrett Date: Fri, 2 Feb 2024 13:42:35 +0000 Subject: [PATCH 148/241] fix: order() expects literals to be wrapped in Arel.sql --- .../show_taxon_concept_serializer_cites.rb | 71 +++++++++++-------- 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/app/serializers/species/show_taxon_concept_serializer_cites.rb b/app/serializers/species/show_taxon_concept_serializer_cites.rb index 4da10c10a..323eeffc3 100644 --- a/app/serializers/species/show_taxon_concept_serializer_cites.rb +++ b/app/serializers/species/show_taxon_concept_serializer_cites.rb @@ -76,12 +76,14 @@ def quotas '[Quota for ' || (taxon_concept->>'rank')::TEXT || ' ' || (taxon_concept->>'full_name')::TEXT || ']' END AS subspecies_info SQL - ). - order(<<-SQL - trade_restrictions.start_date DESC, - geo_entity_en->>'name' ASC, trade_restrictions.notes ASC, - subspecies_info DESC - SQL + ).order( + Arel.sql( + <<-SQL + trade_restrictions.start_date DESC, + geo_entity_en->>'name' ASC, trade_restrictions.notes ASC, + subspecies_info DESC + SQL + ) ).all end @@ -122,12 +124,14 @@ def cites_suspensions '[Suspension for ' || (taxon_concept->>'rank')::TEXT || ' ' || (taxon_concept->>'full_name')::TEXT || ']' END AS subspecies_info SQL - ). - order(<<-SQL - trade_restrictions.is_current DESC, - trade_restrictions.start_date DESC, geo_entity_en->>'name' ASC, - subspecies_info DESC - SQL + ).order( + Arel.sql( + <<-SQL + trade_restrictions.is_current DESC, + trade_restrictions.start_date DESC, geo_entity_en->>'name' ASC, + subspecies_info DESC + SQL + ) ).all end @@ -147,11 +151,14 @@ def eu_decisions ", object_and_children, ancestors, object.id, ancestors_field). select(eu_decision_select_attrs). joins('LEFT JOIN eu_suspensions_applicability_view v ON eu_decisions.id = v.id'). - order(<<-SQL + order( + Arel.sql( + <<-SQL geo_entity_en->>'name' ASC, start_date DESC, subspecies_info DESC - SQL + SQL + ) ).all end @@ -245,14 +252,16 @@ def cites_listing_changes ELSE NULL END AS subspecies_info SQL - ). - order(<<-SQL - effective_at DESC, - change_type_order ASC, - species_listing_name ASC, - subspecies_info DESC, - party_full_name_en ASC - SQL + ).order( + Arel.sql( + <<-SQL + effective_at DESC, + change_type_order ASC, + species_listing_name ASC, + subspecies_info DESC, + party_full_name_en ASC + SQL + ) ).all end @@ -311,14 +320,16 @@ def eu_listing_changes ELSE NULL END AS subspecies_info SQL - ). - order(<<-SQL - effective_at DESC, - change_type_order ASC, - species_listing_name ASC, - subspecies_info DESC, - party_full_name_en ASC - SQL + ).order( + Arel.sql( + <<-SQL + effective_at DESC, + change_type_order ASC, + species_listing_name ASC, + subspecies_info DESC, + party_full_name_en ASC + SQL + ) ).all end From e15dc9b937b21b59a78d11ece908cabcc3c6843a Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 2 Feb 2024 13:56:26 +0000 Subject: [PATCH 149/241] DEPRECATION WARNING: Enumerating ActiveModel::Errors as a hash has been deprecated. --- app/models/nomenclature_change/output.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/models/nomenclature_change/output.rb b/app/models/nomenclature_change/output.rb index 0bbd1ef49..febb1e042 100644 --- a/app/models/nomenclature_change/output.rb +++ b/app/models/nomenclature_change/output.rb @@ -183,9 +183,11 @@ def validate_tmp_taxon_concept unless @tmp_taxon_concept errors.add(:new_taxon_concept, "can\'t be blank") end - return true if @tmp_taxon_concept.valid? - @tmp_taxon_concept.errors.each do |attribute, message| + + @tmp_taxon_concept.errors.each do |error| + attribute = error.attribute + message = error.message if [:parent_id, :rank, :name_status, :author_year, :full_name]. include?(attribute) errors.add(:"new_#{attribute}", message) From 4ede79584176f49b5bc1cf12747af9edf1936342 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 2 Feb 2024 13:56:47 +0000 Subject: [PATCH 150/241] DEPRECATION WARNING: Calling `<<` to an ActiveModel::Errors message array in order to add an error is deprecated. Please call `ActiveModel::Errors#add` instead. --- app/models/nomenclature_change.rb | 2 +- app/services/trade/sandbox.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/nomenclature_change.rb b/app/models/nomenclature_change.rb index e61ecd79a..2335d2e98 100644 --- a/app/models/nomenclature_change.rb +++ b/app/models/nomenclature_change.rb @@ -48,7 +48,7 @@ def cannot_update_when_locked if status_was == NomenclatureChange::CLOSED || status_was == NomenclatureChange::SUBMITTED && status != NomenclatureChange::CLOSED - errors[:base] << "Nomenclature change is locked for updates" + errors.add(:base, "Nomenclature change is locked for updates") return false end end diff --git a/app/services/trade/sandbox.rb b/app/services/trade/sandbox.rb index 14178b730..f74a47252 100644 --- a/app/services/trade/sandbox.rb +++ b/app/services/trade/sandbox.rb @@ -28,7 +28,7 @@ def copy_from_sandbox_to_shipments(submitter) @moved_rows_cnt = pg_result.first['copy_transactions_from_sandbox_to_shipments'].to_i if @moved_rows_cnt < 0 # if -1 returned, not all rows have been moved - @annual_report_upload.errors[:base] << "Submit failed, could not save all rows." + @annual_report_upload.errors.add(:base, "Submit failed, could not save all rows.") success = false raise ActiveRecord::Rollback end From ccb968c7a7ab69e337bc559eb2aed807ca0ae110 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 2 Feb 2024 14:25:43 +0000 Subject: [PATCH 151/241] DEPRECATION WARNING: connection_config is deprecated and will be removed from Rails 7.0 (Use connection_db_config instead) --- lib/psql_command.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/psql_command.rb b/lib/psql_command.rb index df40d77d9..3e06cd9c6 100644 --- a/lib/psql_command.rb +++ b/lib/psql_command.rb @@ -1,7 +1,7 @@ class PsqlCommand def initialize(sql_cmd) - db_conf = ApplicationRecord.connection_config + db_conf = ApplicationRecord.connection_db_config.configuration_hash @host = db_conf[:host] || 'localhost' @port = db_conf[:port] || 5432 @username = db_conf[:username] From 8110f42a7927b89d65441ce65639e97a4535839e Mon Sep 17 00:00:00 2001 From: Daniel Perrett Date: Fri, 2 Feb 2024 15:25:09 +0000 Subject: [PATCH 152/241] fix: wrap another two order clauses in an Arel.sql --- .../admin/distributions_controller.rb | 7 +++-- app/models/m_taxon_concept.rb | 28 +++++++++++-------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/app/controllers/admin/distributions_controller.rb b/app/controllers/admin/distributions_controller.rb index 4886cb23d..299f4208f 100644 --- a/app/controllers/admin/distributions_controller.rb +++ b/app/controllers/admin/distributions_controller.rb @@ -5,8 +5,11 @@ class Admin::DistributionsController < Admin::TaxonConceptAssociatedTypesControl before_action :load_search, :only => [:index] def index - @distributions = @taxon_concept.distributions. - joins(:geo_entity).order('UPPER(geo_entities.name_en) ASC') + @distributions = @taxon_concept.distributions.joins( + :geo_entity + ).order( + Arel.sql('UPPER(geo_entities.name_en) ASC') + ) end def edit diff --git a/app/models/m_taxon_concept.rb b/app/models/m_taxon_concept.rb index 56b8035f4..5a4ce3642 100644 --- a/app/models/m_taxon_concept.rb +++ b/app/models/m_taxon_concept.rb @@ -111,17 +111,23 @@ class MTaxonConcept < ApplicationRecord belongs_to :taxon_concept, :foreign_key => :id, optional: true has_many :cites_listing_changes, :foreign_key => :taxon_concept_id, :class_name => 'MCitesListingChange' - has_many :historic_cites_listing_changes_for_downloads, -> { where(show_in_downloads: true).order( - <<-SQL - effective_at, - CASE - WHEN change_type_name = 'ADDITION' THEN 0 - WHEN change_type_name = 'RESERVATION' THEN 1 - WHEN change_type_name = 'RESERVATION_WITHDRAWAL' THEN 2 - WHEN change_type_name = 'DELETION' THEN 3 - END - SQL - ) }, :foreign_key => :taxon_concept_id, + has_many :historic_cites_listing_changes_for_downloads, -> { + where( + show_in_downloads: true + ).order( + Arel.sql( + <<-SQL + effective_at, + CASE + WHEN change_type_name = 'ADDITION' THEN 0 + WHEN change_type_name = 'RESERVATION' THEN 1 + WHEN change_type_name = 'RESERVATION_WITHDRAWAL' THEN 2 + WHEN change_type_name = 'DELETION' THEN 3 + END + SQL + ) + ) + }, :foreign_key => :taxon_concept_id, :class_name => 'MCitesListingChange' has_many :current_cites_additions, -> { where(is_current: true, change_type_name: ChangeType::ADDITION).order('effective_at DESC, species_listing_name ASC') }, :foreign_key => :taxon_concept_id, From f934375f5ed36e081b7083e1a0301e09ec38c593 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 2 Feb 2024 22:52:04 +0000 Subject: [PATCH 153/241] Upgrade Gems and add comments to gemfile --- Gemfile | 10 ++++------ Gemfile.lock | 27 ++++++++++++--------------- config/environments/development.rb | 9 --------- config/initializers/ahoy.rb | 2 +- db/migrate/20240202224209_ahoy_5.rb | 9 +++++++++ 5 files changed, 26 insertions(+), 31 deletions(-) create mode 100644 db/migrate/20240202224209_ahoy_5.rb diff --git a/Gemfile b/Gemfile index 9b31257ff..4edee0214 100644 --- a/Gemfile +++ b/Gemfile @@ -35,7 +35,7 @@ gem 'nokogiri', '~> 1.16' gem 'mobility', '~> 1.2', '>= 1.2.9' gem 'devise', '~> 4.9', '>= 4.9.3' gem 'cancancan', '~> 3.5' -gem 'ahoy_matey', '4.2.1' # TODO: latest 5.0.2. Can't upgrade to 5.0 until upgrade to Rails 6.1 +gem 'ahoy_matey', '~> 5.0', '>= 5.0.2' gem 'uuidtools', '~> 2.2' # For Ahoy. (https://github.com/ankane/ahoy/blob/v2.2.1/docs/Ahoy-2-Upgrade.md#activerecordstore) # TODO: starting from v1.4, it break our test due to redirection changes: @@ -48,7 +48,7 @@ gem 'groupdate', '~> 6.4' gem 'rubyzip', '~> 2.3', '>= 2.3.2' gem 'responders', '~> 3.1', '>= 3.1.1' # https://guides.rubyonrails.org/v4.2/upgrading_ruby_on_rails.html#responders -gem 'sidekiq', '< 7' # TODO, latest is 7 +gem 'sidekiq', '< 7' # TODO, latest is 7, which required Redis 6.2+, but our servers running Redis 4.0.9. gem 'sidekiq-status', '~> 3.0', '>= 3.0.3' gem 'sidekiq-unique-jobs', '7.1.31' # TODO: can upgrade to latest when sidekiq upgrade to 7 gem 'sidekiq-cron', '~> 1.12' @@ -57,8 +57,8 @@ gem 'httparty', '~> 0.21.0' gem 'kaminari', '~> 1.2', '>= 1.2.2' # TODO: Suggest migrate to pagy gem. -gem 'acts-as-taggable-on', '9.0.1' # TODO: latest v10 @ 2023. Can upgrade after upgrade to Rails 6.1 -gem 'carrierwave', '2.2.5' # TODO: latest is 3.0.5 @ 2023. can upgrade to v3 after Rails 6 +gem 'acts-as-taggable-on', '~> 10.0' +gem 'carrierwave', '~> 3.0', '>= 3.0.5' # PDF gem 'prawn', '0.13.2' @@ -108,9 +108,7 @@ group :development do gem 'capistrano-sidekiq', '~> 2.3', '>= 2.3.1' gem 'slackistrano', '0.1.9', require: false gem 'brightbox', '2.3.9' - gem 'rack-cors', '0.3.0' ,:require => 'rack/cors' # TODO: remove when upgrade Rails. gem 'jslint_on_rails', '1.1.1' - # gem 'rubocop', '0.40.0', require: false gem 'rubocop-rails' gem 'rubocop-rspec' gem 'rbnacl', '4.0.2' diff --git a/Gemfile.lock b/Gemfile.lock index e7779cdf4..68b634e8e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -69,15 +69,15 @@ GEM minitest (>= 5.1) tzinfo (~> 2.0) zeitwerk (~> 2.3) - acts-as-taggable-on (9.0.1) - activerecord (>= 6.0, < 7.1) + acts-as-taggable-on (10.0.0) + activerecord (>= 6.1, < 7.2) addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) afm (0.2.2) - ahoy_matey (4.2.1) - activesupport (>= 5.2) - device_detector - safely_block (>= 0.2.1) + ahoy_matey (5.0.2) + activesupport (>= 6.1) + device_detector (>= 1) + safely_block (>= 0.4) airbrussh (1.5.1) sshkit (>= 1.6.1, != 1.7.0) annotate (2.5.0) @@ -150,13 +150,12 @@ GEM rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) - carrierwave (2.2.5) - activemodel (>= 5.0.0) - activesupport (>= 5.0.0) + carrierwave (3.0.5) + activemodel (>= 6.0.0) + activesupport (>= 6.0.0) addressable (~> 2.6) image_processing (~> 1.1) marcel (~> 1.0.0) - mini_mime (>= 0.1.3) ssrf_filter (~> 1.0) chartkick (5.0.5) chronic_duration (0.10.6) @@ -367,7 +366,6 @@ GEM raabro (1.4.0) racc (1.7.3) rack (2.2.8) - rack-cors (0.3.0) rack-mini-profiler (2.3.4) rack (>= 1.2.0) rack-test (2.1.0) @@ -585,8 +583,8 @@ DEPENDENCIES actionpack-action_caching (~> 1.2, >= 1.2.2) actionpack-page_caching (~> 1.2, >= 1.2.4) active_model_serializers (= 0.8.4) - acts-as-taggable-on (= 9.0.1) - ahoy_matey (= 4.2.1) + acts-as-taggable-on (~> 10.0) + ahoy_matey (~> 5.0, >= 5.0.2) annotate (= 2.5.0) appsignal (~> 3.5, >= 3.5.5) aws-sdk (~> 2) @@ -605,7 +603,7 @@ DEPENDENCIES capistrano-rvm (= 0.1.2) capistrano-sidekiq (~> 2.3, >= 2.3.1) capybara (>= 3.26) - carrierwave (= 2.2.5) + carrierwave (~> 3.0, >= 3.0.5) chartkick (~> 5.0, >= 5.0.5) codeclimate-test-reporter (= 0.1.1) coffee-rails (~> 5.0) @@ -642,7 +640,6 @@ DEPENDENCIES pg_search (~> 2.3, >= 2.3.6) prawn (= 0.13.2) puma (~> 5.0) - rack-cors (= 0.3.0) rack-mini-profiler (~> 2.0) rails (= 6.1.7.6) rails-controller-testing diff --git a/config/environments/development.rb b/config/environments/development.rb index bf5152379..9bf634ce9 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -83,13 +83,4 @@ config.action_mailer.default_url_options = { host: Rails.application.secrets.mailer[:host] || 'http://localhost:3000' } - - # CORS settings - # TODO: Rails 5 should build-in, need change this part when upgrade. - config.middleware.use Rack::Cors do - allow do - origins '*' - resource '*', :headers => :any, :methods => [:get, :post, :delete, :put, :options, :head] - end - end end diff --git a/config/initializers/ahoy.rb b/config/initializers/ahoy.rb index ebac64204..dd54da01c 100644 --- a/config/initializers/ahoy.rb +++ b/config/initializers/ahoy.rb @@ -51,7 +51,7 @@ def ensure_uuid(id) Ahoy.track_bots = Rails.env.test? Ahoy.geocode = false # we use our own geocoder (Sapi::GeoIP) Ahoy.mask_ips = true -Ahoy.cookies = false # TODO: when upgrade to Ahoy v5, change value to :none +Ahoy.cookies = :none # https://github.com/ankane/ahoy/tree/v2.2.1#exceptions Safely.report_exception_method = ->(e) { Appsignal.add_exception(exception) if defined? Appsignal } diff --git a/db/migrate/20240202224209_ahoy_5.rb b/db/migrate/20240202224209_ahoy_5.rb new file mode 100644 index 000000000..94d63435a --- /dev/null +++ b/db/migrate/20240202224209_ahoy_5.rb @@ -0,0 +1,9 @@ +class Ahoy5 < ActiveRecord::Migration[6.1] + disable_ddl_transaction! + + def change + # https://github.com/ankane/ahoy/tree/v5.0.2?tab=readme-ov-file#50 + # before v1.4.0, they named it `visitor_id` instead of `visitor_token`. + add_index :ahoy_visits, [:visitor_id, :started_at], algorithm: :concurrently + end +end From 8f8bdea2ba6e07d059bb3b000436b1985ab50c07 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Sat, 3 Feb 2024 22:41:40 +0000 Subject: [PATCH 154/241] fix ahoy config --- config/initializers/ahoy.rb | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/config/initializers/ahoy.rb b/config/initializers/ahoy.rb index dd54da01c..e82c8e65d 100644 --- a/config/initializers/ahoy.rb +++ b/config/initializers/ahoy.rb @@ -4,7 +4,8 @@ class Ahoy::Store < Ahoy::DatabaseStore UUID_NAMESPACE = UUIDTools::UUID.parse("dcd74c26-8fc9-453a-a9c2-afc445c3258d") def authenticate(data) - # disables automatic linking of visits and users + # https://github.com/ankane/ahoy/tree/v5.0.2?tab=readme-ov-file#gdpr-compliance-1 + # disables automatic linking of visits and users (for GDPR compliance) end def visit_model @@ -15,11 +16,31 @@ def event_model Ahoy::Event end + # https://github.com/ankane/ahoy/issues/549 + # This project start using ahoy since version 1.0.1 + # The DB migration file come with version 1.0.1 create columns `id` and `visitor_id`. + # (https://github.com/ankane/ahoy/blob/v1.0.1/lib/generators/ahoy/stores/templates/active_record_visits_migration.rb) + # However it has changed since version 1.4.0, from `id` to `visit_token`, and from `visitor_id` to `visitor_token`. + # (https://github.com/ankane/ahoy/blob/v1.4.0/lib/generators/ahoy/stores/templates/active_record_visits_migration.rb) + # When we upgrade this gem, we need to check the source code of Ahoy::DatabaseStore, + # (5.0.2 as reference: https://github.com/ankane/ahoy/blob/v5.0.2/lib/ahoy/database_store.rb) + # see how the latest `visit` method look like, and override it with the old column names. def visit - @visit ||= visit_model.find_by(id: ensure_uuid(ahoy.visit_token)) if ahoy.visit_token + unless defined?(@visit) + if ahoy.send(:existing_visit_token) || ahoy.instance_variable_get(:@visit_token) + # find_by raises error by default with Mongoid when not found + @visit = visit_model.where(id: ensure_uuid(ahoy.visit_token)).take if ahoy.visit_token + elsif !Ahoy.cookies? && ahoy.visitor_token + @visit = visit_model.where(visitor_id: ensure_uuid(ahoy.visitor_token)).where(started_at: Ahoy.visit_duration.ago..).order(started_at: :desc).first + else + @visit = nil + end + end + @visit end def track_visit(data) + # Map the new column names (since 1.4.0), to old column name (< 1.4.0). data[:id] = ensure_uuid(data.delete(:visit_token)) data[:visitor_id] = ensure_uuid(data.delete(:visitor_token)) @@ -32,6 +53,7 @@ def track_visit(data) end def track_event(data) + # Map the new column names (since 1.4.0), to old column name (< 1.4.0). data[:id] = ensure_uuid(data.delete(:event_id)) super(data) end From 9903b217f1ecdab657a0faaf3dce2b15afa2dbdb Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Sun, 4 Feb 2024 17:49:54 +0000 Subject: [PATCH 155/241] Remove rails-observers gem. --- Gemfile | 1 - Gemfile.lock | 3 - app/controllers/admin/purposes_controller.rb | 1 - app/controllers/admin/sources_controller.rb | 1 - app/controllers/admin/terms_controller.rb | 1 - app/controllers/admin/units_controller.rb | 1 - app/models/annotation.rb | 7 + app/models/annotation_observer.rb | 9 -- app/models/change_observer.rb | 90 ----------- app/models/change_type.rb | 1 + app/models/concerns/changable.rb | 151 ++++++++++++++++++ app/models/concerns/deletable.rb | 20 +++ app/models/designation.rb | 1 + app/models/destroy_observer.rb | 18 --- app/models/distribution.rb | 1 + app/models/document.rb | 41 +++++ app/models/document_observer.rb | 49 ------ app/models/eu_decision.rb | 9 ++ app/models/eu_decision_observer.rb | 7 - app/models/eu_decision_type.rb | 2 + app/models/events/cites_cop.rb | 6 + app/models/events/cites_cop_observer.rb | 8 - .../events/cites_suspension_notification.rb | 5 + .../cites_suspension_notification_observer.rb | 8 - app/models/events/eu_council_regulation.rb | 2 +- app/models/events/eu_event.rb | 6 + app/models/events/eu_event_observer.rb | 10 -- .../events/eu_implementing_regulation.rb | 2 +- app/models/events/eu_regulation.rb | 19 ++- app/models/events/eu_regulation_observer.rb | 19 --- app/models/events/eu_suspension_regulation.rb | 19 ++- .../eu_suspension_regulation_observer.rb | 17 -- app/models/geo_entity.rb | 7 + app/models/geo_entity_observer.rb | 10 -- app/models/instrument.rb | 1 + app/models/language.rb | 2 + app/models/listing_change.rb | 58 +++++++ app/models/listing_change_observer.rb | 58 ------- app/models/nomenclature_change.rb | 13 ++ app/models/nomenclature_change_observer.rb | 16 -- app/models/preset_tag.rb | 1 + app/models/rank.rb | 1 + app/models/reference.rb | 1 + app/models/species_listing.rb | 1 + app/models/taxon_common.rb | 1 + app/models/taxon_concept.rb | 80 ++++++++++ app/models/taxon_concept_observer.rb | 109 ------------- app/models/taxon_concept_reference.rb | 1 + app/models/taxon_instrument.rb | 1 + app/models/taxon_relationship.rb | 1 + app/models/taxonomy.rb | 1 + app/models/trade/annual_report_upload.rb | 6 + .../trade/annual_report_upload_observer.rb | 11 -- app/models/trade/shipment.rb | 29 ++++ app/models/trade/shipment_observer.rb | 33 ---- app/models/trade/trade_data_download.rb | 7 + .../trade/trade_data_download_observer.rb | 7 - app/models/trade_codes/purpose.rb | 20 +++ app/models/trade_codes/source.rb | 20 +++ app/models/trade_codes/term.rb | 20 +++ app/models/trade_codes/unit.rb | 20 +++ app/models/trade_restriction.rb | 74 +++++++++ app/models/trade_restriction_observer.rb | 88 ---------- .../trade_restrictions/cites_suspension.rb | 8 + .../cites_suspension_observer.rb | 7 - app/models/trade_restrictions/quota.rb | 9 ++ .../trade_restrictions/quota_observer.rb | 7 - app/models/user.rb | 2 + app/sweepers/purpose_sweeper.rb | 29 ---- app/sweepers/source_sweeper.rb | 29 ---- app/sweepers/term_sweeper.rb | 29 ---- app/sweepers/unit_sweeper.rb | 29 ---- config/application.rb | 11 -- 73 files changed, 671 insertions(+), 722 deletions(-) delete mode 100644 app/models/annotation_observer.rb delete mode 100644 app/models/change_observer.rb create mode 100644 app/models/concerns/changable.rb create mode 100644 app/models/concerns/deletable.rb delete mode 100644 app/models/destroy_observer.rb delete mode 100644 app/models/document_observer.rb delete mode 100644 app/models/eu_decision_observer.rb delete mode 100644 app/models/events/cites_cop_observer.rb delete mode 100644 app/models/events/cites_suspension_notification_observer.rb create mode 100644 app/models/events/eu_event.rb delete mode 100644 app/models/events/eu_event_observer.rb delete mode 100644 app/models/events/eu_regulation_observer.rb delete mode 100644 app/models/events/eu_suspension_regulation_observer.rb delete mode 100644 app/models/geo_entity_observer.rb delete mode 100644 app/models/listing_change_observer.rb delete mode 100644 app/models/nomenclature_change_observer.rb delete mode 100644 app/models/taxon_concept_observer.rb delete mode 100644 app/models/trade/annual_report_upload_observer.rb delete mode 100644 app/models/trade/shipment_observer.rb delete mode 100644 app/models/trade/trade_data_download_observer.rb delete mode 100644 app/models/trade_restriction_observer.rb delete mode 100644 app/models/trade_restrictions/cites_suspension_observer.rb delete mode 100644 app/models/trade_restrictions/quota_observer.rb delete mode 100644 app/sweepers/purpose_sweeper.rb delete mode 100644 app/sweepers/source_sweeper.rb delete mode 100644 app/sweepers/term_sweeper.rb delete mode 100644 app/sweepers/unit_sweeper.rb diff --git a/Gemfile b/Gemfile index 4edee0214..50108cb66 100644 --- a/Gemfile +++ b/Gemfile @@ -66,7 +66,6 @@ gem 'pdfkit', '~> 0.8.7.3' gem 'wkhtmltopdf-binary', '~> 0.12.6.6' gem 'aws-sdk', '~> 2' # TODO: v2 Deprecated, need to upgrade to v3 -gem 'rails-observers', '~> 0.1.5' # A feature that removed from core in Rails 4.0, maybe be better migrate away from this. # See https://github.com/sstephenson/execjs#readme for more supported runtimes # gem 'therubyracer', :platforms => :ruby diff --git a/Gemfile.lock b/Gemfile.lock index 68b634e8e..0cb6203d6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -396,8 +396,6 @@ GEM rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) - rails-observers (0.1.5) - activemodel (>= 4.0) railties (6.1.7.6) actionpack (= 6.1.7.6) activesupport (= 6.1.7.6) @@ -643,7 +641,6 @@ DEPENDENCIES rack-mini-profiler (~> 2.0) rails (= 6.1.7.6) rails-controller-testing - rails-observers (~> 0.1.5) rbnacl (= 4.0.2) rbnacl-libsodium (= 1.0.16) request_store (~> 1.5, >= 1.5.1) diff --git a/app/controllers/admin/purposes_controller.rb b/app/controllers/admin/purposes_controller.rb index a597f6025..ec6712fcf 100644 --- a/app/controllers/admin/purposes_controller.rb +++ b/app/controllers/admin/purposes_controller.rb @@ -1,6 +1,5 @@ class Admin::PurposesController < Admin::StandardAuthorizationController respond_to :json, :only => [:update] - cache_sweeper :purpose_sweeper def index index! do |format| diff --git a/app/controllers/admin/sources_controller.rb b/app/controllers/admin/sources_controller.rb index f8546d6a2..4fc87ba1c 100644 --- a/app/controllers/admin/sources_controller.rb +++ b/app/controllers/admin/sources_controller.rb @@ -1,6 +1,5 @@ class Admin::SourcesController < Admin::StandardAuthorizationController respond_to :json, :only => [:update] - cache_sweeper :source_sweeper def index index! do |format| diff --git a/app/controllers/admin/terms_controller.rb b/app/controllers/admin/terms_controller.rb index 16ec47534..96909bc95 100644 --- a/app/controllers/admin/terms_controller.rb +++ b/app/controllers/admin/terms_controller.rb @@ -1,6 +1,5 @@ class Admin::TermsController < Admin::StandardAuthorizationController respond_to :json, :only => [:update] - cache_sweeper :term_sweeper def index index! do |format| diff --git a/app/controllers/admin/units_controller.rb b/app/controllers/admin/units_controller.rb index ccf0a4690..d264c61e5 100644 --- a/app/controllers/admin/units_controller.rb +++ b/app/controllers/admin/units_controller.rb @@ -1,6 +1,5 @@ class Admin::UnitsController < Admin::StandardAuthorizationController respond_to :json, :only => [:update] - cache_sweeper :unit_sweeper def index index! do |format| diff --git a/app/models/annotation.rb b/app/models/annotation.rb index 6439e0205..159651181 100644 --- a/app/models/annotation.rb +++ b/app/models/annotation.rb @@ -23,6 +23,7 @@ # class Annotation < ApplicationRecord + include Deletable extend Mobility include TrackWhoDoesIt @@ -43,6 +44,12 @@ class Annotation < ApplicationRecord scope :for_eu, -> { joins(:event).where("events.type = 'EuRegulation'"). order([:parent_symbol, :symbol]) } + before_save do + if event + self.parent_symbol = event.name + end + end + def self.search(query) if query.present? where("UPPER(symbol) LIKE UPPER(:query) diff --git a/app/models/annotation_observer.rb b/app/models/annotation_observer.rb deleted file mode 100644 index 51eb32907..000000000 --- a/app/models/annotation_observer.rb +++ /dev/null @@ -1,9 +0,0 @@ -class AnnotationObserver < ActiveRecord::Observer - - def before_save(annotation) - if annotation.event - annotation.parent_symbol = annotation.event.name - end - end - -end diff --git a/app/models/change_observer.rb b/app/models/change_observer.rb deleted file mode 100644 index 63adb43fb..000000000 --- a/app/models/change_observer.rb +++ /dev/null @@ -1,90 +0,0 @@ -class ChangeObserver < ActiveRecord::Observer - observe :taxon_common, :distribution, :eu_decision, - :listing_change, :taxon_concept_reference, - :taxon_instrument, :taxon_relationship, :cites_suspension, :quota, - :geo_entity, :language - - def after_save(model) - clear_cache model - - # For models that are not directly related to taxon concepts - # but for which is anyway preferable for the changes to be reflacted - # immedtiately on the public interface, clear the entire serializer cache. - # Models like GeoEntity and Language are not directly linked to TaxonConcept, - # so bumping the dependents_updated_at timestamp seems a bit confusing. - # Also there's currently no easy way to tell who updated those objects. - # It's quite rare that updates to those objects will occur, - # so there shouldn't be no harm in clearning the entire serializer cache. - unless model.respond_to?(:taxon_concept) - clear_show_tc_serializer_cache - return - end - - if model.taxon_concept - bump_dependents_timestamp(model.taxon_concept, model.updated_by_id) - end - # Rails 5.1 to 5.2 - # DEPRECATION WARNING: The behavior of `attribute_was` inside of after callbacks will be changing in the next version of Rails. - # The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). - # To maintain the current behavior, use `attribute_before_last_save` instead. - # - # DEPRECATION WARNING: The behavior of `attribute_changed?` inside of after callbacks will be changing in the next version of Rails. - # The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). - # To maintain the current behavior, use `saved_change_to_attribute?` instead. - # - # DEPRECATION WARNING: The behavior of `changed_attributes` inside of after callbacks will be changing in the next version of Rails. - # The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). - # To maintain the current behavior, use `saved_changes.transform_values(&:first)` instead. - # - # == Original code == - # if model.taxon_concept.nil? && model.taxon_concept_id_was || - # model.taxon_concept && model.taxon_concept_id_was && model.taxon_concept_id != model.taxon_concept_id_was - # previous_taxon_concept = TaxonConcept.find_by_id(model.taxon_concept_id_was) - # if previous_taxon_concept - # bump_dependents_timestamp(previous_taxon_concept, model.updated_by_id) - # end - # end - # == Changed to fix deprecation warnings == - if model.taxon_concept.nil? && model.taxon_concept_id_before_last_save || - model.taxon_concept && model.taxon_concept_id_before_last_save && model.taxon_concept_id != model.taxon_concept_id_before_last_save - previous_taxon_concept = TaxonConcept.find_by_id(model.taxon_concept_id_before_last_save) - if previous_taxon_concept - bump_dependents_timestamp(previous_taxon_concept, model.updated_by_id) - end - end - end - - def before_destroy(model) - clear_cache model - if model.respond_to?(:taxon_concept) && model.taxon_concept && model.can_be_deleted? - # currently no easy means to tell who deleted the dependent object - bump_dependents_timestamp(model.taxon_concept, nil) - end - end - - protected - - def clear_cache(model) - return unless model.respond_to?(:taxon_concept) - DownloadsCacheCleanupWorker.perform_async(model.class.to_s.tableize) - end - - def clear_show_tc_serializer_cache - ## - # Disabling because we use memcache on production, but memcache doesn't implement this method. - # For now, changes to records that appear in serializers will not change until the caches are expired, - # which is 24 hours. Possible solution: - # https://unep-wcmc.codebasehq.com/projects/cites-support-maintenance/tickets/114 - - Rails.cache.delete_matched('*ShowTaxonConceptSerializer*') unless Rails.env.production? - end - - def bump_dependents_timestamp(taxon_concept, updated_by_id) - return unless taxon_concept - TaxonConcept.where(id: taxon_concept.id).update_all( - dependents_updated_at: Time.now, - dependents_updated_by_id: updated_by_id - ) - DownloadsCacheCleanupWorker.perform_async('taxon_concepts') - end -end diff --git a/app/models/change_type.rb b/app/models/change_type.rb index 28fb8c732..613c8d0fe 100644 --- a/app/models/change_type.rb +++ b/app/models/change_type.rb @@ -13,6 +13,7 @@ # class ChangeType < ApplicationRecord + include Deletable extend Mobility # Migrated to controller (Strong Parameters) # attr_accessible :name, :display_name_en, :display_name_es, :display_name_fr, diff --git a/app/models/concerns/changable.rb b/app/models/concerns/changable.rb new file mode 100644 index 000000000..40dad5b3f --- /dev/null +++ b/app/models/concerns/changable.rb @@ -0,0 +1,151 @@ +module Changable + extend ActiveSupport::Concern + + included do + ######################################################### + ### after save + after_save :changable_after_save_callback + after_commit :changable_after_save_callback_on_commit, on: [:create, :update] + ######################################################### + ### before destroy + before_destroy :changable_before_destroy_callback + after_commit :changable_before_destroy_callback_on_commit, on: :destroy + end + + private + + def changable_before_destroy_callback + if respond_to?(:taxon_concept) && taxon_concept && can_be_deleted? + # currently no easy means to tell who deleted the dependent object + changable_bump_dependents_timestamp_part_one(taxon_concept, nil) + end + end + + def changable_before_destroy_callback_on_commit + changable_clear_cache + if respond_to?(:taxon_concept) && taxon_concept && can_be_deleted? + # currently no easy means to tell who deleted the dependent object + changable_bump_dependents_timestamp_part_two + end + end + + def changable_after_save_callback + unless respond_to?(:taxon_concept) + return + end + + if taxon_concept + changable_bump_dependents_timestamp_part_one(taxon_concept, updated_by_id) + end + # Rails 5.1 to 5.2 + # DEPRECATION WARNING: The behavior of `attribute_was` inside of after callbacks will be changing in the next version of Rails. + # The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). + # To maintain the current behavior, use `attribute_before_last_save` instead. + # + # DEPRECATION WARNING: The behavior of `attribute_changed?` inside of after callbacks will be changing in the next version of Rails. + # The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). + # To maintain the current behavior, use `saved_change_to_attribute?` instead. + # + # DEPRECATION WARNING: The behavior of `changed_attributes` inside of after callbacks will be changing in the next version of Rails. + # The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). + # To maintain the current behavior, use `saved_changes.transform_values(&:first)` instead. + # + # == Original code == + # if taxon_concept.nil? && taxon_concept_id_was || + # taxon_concept && taxon_concept_id_was && taxon_concept_id != taxon_concept_id_was + # previous_taxon_concept = TaxonConcept.find_by_id(taxon_concept_id_was) + # if previous_taxon_concept + # bump_dependents_timestamp(previous_taxon_concept, updated_by_id) + # end + # end + # == Changed to fix deprecation warnings == + if taxon_concept.nil? && taxon_concept_id_before_last_save || + taxon_concept && taxon_concept_id_before_last_save && taxon_concept_id != taxon_concept_id_before_last_save + previous_taxon_concept = TaxonConcept.find_by_id(taxon_concept_id_before_last_save) + if previous_taxon_concept + changable_bump_dependents_timestamp_part_one(previous_taxon_concept, updated_by_id) + end + end + end + + def changable_after_save_callback_on_commit + changable_clear_cache + + # For models that are not directly related to taxon concepts + # but for which is anyway preferable for the changes to be reflacted + # immedtiately on the public interface, clear the entire serializer cache. + # Models like GeoEntity and Language are not directly linked to TaxonConcept, + # so bumping the dependents_updated_at timestamp seems a bit confusing. + # Also there's currently no easy way to tell who updated those objects. + # It's quite rare that updates to those objects will occur, + # so there shouldn't be no harm in clearning the entire serializer cache. + unless respond_to?(:taxon_concept) + changable_clear_show_tc_serializer_cache + return + end + + if taxon_concept + changable_bump_dependents_timestamp_part_two + end + # Rails 5.1 to 5.2 + # DEPRECATION WARNING: The behavior of `attribute_was` inside of after callbacks will be changing in the next version of Rails. + # The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). + # To maintain the current behavior, use `attribute_before_last_save` instead. + # + # DEPRECATION WARNING: The behavior of `attribute_changed?` inside of after callbacks will be changing in the next version of Rails. + # The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). + # To maintain the current behavior, use `saved_change_to_attribute?` instead. + # + # DEPRECATION WARNING: The behavior of `changed_attributes` inside of after callbacks will be changing in the next version of Rails. + # The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). + # To maintain the current behavior, use `saved_changes.transform_values(&:first)` instead. + # + # == Original code == + # if taxon_concept.nil? && taxon_concept_id_was || + # taxon_concept && taxon_concept_id_was && taxon_concept_id != taxon_concept_id_was + # previous_taxon_concept = TaxonConcept.find_by_id(taxon_concept_id_was) + # if previous_taxon_concept + # bump_dependents_timestamp(previous_taxon_concept, updated_by_id) + # end + # end + # == Changed to fix deprecation warnings == + if taxon_concept.nil? && taxon_concept_id_before_last_save || + taxon_concept && taxon_concept_id_before_last_save && taxon_concept_id != taxon_concept_id_before_last_save + previous_taxon_concept = TaxonConcept.find_by_id(taxon_concept_id_before_last_save) + if previous_taxon_concept + changable_bump_dependents_timestamp_part_two + end + end + end + + def changable_clear_cache + return unless respond_to?(:taxon_concept) + + DownloadsCacheCleanupWorker.perform_async(self.class.to_s.tableize) + end + + def changable_bump_dependents_timestamp_part_one(taxon_concept, updated_by_id) + return unless taxon_concept + + TaxonConcept.where(id: taxon_concept.id).update_all( + dependents_updated_at: Time.now, + dependents_updated_by_id: updated_by_id + ) + end + + def changable_bump_dependents_timestamp_part_two + return unless taxon_concept + + DownloadsCacheCleanupWorker.perform_async('taxon_concepts') + end + + def changable_clear_show_tc_serializer_cache + ## + # Disabling because we use memcache on production, but memcache doesn't implement this method. + # For now, changes to records that appear in serializers will not change until the caches are expired, + # which is 24 hours. Possible solution: + # https://unep-wcmc.codebasehq.com/projects/cites-support-maintenance/tickets/114 + + Rails.cache.delete_matched('*ShowTaxonConceptSerializer*') unless Rails.env.production? + end +end diff --git a/app/models/concerns/deletable.rb b/app/models/concerns/deletable.rb new file mode 100644 index 000000000..138ed5366 --- /dev/null +++ b/app/models/concerns/deletable.rb @@ -0,0 +1,20 @@ +module Deletable + extend ActiveSupport::Concern + + included do + before_destroy :before_destroy_checking + end + + private + + def before_destroy_checking + unless can_be_deleted? + msg = 'not allowed' + unless dependent_objects.empty? + msg << " (dependent objects present: #{dependent_objects.join(', ')})" + end + errors.add(:base, msg) + throw :abort + end + end +end diff --git a/app/models/designation.rb b/app/models/designation.rb index b49873465..450581640 100644 --- a/app/models/designation.rb +++ b/app/models/designation.rb @@ -12,6 +12,7 @@ class Designation < ApplicationRecord # Migrated to controller (Strong Parameters) # attr_accessible :name, :taxonomy_id + include Deletable include Dictionary build_dictionary :cites, :eu, :cms diff --git a/app/models/destroy_observer.rb b/app/models/destroy_observer.rb deleted file mode 100644 index fbca07ebd..000000000 --- a/app/models/destroy_observer.rb +++ /dev/null @@ -1,18 +0,0 @@ -class DestroyObserver < ActiveRecord::Observer - observe :taxonomy, :rank, :taxon_concept, :designation, :change_type, - :species_listing, :geo_entity, :language, :term, :unit, :source, :purpose, - :user, :cites_suspension_notification, :cites_cop, :eu_regulation, - :eu_decision_type, :reference, :instrument, :eu_suspension_regulation, - :preset_tag, :annotation - - def before_destroy(model) - unless model.can_be_deleted? - msg = 'not allowed' - unless model.dependent_objects.empty? - msg << " (dependent objects present: #{model.dependent_objects.join(', ')})" - end - model.errors.add(:base, msg) - throw :abort - end - end -end diff --git a/app/models/distribution.rb b/app/models/distribution.rb index b432395ff..f82d24c98 100644 --- a/app/models/distribution.rb +++ b/app/models/distribution.rb @@ -13,6 +13,7 @@ # class Distribution < ApplicationRecord + include Changable include TrackWhoDoesIt # Migrated to controller (Strong Parameters) # attr_accessible :geo_entity_id, :taxon_concept_id, :tag_list, diff --git a/app/models/document.rb b/app/models/document.rb index 30f2f7754..6b62e7c87 100644 --- a/app/models/document.rb +++ b/app/models/document.rb @@ -65,6 +65,9 @@ class Document < ApplicationRecord before_validation :set_title before_validation :reset_designation_if_event_set + after_save :sync_sort_index + after_commit :clear_cache + # order docs based on a custom list of ids scope :for_ids_with_order, ->(ids) { order = sanitize_sql_array( @@ -144,4 +147,42 @@ def is_pdf? (attr =~ /\.pdf/).present? end + def clear_cache + DocumentSearch.clear_cache + end + + def sync_sort_index + # Rails 5.1 to 5.2 + # DEPRECATION WARNING: The behavior of `attribute_was` inside of after callbacks will be changing in the next version of Rails. + # The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). + # To maintain the current behavior, use `attribute_before_last_save` instead. + # + # DEPRECATION WARNING: The behavior of `attribute_changed?` inside of after callbacks will be changing in the next version of Rails. + # The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). + # To maintain the current behavior, use `saved_change_to_attribute?` instead. + # + # DEPRECATION WARNING: The behavior of `changed_attributes` inside of after callbacks will be changing in the next version of Rails. + # The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). + # To maintain the current behavior, use `saved_changes.transform_values(&:first)` instead. + # + # == Original code == + # if sort_index_changed? + # == Changed to fix deprecation warnings == + if saved_change_to_sort_index? + if primary_language_document && + primary_language_document_id != id + primary_language_document.update_attribute( + :sort_index, + sort_index + ) + else + secondary_language_documents.reload.each do |d| + d.update_attribute( + :sort_index, + sort_index + ) + end + end + end + end end diff --git a/app/models/document_observer.rb b/app/models/document_observer.rb deleted file mode 100644 index f2b27f293..000000000 --- a/app/models/document_observer.rb +++ /dev/null @@ -1,49 +0,0 @@ -class DocumentObserver < ActiveRecord::Observer - - def after_save(document) - sync_sort_index(document) - DocumentSearch.clear_cache - end - - def after_destroy(document) - DocumentSearch.clear_cache - end - - private - - def sync_sort_index(document) - # Rails 5.1 to 5.2 - # DEPRECATION WARNING: The behavior of `attribute_was` inside of after callbacks will be changing in the next version of Rails. - # The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). - # To maintain the current behavior, use `attribute_before_last_save` instead. - # - # DEPRECATION WARNING: The behavior of `attribute_changed?` inside of after callbacks will be changing in the next version of Rails. - # The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). - # To maintain the current behavior, use `saved_change_to_attribute?` instead. - # - # DEPRECATION WARNING: The behavior of `changed_attributes` inside of after callbacks will be changing in the next version of Rails. - # The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). - # To maintain the current behavior, use `saved_changes.transform_values(&:first)` instead. - # - # == Original code == - # if document.sort_index_changed? - # == Changed to fix deprecation warnings == - if document.saved_change_to_sort_index? - if document.primary_language_document && - document.primary_language_document_id != document.id - document.primary_language_document.update_attribute( - :sort_index, - document.sort_index - ) - else - document.secondary_language_documents.reload.each do |d| - d.update_attribute( - :sort_index, - document.sort_index - ) - end - end - end - end - -end diff --git a/app/models/eu_decision.rb b/app/models/eu_decision.rb index 59f2741af..a540b9165 100644 --- a/app/models/eu_decision.rb +++ b/app/models/eu_decision.rb @@ -29,6 +29,7 @@ require 'digest/sha1' require 'csv' class EuDecision < ApplicationRecord + include Changable extend Mobility include TrackWhoDoesIt # Migrated to controller (Strong Parameters) @@ -54,6 +55,8 @@ class EuDecision < ApplicationRecord translates :nomenclature_note + after_commit :cache_cleanup, on: :destroy + def year start_date ? start_date.strftime('%Y') : '' end @@ -90,4 +93,10 @@ def eu_decision_type_and_or_srg_history return if eu_decision_type_id || srg_history_id errors.add(:base, "Eu decision type and SRG history can't be blank at the same time") end + + private + + def cache_cleanup + DownloadsCacheCleanupWorker.perform_async('eu_decisions') + end end diff --git a/app/models/eu_decision_observer.rb b/app/models/eu_decision_observer.rb deleted file mode 100644 index 66c08187c..000000000 --- a/app/models/eu_decision_observer.rb +++ /dev/null @@ -1,7 +0,0 @@ -class EuDecisionObserver < ActiveRecord::Observer - - def after_destroy(eu_decision) - DownloadsCacheCleanupWorker.perform_async('eu_decisions') - end - -end diff --git a/app/models/eu_decision_type.rb b/app/models/eu_decision_type.rb index 1ba1942f6..dc2dc7190 100644 --- a/app/models/eu_decision_type.rb +++ b/app/models/eu_decision_type.rb @@ -11,6 +11,8 @@ # class EuDecisionType < ApplicationRecord + include Deletable + # Migrated to controller (Strong Parameters) # attr_accessible :name, :tooltip, :decision_type include Dictionary diff --git a/app/models/events/cites_cop.rb b/app/models/events/cites_cop.rb index b92787a86..f127b4982 100644 --- a/app/models/events/cites_cop.rb +++ b/app/models/events/cites_cop.rb @@ -24,6 +24,7 @@ # class CitesCop < Event + include Deletable # Migrated to controller (Strong Parameters) # attr_accessible :is_current has_many :listing_changes, :foreign_key => :event_id @@ -32,6 +33,11 @@ class CitesCop < Event validate :designation_is_cites validates :effective_at, :presence => true + before_validation do + cites = Designation.find_by_name('CITES') + self.designation_id = cites && cites.id + end + def self.elibrary_document_types [Document::Proposal] end diff --git a/app/models/events/cites_cop_observer.rb b/app/models/events/cites_cop_observer.rb deleted file mode 100644 index 3ab2a95f6..000000000 --- a/app/models/events/cites_cop_observer.rb +++ /dev/null @@ -1,8 +0,0 @@ -class CitesCopObserver < ActiveRecord::Observer - - def before_validation(cites_cop) - cites = Designation.find_by_name('CITES') - cites_cop.designation_id = cites && cites.id - end - -end diff --git a/app/models/events/cites_suspension_notification.rb b/app/models/events/cites_suspension_notification.rb index 0bf6af6ff..48bae5c1e 100644 --- a/app/models/events/cites_suspension_notification.rb +++ b/app/models/events/cites_suspension_notification.rb @@ -24,6 +24,7 @@ # class CitesSuspensionNotification < Event + include Deletable # Migrated to controller (Strong Parameters) # attr_accessible :subtype, :new_subtype, :end_date attr_accessor :new_subtype @@ -36,6 +37,10 @@ class CitesSuspensionNotification < Event validates :effective_at, :presence => true before_save :handle_new_subtype + before_validation do + cites = Designation.find_by_name('CITES') + self.designation_id = cites && cites.id + end def handle_new_subtype unless new_subtype.blank? diff --git a/app/models/events/cites_suspension_notification_observer.rb b/app/models/events/cites_suspension_notification_observer.rb deleted file mode 100644 index 252f7cf4b..000000000 --- a/app/models/events/cites_suspension_notification_observer.rb +++ /dev/null @@ -1,8 +0,0 @@ -class CitesSuspensionNotificationObserver < ActiveRecord::Observer - - def before_validation(cites_suspension_notification) - cites = Designation.find_by_name('CITES') - cites_suspension_notification.designation_id = cites && cites.id - end - -end diff --git a/app/models/events/eu_council_regulation.rb b/app/models/events/eu_council_regulation.rb index 091379ded..1cfe85149 100644 --- a/app/models/events/eu_council_regulation.rb +++ b/app/models/events/eu_council_regulation.rb @@ -23,7 +23,7 @@ # elib_legacy_id :integer # -class EuCouncilRegulation < Event +class EuCouncilRegulation < EuEvent validate :designation_is_eu validates :effective_at, :presence => true end diff --git a/app/models/events/eu_event.rb b/app/models/events/eu_event.rb new file mode 100644 index 000000000..12b81e69e --- /dev/null +++ b/app/models/events/eu_event.rb @@ -0,0 +1,6 @@ +class EuEvent < Event + before_validation do + eu = Designation.find_by_name('EU') + self.designation_id = eu && eu.id + end +end diff --git a/app/models/events/eu_event_observer.rb b/app/models/events/eu_event_observer.rb deleted file mode 100644 index 46fe329ab..000000000 --- a/app/models/events/eu_event_observer.rb +++ /dev/null @@ -1,10 +0,0 @@ -class EuEventObserver < ActiveRecord::Observer - observe :eu_regulation, :eu_suspension_regulation, - :eu_implementing_regulation, :eu_council_regulation - - def before_validation(eu_event) - eu = Designation.find_by_name('EU') - eu_event.designation_id = eu && eu.id - end - -end diff --git a/app/models/events/eu_implementing_regulation.rb b/app/models/events/eu_implementing_regulation.rb index 2859f8072..6c7af541f 100644 --- a/app/models/events/eu_implementing_regulation.rb +++ b/app/models/events/eu_implementing_regulation.rb @@ -23,7 +23,7 @@ # elib_legacy_id :integer # -class EuImplementingRegulation < Event +class EuImplementingRegulation < EuEvent validate :designation_is_eu validates :effective_at, :presence => true end diff --git a/app/models/events/eu_regulation.rb b/app/models/events/eu_regulation.rb index 3eebcf2ec..360eca871 100644 --- a/app/models/events/eu_regulation.rb +++ b/app/models/events/eu_regulation.rb @@ -23,7 +23,9 @@ # elib_legacy_id :integer # -class EuRegulation < Event +class EuRegulation < EuEvent + include Deletable + # Migrated to controller (Strong Parameters) # attr_accessible :listing_changes_event_id, :end_date attr_accessor :listing_changes_event_id @@ -34,14 +36,25 @@ class EuRegulation < Event validate :designation_is_eu validates :effective_at, :presence => true + after_commit :async_event_listing_changes_copy_worker, on: :create + def activate! super - notify_observers(:after_activate) + EuRegulationActivationWorker.perform_async(id, true) end def deactivate! super - notify_observers(:after_deactivate) + EuRegulationActivationWorker.perform_async(id, false) end + private + + def async_event_listing_changes_copy_worker + unless listing_changes_event_id.blank? + EventListingChangesCopyWorker.perform_async( + listing_changes_event_id.to_i, id + ) + end + end end diff --git a/app/models/events/eu_regulation_observer.rb b/app/models/events/eu_regulation_observer.rb deleted file mode 100644 index a00211c40..000000000 --- a/app/models/events/eu_regulation_observer.rb +++ /dev/null @@ -1,19 +0,0 @@ -class EuRegulationObserver < ActiveRecord::Observer - - def after_create(eu_regulation) - unless eu_regulation.listing_changes_event_id.blank? - EventListingChangesCopyWorker.perform_async( - eu_regulation.listing_changes_event_id.to_i, eu_regulation.id - ) - end - end - - def after_activate(eu_regulation) - EuRegulationActivationWorker.perform_async(eu_regulation.id, true) - end - - def after_deactivate(eu_regulation) - EuRegulationActivationWorker.perform_async(eu_regulation.id, false) - end - -end diff --git a/app/models/events/eu_suspension_regulation.rb b/app/models/events/eu_suspension_regulation.rb index 136c94ae1..f52487332 100644 --- a/app/models/events/eu_suspension_regulation.rb +++ b/app/models/events/eu_suspension_regulation.rb @@ -23,7 +23,9 @@ # elib_legacy_id :integer # -class EuSuspensionRegulation < Event +class EuSuspensionRegulation < EuEvent + include Deletable + # Migrated to controller (Strong Parameters) # attr_accessible :eu_suspensions_event_id attr_accessor :eu_suspensions_event_id @@ -37,6 +39,10 @@ class EuSuspensionRegulation < Event validates :effective_at, :presence => true validate :end_date_presence + after_update :touch_suspensions_and_taxa + after_commit :after_create_async_tasks, on: :create + after_commit :async_downloads_cache_cleanup, on: :update + def name_and_date "#{self.name} (Effective from: #{self.effective_at.strftime("%d/%m/%Y")})" end @@ -69,4 +75,15 @@ def end_date_presence end end + def after_create_async_tasks + unless eu_suspensions_event_id.blank? + EventEuSuspensionCopyWorker.perform_async(eu_suspensions_event_id, id) + DownloadsCacheCleanupWorker.perform_async('eu_decisions') + end + end + + def async_downloads_cache_cleanup + DownloadsCacheCleanupWorker.perform_async('eu_decisions') + end + end diff --git a/app/models/events/eu_suspension_regulation_observer.rb b/app/models/events/eu_suspension_regulation_observer.rb deleted file mode 100644 index 4f45aa1ef..000000000 --- a/app/models/events/eu_suspension_regulation_observer.rb +++ /dev/null @@ -1,17 +0,0 @@ -class EuSuspensionRegulationObserver < ActiveRecord::Observer - - def after_create(eu_suspension_regulation) - unless eu_suspension_regulation.eu_suspensions_event_id.blank? - EventEuSuspensionCopyWorker.perform_async( - eu_suspension_regulation.eu_suspensions_event_id, - eu_suspension_regulation.id - ) - DownloadsCacheCleanupWorker.perform_async('eu_decisions') - end - end - - def after_update(eu_suspension_regulation) - eu_suspension_regulation.touch_suspensions_and_taxa - DownloadsCacheCleanupWorker.perform_async('eu_decisions') - end -end diff --git a/app/models/geo_entity.rb b/app/models/geo_entity.rb index d7039f0e4..c4867f5a3 100644 --- a/app/models/geo_entity.rb +++ b/app/models/geo_entity.rb @@ -18,6 +18,8 @@ # class GeoEntity < ApplicationRecord + include Changable + include Deletable extend Mobility # Migrated to controller (Strong Parameters) # attr_accessible :geo_entity_type_id, :iso_code2, :iso_code3, @@ -66,6 +68,8 @@ class GeoEntity < ApplicationRecord scope :current, -> { where(:is_current => true) } + after_commit :geo_entity_search_increment_cache_iterator + def self.nodes_and_descendants(nodes_ids = []) joins_sql = <<-SQL INNER JOIN ( @@ -137,4 +141,7 @@ def dependent_objects_map } end + def geo_entity_search_increment_cache_iterator + GeoEntitySearch.increment_cache_iterator + end end diff --git a/app/models/geo_entity_observer.rb b/app/models/geo_entity_observer.rb deleted file mode 100644 index e90961a63..000000000 --- a/app/models/geo_entity_observer.rb +++ /dev/null @@ -1,10 +0,0 @@ -class GeoEntityObserver < ActiveRecord::Observer - - def after_save(geo_entity) - GeoEntitySearch.increment_cache_iterator - end - - def after_destroy(geo_entity) - GeoEntitySearch.increment_cache_iterator - end -end diff --git a/app/models/instrument.rb b/app/models/instrument.rb index e5b6a4c32..5cc70d599 100644 --- a/app/models/instrument.rb +++ b/app/models/instrument.rb @@ -10,6 +10,7 @@ # class Instrument < ApplicationRecord + include Deletable # Migrated to controller (Strong Parameters) # attr_accessible :designation_id, :name diff --git a/app/models/language.rb b/app/models/language.rb index fc324c2af..ca9b400bf 100644 --- a/app/models/language.rb +++ b/app/models/language.rb @@ -13,6 +13,8 @@ # class Language < ApplicationRecord + include Changable + include Deletable extend Mobility # Migrated to controller (Strong Parameters) # attr_accessible :iso_code1, :iso_code3, :name_en, :name_fr, :name_es diff --git a/app/models/listing_change.rb b/app/models/listing_change.rb index 15c33a614..4daa4efd7 100644 --- a/app/models/listing_change.rb +++ b/app/models/listing_change.rb @@ -27,6 +27,7 @@ # class ListingChange < ApplicationRecord + include Changable extend Mobility include TrackWhoDoesIt # Migrated to controller (Strong Parameters) @@ -62,6 +63,8 @@ class ListingChange < ApplicationRecord validate :species_listing_designation_mismatch validate :event_designation_mismatch + before_save :listing_change_before_save_callback + accepts_nested_attributes_for :party_listing_distribution, :reject_if => proc { |attributes| attributes['geo_entity_id'].blank? } @@ -183,4 +186,59 @@ def event_designation_mismatch return false end end + + def listing_change_before_save_callback + # check if annotation should be deleted + if annotation && + annotation.short_note_en.blank? && + annotation.short_note_fr.blank? && + annotation.short_note_es.blank? && + annotation.full_note_en.blank? && + annotation.full_note_fr.blank? && + annotation.full_note_es.blank? + ann = annotation + self.annotation = nil + if ann.reload.listing_changes.empty? + ann.delete + end + end + + original_change_type = ChangeType.find(change_type_id) + return self if original_change_type.name == ChangeType::EXCEPTION + return self if excluded_geo_entities_ids.nil? && + excluded_taxon_concepts_ids.nil? + new_exclusions = [] + exclusion_change_type = ChangeType.find_by_name_and_designation_id( + ChangeType::EXCEPTION, original_change_type.designation_id + ) + + # geographic exclusions + excluded_geo_entities_ids = excluded_geo_entities_ids && + excluded_geo_entities_ids.reject(&:blank?) + excluded_geo_entities = + if excluded_geo_entities_ids && !excluded_geo_entities_ids.empty? + new_exclusions << ListingChange.new( + :change_type_id => exclusion_change_type.id, + :species_listing_id => species_listing_id, + :taxon_concept_id => taxon_concept_id, + :geo_entity_ids => excluded_geo_entities_ids + ) + end + + # taxonomic exclusions + excluded_taxon_concepts_ids = excluded_taxon_concepts_ids && + excluded_taxon_concepts_ids.split(',').reject(&:blank?) + excluded_taxon_concepts = + if excluded_taxon_concepts_ids && !excluded_taxon_concepts_ids.empty? + excluded_taxon_concepts_ids.map do |id| + new_exclusions << ListingChange.new( + :change_type_id => exclusion_change_type.id, + :species_listing_id => species_listing_id, + :taxon_concept_id => id + ) + end + end + + self.exclusions = new_exclusions + end end diff --git a/app/models/listing_change_observer.rb b/app/models/listing_change_observer.rb deleted file mode 100644 index 26d8f57d4..000000000 --- a/app/models/listing_change_observer.rb +++ /dev/null @@ -1,58 +0,0 @@ -class ListingChangeObserver < ActiveRecord::Observer - - def before_save(listing_change) - # check if annotation should be deleted - if listing_change.annotation && - listing_change.annotation.short_note_en.blank? && - listing_change.annotation.short_note_fr.blank? && - listing_change.annotation.short_note_es.blank? && - listing_change.annotation.full_note_en.blank? && - listing_change.annotation.full_note_fr.blank? && - listing_change.annotation.full_note_es.blank? - ann = listing_change.annotation - listing_change.annotation = nil - if ann.reload.listing_changes.empty? - ann.delete - end - end - - original_change_type = ChangeType.find(listing_change.change_type_id) - return listing_change if original_change_type.name == ChangeType::EXCEPTION - return listing_change if listing_change.excluded_geo_entities_ids.nil? && - listing_change.excluded_taxon_concepts_ids.nil? - new_exclusions = [] - exclusion_change_type = ChangeType.find_by_name_and_designation_id( - ChangeType::EXCEPTION, original_change_type.designation_id - ) - - # geographic exclusions - excluded_geo_entities_ids = listing_change.excluded_geo_entities_ids && - listing_change.excluded_geo_entities_ids.reject(&:blank?) - excluded_geo_entities = - if excluded_geo_entities_ids && !excluded_geo_entities_ids.empty? - new_exclusions << ListingChange.new( - :change_type_id => exclusion_change_type.id, - :species_listing_id => listing_change.species_listing_id, - :taxon_concept_id => listing_change.taxon_concept_id, - :geo_entity_ids => excluded_geo_entities_ids - ) - end - - # taxonomic exclusions - excluded_taxon_concepts_ids = listing_change.excluded_taxon_concepts_ids && - listing_change.excluded_taxon_concepts_ids.split(',').reject(&:blank?) - excluded_taxon_concepts = - if excluded_taxon_concepts_ids && !excluded_taxon_concepts_ids.empty? - excluded_taxon_concepts_ids.map do |id| - new_exclusions << ListingChange.new( - :change_type_id => exclusion_change_type.id, - :species_listing_id => listing_change.species_listing_id, - :taxon_concept_id => id - ) - end - end - - listing_change.exclusions = new_exclusions - end - -end diff --git a/app/models/nomenclature_change.rb b/app/models/nomenclature_change.rb index 2335d2e98..91bc670d1 100644 --- a/app/models/nomenclature_change.rb +++ b/app/models/nomenclature_change.rb @@ -28,6 +28,19 @@ class NomenclatureChange < ApplicationRecord validates :status, presence: true validate :cannot_update_when_locked + after_save do + if status == self.class::SUBMITTED + Rails.logger.warn "SUBMIT #{type}" + begin + processor_klass = "#{type}::Processor".constantize + rescue NameError + Rails.logger.warn "No processor found for #{type}" + else + processor_klass.new(self).run + end + end + end + def in_progress? ![NomenclatureChange::SUBMITTED, NomenclatureChange::CLOSED]. include?(status) diff --git a/app/models/nomenclature_change_observer.rb b/app/models/nomenclature_change_observer.rb deleted file mode 100644 index bd2590760..000000000 --- a/app/models/nomenclature_change_observer.rb +++ /dev/null @@ -1,16 +0,0 @@ -class NomenclatureChangeObserver < ActiveRecord::Observer - - def after_save(nomenclature_change) - if nomenclature_change.status == nomenclature_change.class::SUBMITTED - Rails.logger.warn "SUBMIT #{nomenclature_change.type}" - begin - processor_klass = "#{nomenclature_change.type}::Processor".constantize - rescue NameError - Rails.logger.warn "No processor found for #{nomenclature_change.type}" - else - processor_klass.new(nomenclature_change).run - end - end - end - -end diff --git a/app/models/preset_tag.rb b/app/models/preset_tag.rb index f87372d66..7ef4a964a 100644 --- a/app/models/preset_tag.rb +++ b/app/models/preset_tag.rb @@ -10,6 +10,7 @@ # class PresetTag < ApplicationRecord + include Deletable # Migrated to controller (Strong Parameters) # attr_accessible :model, :name diff --git a/app/models/rank.rb b/app/models/rank.rb index bbd2532ab..248098936 100644 --- a/app/models/rank.rb +++ b/app/models/rank.rb @@ -14,6 +14,7 @@ # class Rank < ApplicationRecord + include Deletable extend Mobility # Migrated to controller (Strong Parameters) # attr_accessible :name, :display_name_en, :display_name_es, :display_name_fr, diff --git a/app/models/reference.rb b/app/models/reference.rb index a445a1808..dbce3aad8 100644 --- a/app/models/reference.rb +++ b/app/models/reference.rb @@ -17,6 +17,7 @@ # class Reference < ApplicationRecord + include Deletable include TrackWhoDoesIt # Migrated to controller (Strong Parameters) # attr_accessible :citation, :created_by_id, :updated_by_id diff --git a/app/models/species_listing.rb b/app/models/species_listing.rb index 1d722dadd..f228bf179 100644 --- a/app/models/species_listing.rb +++ b/app/models/species_listing.rb @@ -11,6 +11,7 @@ # class SpeciesListing < ApplicationRecord + include Deletable # Migrated to controller (Strong Parameters) # attr_accessible :designation_id, :name, :abbreviation diff --git a/app/models/taxon_common.rb b/app/models/taxon_common.rb index f681562b8..91fe7915c 100644 --- a/app/models/taxon_common.rb +++ b/app/models/taxon_common.rb @@ -12,6 +12,7 @@ # class TaxonCommon < ApplicationRecord + include Changable include TrackWhoDoesIt # Migrated to controller (Strong Parameters) # attr_accessible :common_name_id, :taxon_concept_id, :created_by_id, diff --git a/app/models/taxon_concept.rb b/app/models/taxon_concept.rb index c44c8ced4..c84707f3d 100644 --- a/app/models/taxon_concept.rb +++ b/app/models/taxon_concept.rb @@ -31,6 +31,7 @@ # class TaxonConcept < ApplicationRecord + include Deletable extend Mobility include TrackWhoDoesIt has_paper_trail versions: { class_name: "TaxonConceptVersion" }, on: :destroy, @@ -173,6 +174,73 @@ class TaxonConcept < ApplicationRecord } before_validation :ensure_taxonomic_position + before_validation do + self.full_name = + if rank && parent && ['A', 'N'].include?(name_status) + rank_name = rank.name + parent_full_name = parent.full_name + name = scientific_name + # if name is present, just in case it is a multipart name + # e.g. when changing status from S, T, H + # make sure to only use last part + if name.present? + name = TaxonName.sanitize_scientific_name(name) + end + if name.blank? + nil + elsif [Rank::SPECIES, Rank::SUBSPECIES].include?(rank_name) + "#{parent_full_name} #{name.downcase}" + elsif rank_name == Rank::VARIETY + "#{parent_full_name} var. #{name.downcase}" + else + name + end + else + scientific_name + end + end + after_create do + ensure_species_touched + Species::Search.increment_cache_iterator + Species::TaxonConceptPrefixMatcher.increment_cache_iterator + Checklist::Checklist.increment_cache_iterator + end + after_update do + ensure_species_touched + if saved_change_to_rank_id? || + saved_change_to_taxon_name_id? || + saved_change_to_parent_id? || + saved_change_to_name_status? + Species::Search.increment_cache_iterator + Species::TaxonConceptPrefixMatcher.increment_cache_iterator + Checklist::Checklist.increment_cache_iterator + end + end + after_save do + if ['A', 'N'].include? name_status + tcd = TaxonConceptData.new(self) + data = tcd.to_h + update_column(:data, data) + self.data = data + end + if name_status == 'S' + rebuild_relationships(accepted_names_ids) + end + if name_status == 'T' + rebuild_relationships(accepted_names_for_trade_name_ids) + end + if name_status == 'H' + rebuild_relationships(hybrid_parents_ids) + end + end + after_destroy do + ensure_species_touched + Species::Search.increment_cache_iterator + Species::TaxonConceptPrefixMatcher.increment_cache_iterator + Checklist::Checklist.increment_cache_iterator + end + after_touch :ensure_species_touched + after_commit :cache_cleanup translates :nomenclature_note @@ -506,4 +574,16 @@ def full_name_cannot_be_changed true end + def cache_cleanup + DownloadsCacheCleanupWorker.perform_async('taxon_concepts') + end + + def ensure_species_touched + if rank && parent && [Rank::SUBSPECIES, Rank::VARIETY].include?(rank.name) + # touch parent if we're a variety or subspecies + Rails.logger.info "Touch species" + parent.touch + end + end + end diff --git a/app/models/taxon_concept_observer.rb b/app/models/taxon_concept_observer.rb deleted file mode 100644 index 1f0e8e5b3..000000000 --- a/app/models/taxon_concept_observer.rb +++ /dev/null @@ -1,109 +0,0 @@ -class TaxonConceptObserver < ActiveRecord::Observer - - def before_validation(taxon_concept) - taxon_concept.full_name = - if taxon_concept.rank && - taxon_concept.parent && - ['A', 'N'].include?(taxon_concept.name_status) - rank_name = taxon_concept.rank.name - parent_full_name = taxon_concept.parent.full_name - name = taxon_concept.scientific_name - # if name is present, just in case it is a multipart name - # e.g. when changing status from S, T, H - # make sure to only use last part - if name.present? - name = TaxonName.sanitize_scientific_name(name) - end - if name.blank? - nil - elsif [Rank::SPECIES, Rank::SUBSPECIES].include?(rank_name) - "#{parent_full_name} #{name.downcase}" - elsif rank_name == Rank::VARIETY - "#{parent_full_name} var. #{name.downcase}" - else - name - end - else - taxon_concept.scientific_name - end - end - - def after_create(taxon_concept) - ensure_species_touched(taxon_concept) - Species::Search.increment_cache_iterator - Species::TaxonConceptPrefixMatcher.increment_cache_iterator - Checklist::Checklist.increment_cache_iterator - end - - def after_destroy(taxon_concept) - ensure_species_touched(taxon_concept) - Species::Search.increment_cache_iterator - Species::TaxonConceptPrefixMatcher.increment_cache_iterator - Checklist::Checklist.increment_cache_iterator - DownloadsCacheCleanupWorker.perform_async('taxon_concepts') - end - - def after_update(taxon_concept) - ensure_species_touched(taxon_concept) - # Rails 5.1 to 5.2 - # DEPRECATION WARNING: The behavior of `attribute_was` inside of after callbacks will be changing in the next version of Rails. - # The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). - # To maintain the current behavior, use `attribute_before_last_save` instead. - # - # DEPRECATION WARNING: The behavior of `attribute_changed?` inside of after callbacks will be changing in the next version of Rails. - # The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). - # To maintain the current behavior, use `saved_change_to_attribute?` instead. - # - # DEPRECATION WARNING: The behavior of `changed_attributes` inside of after callbacks will be changing in the next version of Rails. - # The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). - # To maintain the current behavior, use `saved_changes.transform_values(&:first)` instead. - # - # == Original code == - # if taxon_concept.rank_id_changed? || - # taxon_concept.taxon_name_id_changed? || - # taxon_concept.parent_id_changed? || - # taxon_concept.name_status_changed? - # == Changed to fix deprecation warnings == - if taxon_concept.saved_change_to_rank_id? || - taxon_concept.saved_change_to_taxon_name_id? || - taxon_concept.saved_change_to_parent_id? || - taxon_concept.saved_change_to_name_status? - Species::Search.increment_cache_iterator - Species::TaxonConceptPrefixMatcher.increment_cache_iterator - Checklist::Checklist.increment_cache_iterator - end - end - - def after_touch(taxon_concept) - ensure_species_touched(taxon_concept) - end - - def ensure_species_touched(taxon_concept) - if taxon_concept.rank && taxon_concept.parent && - [Rank::SUBSPECIES, Rank::VARIETY].include?(taxon_concept.rank.name) - # touch parent if we're a variety or subspecies - Rails.logger.info "Touch species" - taxon_concept.parent.touch - end - end - - def after_save(taxon_concept) - if ['A', 'N'].include? taxon_concept.name_status - tcd = TaxonConceptData.new(taxon_concept) - data = tcd.to_h - taxon_concept.update_column(:data, data) - taxon_concept.data = data - end - if taxon_concept.name_status == 'S' - taxon_concept.rebuild_relationships(taxon_concept.accepted_names_ids) - end - if taxon_concept.name_status == 'T' - taxon_concept.rebuild_relationships(taxon_concept.accepted_names_for_trade_name_ids) - end - if taxon_concept.name_status == 'H' - taxon_concept.rebuild_relationships(taxon_concept.hybrid_parents_ids) - end - DownloadsCacheCleanupWorker.perform_async('taxon_concepts') - end - -end diff --git a/app/models/taxon_concept_reference.rb b/app/models/taxon_concept_reference.rb index 1cd2f95ec..3149de990 100644 --- a/app/models/taxon_concept_reference.rb +++ b/app/models/taxon_concept_reference.rb @@ -15,6 +15,7 @@ # class TaxonConceptReference < ApplicationRecord + include Changable include TrackWhoDoesIt # Migrated to controller (Strong Parameters) # attr_accessible :reference_id, :taxon_concept_id, :is_standard, :is_cascaded, diff --git a/app/models/taxon_instrument.rb b/app/models/taxon_instrument.rb index ed9bcdc0a..f4731b050 100644 --- a/app/models/taxon_instrument.rb +++ b/app/models/taxon_instrument.rb @@ -13,6 +13,7 @@ # class TaxonInstrument < ApplicationRecord + include Changable include TrackWhoDoesIt # Migrated to controller (Strong Parameters) # attr_accessible :effective_from, :instrument_id, :taxon_concept_id diff --git a/app/models/taxon_relationship.rb b/app/models/taxon_relationship.rb index 087e71d02..f79992b99 100644 --- a/app/models/taxon_relationship.rb +++ b/app/models/taxon_relationship.rb @@ -13,6 +13,7 @@ # class TaxonRelationship < ApplicationRecord + include Changable include TrackWhoDoesIt # Migrated to controller (Strong Parameters) # attr_accessible :taxon_concept_id, :other_taxon_concept_id, :taxon_relationship_type_id, diff --git a/app/models/taxonomy.rb b/app/models/taxonomy.rb index 129ed4bfd..94ac1a590 100644 --- a/app/models/taxonomy.rb +++ b/app/models/taxonomy.rb @@ -9,6 +9,7 @@ # class Taxonomy < ApplicationRecord + include Deletable include Dictionary build_dictionary :cites_eu, :cms diff --git a/app/models/trade/annual_report_upload.rb b/app/models/trade/annual_report_upload.rb index 7487fce4e..b926bf7c8 100644 --- a/app/models/trade/annual_report_upload.rb +++ b/app/models/trade/annual_report_upload.rb @@ -35,6 +35,12 @@ class Trade::AnnualReportUpload < ApplicationRecord where("epix_created_by_id IS NULL") } + after_create :copy_to_sandbox + before_destroy do + success = sandbox && sandbox.destroy + throw(:abort) unless success + end + def copy_to_sandbox sandbox.copy update_attribute(:number_of_rows, sandbox_shipments.size) diff --git a/app/models/trade/annual_report_upload_observer.rb b/app/models/trade/annual_report_upload_observer.rb deleted file mode 100644 index adfa0b6a0..000000000 --- a/app/models/trade/annual_report_upload_observer.rb +++ /dev/null @@ -1,11 +0,0 @@ -class Trade::AnnualReportUploadObserver < ActiveRecord::Observer - - def after_create(annual_report_upload) - annual_report_upload.copy_to_sandbox - end - - def before_destroy(annual_report_upload) - success = annual_report_upload.sandbox && annual_report_upload.sandbox.destroy - throw(:abort) unless success - end -end diff --git a/app/models/trade/shipment.rb b/app/models/trade/shipment.rb index fd530d727..6252a4c71 100644 --- a/app/models/trade/shipment.rb +++ b/app/models/trade/shipment.rb @@ -71,6 +71,25 @@ class Trade::Shipment < ApplicationRecord belongs_to :exporter, :class_name => "GeoEntity" belongs_to :importer, :class_name => "GeoEntity" + before_save do + @old_permits_ids = [] + [ + import_permits_ids_was, + export_permits_ids_was, + origin_permits_ids_was + ].each do |permits_ids| + @old_permits_ids += permits_ids ? permits_ids.dup : [] + end + unless reported_taxon_concept_id + self.reported_taxon_concept_id = taxon_concept_id + end + end + before_destroy do + @old_permits_ids = permits_ids.dup + end + after_commit :async_tasks_after_save, on: [:create, :update] + after_commit :async_tasks_for_destroy, on: :destroy + after_validation do unless self.errors.empty? && self.ignore_warnings # inject warnings here @@ -154,4 +173,14 @@ def set_permit_number(permit_type, str) send("#{permit_type}_permits_ids=", permits && permits.map(&:id)) end + def async_tasks_after_save + DownloadsCacheCleanupWorker.perform_async('shipments') + disconnected_permits_ids = @old_permits_ids - permits_ids + PermitCleanupWorker.perform_async(disconnected_permits_ids) + end + + def async_tasks_for_destroy + DownloadsCacheCleanupWorker.perform_async('shipments') + PermitCleanupWorker.perform_async(@old_permits_ids) + end end diff --git a/app/models/trade/shipment_observer.rb b/app/models/trade/shipment_observer.rb deleted file mode 100644 index 546d92f25..000000000 --- a/app/models/trade/shipment_observer.rb +++ /dev/null @@ -1,33 +0,0 @@ -class Trade::ShipmentObserver < ActiveRecord::Observer - - def before_save(shipment) - @old_permits_ids = [] - [ - shipment.import_permits_ids_was, - shipment.export_permits_ids_was, - shipment.origin_permits_ids_was - ].each do |permits_ids| - @old_permits_ids += permits_ids ? permits_ids.dup : [] - end - unless shipment.reported_taxon_concept_id - shipment.reported_taxon_concept_id = shipment.taxon_concept_id - end - end - - def before_destroy(shipment) - @old_permits_ids = shipment.permits_ids.dup - end - - def after_save(shipment) - DownloadsCacheCleanupWorker.perform_async('shipments') - disconnected_permits_ids = @old_permits_ids - shipment.permits_ids - PermitCleanupWorker.perform_async(disconnected_permits_ids) - end - - def after_destroy(shipment) - DownloadsCacheCleanupWorker.perform_async('shipments') - disconnected_permits_ids = @old_permits_ids - PermitCleanupWorker.perform_async(disconnected_permits_ids) - end - -end diff --git a/app/models/trade/trade_data_download.rb b/app/models/trade/trade_data_download.rb index 3934e2c4b..32b951998 100644 --- a/app/models/trade/trade_data_download.rb +++ b/app/models/trade/trade_data_download.rb @@ -30,4 +30,11 @@ class Trade::TradeDataDownload < ApplicationRecord # :appendix, :importer, :exporter, :origin, :term, :unit, :source, :purpose, # :number_of_rows, :city, :country, :organization + after_commit :async_downloads_cache_cleanup, on: [:create, :update] + + private + + def async_downloads_cache_cleanup + DownloadsCacheCleanupWorker.perform_async('trade_download_stats') + end end diff --git a/app/models/trade/trade_data_download_observer.rb b/app/models/trade/trade_data_download_observer.rb deleted file mode 100644 index 7b309bb67..000000000 --- a/app/models/trade/trade_data_download_observer.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Trade::TradeDataDownloadObserver < ActiveRecord::Observer - - def after_save(download_log) - DownloadsCacheCleanupWorker.perform_async('trade_download_stats') - end - -end diff --git a/app/models/trade_codes/purpose.rb b/app/models/trade_codes/purpose.rb index bf4aef4b6..29f93a9bf 100644 --- a/app/models/trade_codes/purpose.rb +++ b/app/models/trade_codes/purpose.rb @@ -13,11 +13,15 @@ # class Purpose < TradeCode + include Deletable + validates :code, :length => { :is => 1 } has_many :trade_restriction_purposes has_many :shipments, :class_name => 'Trade::Shipment' + after_commit :expire_controller_action_cache + protected def dependent_objects_map @@ -26,4 +30,20 @@ def dependent_objects_map 'shipments' => shipments } end + + private + + def expire_controller_action_cache + I18n.available_locales.each do |lang| + ActionController::Base.new.send( + :expire_action, + { + controller: 'api/v1/purposes', + format: 'json', + action: 'index', + locale: lang + } + ) + end + end end diff --git a/app/models/trade_codes/source.rb b/app/models/trade_codes/source.rb index 0013fa240..d63ddddf0 100644 --- a/app/models/trade_codes/source.rb +++ b/app/models/trade_codes/source.rb @@ -13,12 +13,16 @@ # class Source < TradeCode + include Deletable + validates :code, :length => { :is => 1 } has_many :trade_restriction_sources has_many :eu_decisions has_many :shipments, :class_name => 'Trade::Shipment' + after_commit :expire_controller_action_cache + protected def dependent_objects_map @@ -28,4 +32,20 @@ def dependent_objects_map 'shipments' => shipments } end + + private + + def expire_controller_action_cache + I18n.available_locales.each do |lang| + ActionController::Base.new.send( + :expire_action, + { + controller: 'api/v1/sources', + format: 'json', + action: 'index', + locale: lang + } + ) + end + end end diff --git a/app/models/trade_codes/term.rb b/app/models/trade_codes/term.rb index 9f5cc7326..55647fb69 100644 --- a/app/models/trade_codes/term.rb +++ b/app/models/trade_codes/term.rb @@ -13,12 +13,16 @@ # class Term < TradeCode + include Deletable + validates :code, :length => { :is => 3 } has_many :trade_restriction_terms has_many :eu_decisions has_many :shipments, :class_name => 'Trade::Shipment' + after_commit :expire_controller_action_cache + protected def dependent_objects_map @@ -28,4 +32,20 @@ def dependent_objects_map 'shipments' => shipments } end + + private + + def expire_controller_action_cache + I18n.available_locales.each do |lang| + ActionController::Base.new.send( + :expire_action, + { + controller: 'api/v1/terms', + format: 'json', + action: 'index', + locale: lang + } + ) + end + end end diff --git a/app/models/trade_codes/unit.rb b/app/models/trade_codes/unit.rb index 702c8efea..bb0495fbf 100644 --- a/app/models/trade_codes/unit.rb +++ b/app/models/trade_codes/unit.rb @@ -13,12 +13,16 @@ # class Unit < TradeCode + include Deletable + validates :code, :length => { :is => 3 } has_many :term_trade_codes_pairs, :as => :trade_code has_many :quotas has_many :shipments, :class_name => 'Trade::Shipment' + after_commit :expire_controller_action_cache + protected def dependent_objects_map @@ -27,4 +31,20 @@ def dependent_objects_map 'shipments' => shipments } end + + private + + def expire_controller_action_cache + I18n.available_locales.each do |lang| + ActionController::Base.new.send( + :expire_action, + { + controller: 'api/v1/units', + format: 'json', + action: 'index', + locale: lang + } + ) + end + end end diff --git a/app/models/trade_restriction.rb b/app/models/trade_restriction.rb index 763000242..c34f39958 100644 --- a/app/models/trade_restriction.rb +++ b/app/models/trade_restriction.rb @@ -60,6 +60,11 @@ class TradeRestriction < ApplicationRecord translates :nomenclature_note + after_save :touch_descendants, unless: -> { taxon_concept.nil? } + before_destroy :touch_descendants, unless: -> { taxon_concept.nil? } + after_save :touch_taxa_with_applicable_distribution, if: -> { taxon_concept.nil? } + before_destroy :touch_taxa_with_applicable_distribution, if: -> { taxon_concept.nil? } + def valid_dates if !(start_date.nil? || end_date.nil?) && (start_date > end_date) self.errors.add(:start_date, ' has to be before end date.') @@ -203,4 +208,73 @@ def self.filter_years(filters) end all end + + private + + def touch_taxa_with_applicable_distribution + update_stmt = TaxonConcept.send(:sanitize_sql_array, [ + "UPDATE taxon_concepts + SET dependents_updated_at = CURRENT_TIMESTAMP, dependents_updated_by_id = :updated_by_id + FROM distributions + WHERE distributions.taxon_concept_id = taxon_concepts.id + AND distributions.geo_entity_id IN (:geo_entity_id)", + updated_by_id: updated_by_id, + geo_entity_id: [ + # Rails 5.1 to 5.2 + # DEPRECATION WARNING: The behavior of `attribute_was` inside of after callbacks will be changing in the next version of Rails. + # The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). + # To maintain the current behavior, use `attribute_before_last_save` instead. + # + # DEPRECATION WARNING: The behavior of `attribute_changed?` inside of after callbacks will be changing in the next version of Rails. + # The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). + # To maintain the current behavior, use `saved_change_to_attribute?` instead. + # + # DEPRECATION WARNING: The behavior of `changed_attributes` inside of after callbacks will be changing in the next version of Rails. + # The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). + # To maintain the current behavior, use `saved_changes.transform_values(&:first)` instead. + # + # == Original code == + # geo_entity_id, geo_entity_id_was + # == Changed to fix deprecation warnings == + geo_entity_id, geo_entity_id_before_last_save + ].compact.uniq + ]) + TaxonConcept.connection.execute update_stmt + end + + def touch_descendants + update_stmt = TaxonConcept.send(:sanitize_sql_array, [ + "UPDATE taxon_concepts + SET dependents_updated_at = CURRENT_TIMESTAMP, dependents_updated_by_id = :updated_by_id + WHERE data IS NOT NULL + AND ARRAY[ + (data->'species_id')::INT, + (data->'genus_id')::INT, + (data->'subfamily_id')::INT, + (data->'family_id')::INT, + (data->'order_id')::INT + ] && ARRAY[:taxon_concept_id] ", + updated_by_id: updated_by_id, + taxon_concept_id: [ + # Rails 5.1 to 5.2 + # DEPRECATION WARNING: The behavior of `attribute_was` inside of after callbacks will be changing in the next version of Rails. + # The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). + # To maintain the current behavior, use `attribute_before_last_save` instead. + # + # DEPRECATION WARNING: The behavior of `attribute_changed?` inside of after callbacks will be changing in the next version of Rails. + # The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). + # To maintain the current behavior, use `saved_change_to_attribute?` instead. + # + # DEPRECATION WARNING: The behavior of `changed_attributes` inside of after callbacks will be changing in the next version of Rails. + # The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). + # To maintain the current behavior, use `saved_changes.transform_values(&:first)` instead. + # + # == Original code == + # taxon_concept_id, taxon_concept_id_was + # == Changed to fix deprecation warnings == + taxon_concept_id, taxon_concept_id_before_last_save + ].compact.uniq + ]) + TaxonConcept.connection.execute(update_stmt) + end end diff --git a/app/models/trade_restriction_observer.rb b/app/models/trade_restriction_observer.rb deleted file mode 100644 index d15b226da..000000000 --- a/app/models/trade_restriction_observer.rb +++ /dev/null @@ -1,88 +0,0 @@ -class TradeRestrictionObserver < ActiveRecord::Observer - - def after_save(trade_restriction) - if trade_restriction.taxon_concept.nil? - touch_taxa_with_applicable_distribution(trade_restriction) - else - touch_descendants(trade_restriction) - end - end - - def before_destroy(trade_restriction) - if trade_restriction.taxon_concept.nil? - touch_taxa_with_applicable_distribution(trade_restriction) - else - touch_descendants(trade_restriction) - end - end - - private - - def touch_taxa_with_applicable_distribution(trade_restriction) - update_stmt = TaxonConcept.send(:sanitize_sql_array, [ - "UPDATE taxon_concepts - SET dependents_updated_at = CURRENT_TIMESTAMP, dependents_updated_by_id = :updated_by_id - FROM distributions - WHERE distributions.taxon_concept_id = taxon_concepts.id - AND distributions.geo_entity_id IN (:geo_entity_id)", - :updated_by_id => trade_restriction.updated_by_id, - :geo_entity_id => [ - # Rails 5.1 to 5.2 - # DEPRECATION WARNING: The behavior of `attribute_was` inside of after callbacks will be changing in the next version of Rails. - # The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). - # To maintain the current behavior, use `attribute_before_last_save` instead. - # - # DEPRECATION WARNING: The behavior of `attribute_changed?` inside of after callbacks will be changing in the next version of Rails. - # The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). - # To maintain the current behavior, use `saved_change_to_attribute?` instead. - # - # DEPRECATION WARNING: The behavior of `changed_attributes` inside of after callbacks will be changing in the next version of Rails. - # The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). - # To maintain the current behavior, use `saved_changes.transform_values(&:first)` instead. - # - # == Original code == - # trade_restriction.geo_entity_id, trade_restriction.geo_entity_id_was - # == Changed to fix deprecation warnings == - trade_restriction.geo_entity_id, trade_restriction.geo_entity_id_before_last_save - ].compact.uniq - ]) - TaxonConcept.connection.execute update_stmt - end - - def touch_descendants(trade_restriction) - update_stmt = TaxonConcept.send(:sanitize_sql_array, [ - "UPDATE taxon_concepts - SET dependents_updated_at = CURRENT_TIMESTAMP, dependents_updated_by_id = :updated_by_id - WHERE data IS NOT NULL - AND ARRAY[ - (data->'species_id')::INT, - (data->'genus_id')::INT, - (data->'subfamily_id')::INT, - (data->'family_id')::INT, - (data->'order_id')::INT - ] && ARRAY[:taxon_concept_id] ", - :updated_by_id => trade_restriction.updated_by_id, - :taxon_concept_id => [ - # Rails 5.1 to 5.2 - # DEPRECATION WARNING: The behavior of `attribute_was` inside of after callbacks will be changing in the next version of Rails. - # The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). - # To maintain the current behavior, use `attribute_before_last_save` instead. - # - # DEPRECATION WARNING: The behavior of `attribute_changed?` inside of after callbacks will be changing in the next version of Rails. - # The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). - # To maintain the current behavior, use `saved_change_to_attribute?` instead. - # - # DEPRECATION WARNING: The behavior of `changed_attributes` inside of after callbacks will be changing in the next version of Rails. - # The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). - # To maintain the current behavior, use `saved_changes.transform_values(&:first)` instead. - # - # == Original code == - # trade_restriction.taxon_concept_id, trade_restriction.taxon_concept_id_was - # == Changed to fix deprecation warnings == - trade_restriction.taxon_concept_id, trade_restriction.taxon_concept_id_before_last_save - ].compact.uniq - ]) - TaxonConcept.connection.execute update_stmt - end - -end diff --git a/app/models/trade_restrictions/cites_suspension.rb b/app/models/trade_restrictions/cites_suspension.rb index 37cb68900..d32afb9e5 100644 --- a/app/models/trade_restrictions/cites_suspension.rb +++ b/app/models/trade_restrictions/cites_suspension.rb @@ -31,6 +31,7 @@ # class CitesSuspension < TradeRestriction + include Changable # Migrated to controller (Strong Parameters) # attr_accessible :start_notification_id, :end_notification_id, # :cites_suspension_confirmations_attributes, @@ -43,6 +44,7 @@ class CitesSuspension < TradeRestriction before_validation :handle_dates before_save :handle_current_flag accepts_nested_attributes_for :cites_suspension_confirmations + after_commit :async_downloads_cache_cleanup, on: :destroy def handle_dates self.publication_date = start_notification && start_notification.effective_at @@ -101,4 +103,10 @@ def self.search(query) all end end + + private + + def async_downloads_cache_cleanup + DownloadsCacheCleanupWorker.perform_async('cites_suspensions') + end end diff --git a/app/models/trade_restrictions/cites_suspension_observer.rb b/app/models/trade_restrictions/cites_suspension_observer.rb deleted file mode 100644 index d03815dd9..000000000 --- a/app/models/trade_restrictions/cites_suspension_observer.rb +++ /dev/null @@ -1,7 +0,0 @@ -class CitesSuspensionObserver < TradeRestrictionObserver - - def after_destroy(cites_suspension) - DownloadsCacheCleanupWorker.perform_async('cites_suspensions') - end - -end diff --git a/app/models/trade_restrictions/quota.rb b/app/models/trade_restrictions/quota.rb index cf1d0dcc4..be9709865 100644 --- a/app/models/trade_restrictions/quota.rb +++ b/app/models/trade_restrictions/quota.rb @@ -31,6 +31,7 @@ # class Quota < TradeRestriction + include Changable # Migrated to controller (Strong Parameters) # attr_accessible :public_display @@ -38,6 +39,8 @@ class Quota < TradeRestriction validates :quota, :numericality => { :greater_than_or_equal_to => -1.0 } validates :geo_entity_id, :presence => true + after_commit :async_downloads_cache_cleanup, on: :destroy + # Each element of CSV columns can be either an array [display_text, method] # or a single symbol if the display text and the method are the same CSV_COLUMNS = [ @@ -101,4 +104,10 @@ def self.count_matching(params) ] ).count end + + private + + def async_downloads_cache_cleanup + DownloadsCacheCleanupWorker.perform_async('quotas') + end end diff --git a/app/models/trade_restrictions/quota_observer.rb b/app/models/trade_restrictions/quota_observer.rb deleted file mode 100644 index 00110632f..000000000 --- a/app/models/trade_restrictions/quota_observer.rb +++ /dev/null @@ -1,7 +0,0 @@ -class QuotaObserver < TradeRestrictionObserver - - def after_destroy(quota) - DownloadsCacheCleanupWorker.perform_async('quotas') - end - -end diff --git a/app/models/user.rb b/app/models/user.rb index dbaf670f1..d35a095ea 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -24,6 +24,8 @@ # class User < ApplicationRecord + include Deletable + devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable # Migrated to controller (Strong Parameters) diff --git a/app/sweepers/purpose_sweeper.rb b/app/sweepers/purpose_sweeper.rb deleted file mode 100644 index 4bb7089ae..000000000 --- a/app/sweepers/purpose_sweeper.rb +++ /dev/null @@ -1,29 +0,0 @@ -class PurposeSweeper < ActionController::Caching::Sweeper - observe Purpose - - def after_create(tc) - expire_cache(tc) - end - - def after_update(tc) - expire_cache(tc) - end - - def after_destroy(tc) - expire_cache(tc) - end - - private - - def expire_cache(tc) - @controller ||= ActionController::Base.new - ["en", "fr", "es"].each do |lang| - expire_action( - controller: 'api/v1/purposes', - format: 'json', - action: 'index', - locale: lang - ) - end - end -end diff --git a/app/sweepers/source_sweeper.rb b/app/sweepers/source_sweeper.rb deleted file mode 100644 index 9cecd6bff..000000000 --- a/app/sweepers/source_sweeper.rb +++ /dev/null @@ -1,29 +0,0 @@ -class SourceSweeper < ActionController::Caching::Sweeper - observe Source - - def after_create(tc) - expire_cache(tc) - end - - def after_update(tc) - expire_cache(tc) - end - - def after_destroy(tc) - expire_cache(tc) - end - - private - - def expire_cache(tc) - @controller ||= ActionController::Base.new - ["en", "fr", "es"].each do |lang| - expire_action( - controller: 'api/v1/sources', - format: 'json', - action: 'index', - locale: lang - ) - end - end -end diff --git a/app/sweepers/term_sweeper.rb b/app/sweepers/term_sweeper.rb deleted file mode 100644 index ebfa4463f..000000000 --- a/app/sweepers/term_sweeper.rb +++ /dev/null @@ -1,29 +0,0 @@ -class TermSweeper < ActionController::Caching::Sweeper - observe Term - - def after_create(tc) - expire_cache(tc) - end - - def after_update(tc) - expire_cache(tc) - end - - def after_destroy(tc) - expire_cache(tc) - end - - private - - def expire_cache(tc) - @controller ||= ActionController::Base.new - ["en", "fr", "es"].each do |lang| - expire_action( - controller: 'api/v1/terms', - format: 'json', - action: 'index', - locale: lang - ) - end - end -end diff --git a/app/sweepers/unit_sweeper.rb b/app/sweepers/unit_sweeper.rb deleted file mode 100644 index f41d74e9d..000000000 --- a/app/sweepers/unit_sweeper.rb +++ /dev/null @@ -1,29 +0,0 @@ -class UnitSweeper < ActionController::Caching::Sweeper - observe Unit - - def after_create(tc) - expire_cache(tc) - end - - def after_update(tc) - expire_cache(tc) - end - - def after_destroy(tc) - expire_cache(tc) - end - - private - - def expire_cache(tc) - @controller ||= ActionController::Base.new - ["en", "fr", "es"].each do |lang| - expire_action( - controller: 'api/v1/units', - format: 'json', - action: 'index', - locale: lang - ) - end - end -end diff --git a/config/application.rb b/config/application.rb index 0cc5846e0..6a3cbb138 100644 --- a/config/application.rb +++ b/config/application.rb @@ -28,17 +28,6 @@ class Application < Rails::Application config.autoload_paths << Rails.root.join("lib", "modules") config.eager_load_paths << Rails.root.join("lib", "modules") - # Activate observers that should always be running. - config.active_record.observers = :destroy_observer, :annotation_observer, - :cites_cop_observer, :cites_suspension_notification_observer, - :eu_regulation_observer, :eu_suspension_regulation_observer, - :eu_event_observer, :"trade/annual_report_upload_observer", - :listing_change_observer, :taxon_concept_observer, - :cites_suspension_observer, :quota_observer, :eu_decision_observer, - :"trade/shipment_observer", :"trade/trade_data_download_observer", - :change_observer, :document_observer, :nomenclature_change_observer, - :geo_entity_observer - # Active Job config.active_job.queue_adapter = :sidekiq config.action_mailer.deliver_later_queue_name = 'default' From 4732d37a401bcd9a3ded55c9d46991b637a5add6 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Sun, 4 Feb 2024 17:56:39 +0000 Subject: [PATCH 156/241] break routes to multiple files --- config/routes.rb | 265 +------------------------------------ config/routes/admin.rb | 119 +++++++++++++++++ config/routes/api.rb | 37 ++++++ config/routes/checklist.rb | 26 ++++ config/routes/species.rb | 6 + config/routes/trade.rb | 31 +++++ 6 files changed, 224 insertions(+), 260 deletions(-) create mode 100644 config/routes/admin.rb create mode 100644 config/routes/api.rb create mode 100644 config/routes/checklist.rb create mode 100644 config/routes/species.rb create mode 100644 config/routes/trade.rb diff --git a/config/routes.rb b/config/routes.rb index 827761932..5a07e9156 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -21,229 +21,15 @@ get 'admin/api_usage/user_overview/:id' => 'admin/api_usage#show', :as => 'api_user_usage' mount Sidekiq::Web => '/sidekiq' - namespace :api do - namespace :v1 do - resources :taxon_concepts, :only => [:index, :show] - resources :auto_complete_taxon_concepts, :only => [:index, :show] - resources :geo_entities, :only => [:index] - resources :terms, :only => [:index] - resources :units, :only => [:index] - resources :sources, :only => [:index] - resources :purposes, :only => [:index] - resources :trade_plus_filters, only: :index - resources :eu_decisions, only: :index - resources :documents, :only => [:index, :show] do - collection do - get 'download_zip' - end - end - resources :document_geo_entities, only: [:index] - resources :events, only: [:index] - resources :document_tags, only: [:index] - get '/dashboard_stats/:iso_code' => 'dashboard_stats#index' - get '/shipments/chart' => 'shipments#chart_query' - get '/shipments/grouped' => 'shipments#grouped_query' - get '/shipments/over_time' => 'shipments#over_time_query' - get '/shipments/aggregated_over_time' => 'shipments#aggregated_over_time_query' - get '/shipments/country' => 'shipments#country_query' - get '/shipments/search' => 'shipments#search_query' - get '/shipments/download' => 'shipments#download_data' - get '/shipments/search_download' => 'shipments#search_download_data' - get '/shipments/search_download_all' => 'shipments#search_download_all_data' - end - resources :languages, :only => [:index] - resources :designations, :only => [:index] - resources :species_listings, :only => [:index] - resources :change_types, :only => [:index] - resources :ranks, :only => [:index] - resources :trade_downloads_cache_cleanup, only: [:index] - end - namespace :admin do - resources :taxonomies, :only => [:index, :create, :update, :destroy] - resources :terms, :only => [:index, :create, :update, :destroy] - resources :sources, :only => [:index, :create, :update, :destroy] - resources :purposes, :only => [:index, :create, :update, :destroy] - resources :units, :only => [:index, :create, :update, :destroy] - resources :taxon_concept_term_pairs, :only => [:index, :create, :update, :destroy] - resources :term_trade_codes_pairs, :only => [:index, :create, :update, :destroy] - resources :languages, :only => [:index, :create, :update, :destroy] - resources :users - resources :designations, :only => [:index, :create, :update, :destroy] - resources :instruments, :only => [:index, :create, :update, :destroy] - resources :species_listings, :only => [:index, :create, :update, :destroy] - resources :change_types, :only => [:index, :create, :update, :destroy] - resources :ranks, :only => [:index, :create, :update, :destroy] - resources :tags, :only => [:index, :create, :update, :destroy] - resources :eu_decision_types, :only => [:index, :create, :update, :destroy] - resources :srg_histories, only: [:index, :create, :update, :destroy] - resources :events do - resource :document_batch, :only => [:new, :create] - resources :documents, :only => [:index, :edit, :update, :destroy] do - get :show_order, on: :collection, controller: :event_documents - post :update_order, on: :collection, controller: :event_documents - end - end - resources :eu_regulations do - post :activate, :on => :member - post :deactivate, :on => :member - resources :listing_changes, :only => [:index, :destroy] - end - resources :eu_suspension_regulations do - post :activate, :on => :member - post :deactivate, :on => :member - resources :eu_suspensions, :only => [:index, :destroy] - end - resources :eu_implementing_regulations - resources :eu_council_regulations - resources :cites_cops - resources :cites_pcs - resources :cites_acs - resources :cites_tcs - resources :ec_srgs - resources :cites_extraordinary_meetings - resource :document_batch, :only => [:new, :create] - resources :documents do - get :autocomplete, :on => :collection - end - - resources :cites_suspension_notifications - resources :references, :only => [:index, :create, :update, :destroy] do - get :autocomplete, :on => :collection - end - resources :geo_entities, :only => [:index, :create, :update, :destroy] do - resources :geo_relationships, :only => [:index, :create, :update, :destroy] - end - resources :cites_hash_annotations, :only => [:index, :create, :update, :destroy] - resources :eu_hash_annotations, :only => [:index, :create, :update, :destroy] - resources :cites_suspensions, :only => [:index, :new, :create, :edit, :update, :destroy] - - resources :quotas, :only => [:index, :destroy] do - collection do - get :duplication - post :duplicate - get :count - end - end - - resources :iucn_mappings, :only => [:index] - resources :cms_mappings, :only => [:index] - resources :ahoy_visits, :only => [:index, :show] - resources :ahoy_events, :only => [:index, :show] - - resources :taxon_concepts do - get :autocomplete, :on => :collection - resources :children, :only => [:index] - resources :taxon_relationships, :only => [:index, :create, :destroy] - resources :comments, only: [:index, :create, :update], - controller: :taxon_concept_comments - resources :designations, :only => [] do - resources :taxon_listing_changes, :as => :listing_changes - end - resources :taxon_commons, :only => [:new, :create, :edit, :update, :destroy] - resources :distributions, :only => [:index, :new, :create, :edit, :update, :destroy] - resources :synonym_relationships, :only => [:new, :create, :edit, :update, :destroy] - resources :trade_name_relationships, :only => [:new, :create, :edit, :update, :destroy] - resources :hybrid_relationships, :only => [:new, :create, :edit, :update, :destroy] - resources :taxon_concept_references, :only => [:index, :new, :create, :destroy, :edit, :update] - resources :names, :only => [:index] - resources :eu_opinions, :only => [:index, :new, :create, :edit, :update, :destroy] - - resources :taxon_quotas, :only => [:index, :new, :create, :edit, :update, :destroy], - :as => :quotas - - resources :taxon_eu_suspensions, - :only => [:index, :new, :create, :edit, :update, :destroy], - :as => :eu_suspensions - - resources :taxon_cites_suspensions, - :only => [:index, :new, :create, :edit, :update, :destroy], - :as => :cites_suspensions - resources :taxon_instruments, :only => [:index, :new, :create, :edit, :update, :destroy] - resources :cites_captivity_processes, - :only => [:index, :new, :create, :edit, :update, :destroy] - end - resources :nomenclature_changes do # TODO: look like only support :index, :show, :destroy - resources :split, controller: 'nomenclature_changes/split' - resources :lump, controller: 'nomenclature_changes/lump' - resources :status_to_accepted, - controller: 'nomenclature_changes/status_to_accepted' - resources :status_to_synonym, - controller: 'nomenclature_changes/status_to_synonym' - resources :status_swap, controller: 'nomenclature_changes/status_swap' - end - get 'exports' => 'exports#index' - get 'exports/download' => 'exports#download' # not sure about this, post?? - get 'stats' => 'statistics#index' - root :to => 'taxon_concepts#index' - end + draw(:api) + draw(:admin) get 'trade/user_can_edit' => 'trade#user_can_edit' - namespace :trade do - resources :annual_report_uploads do - resources :sandbox_shipments do - collection do - post :update_batch - post :destroy_batch - end - end - member do - post 'submit' - end - end - resources :validation_rules - resources :validation_errors, only: [:update, :show] - resources :shipments do - collection do - post :update_batch - post :destroy_batch - get :accepted_taxa_for_reported_taxon_concept - end - end - resources :geo_entities, :only => [:index] - resources :permits, :only => [:index] - get 'exports/download' => 'exports#download' # not sure about this, post?? - get 'exports/download_stats' => 'exports#download_stats', :as => :trade_download_stats - get 'stats' => 'statistics#index' - get 'summary_year' => 'statistics#summary_year' - get 'summary_creation' => 'statistics#summary_creation' - get 'trade_transactions' => 'statistics#trade_transactions' - root :to => 'ember#start' - end + draw(:trade) - namespace :species do - get 'exports' => 'exports#index' - get 'exports/download' => 'exports#download' # not sure about this, post?? - get '*foo' => 'ember#start' - root :to => 'ember#start' - end - - namespace :checklist do - resources :geo_entities, :only => [:index] # TODO: move to API - resources :downloads do - member do - get :download - end - collection do - get :download_index - get :download_history - end - end - resources :taxon_concepts, :only => [:index] do - collection do - get :autocomplete - get :summarise_filters - end - end - resources :timelines, :only => [:index] - resources :documents do - collection do - get 'download_zip' - get 'volume_download' - get 'check_doc_presence' - end - end - end + draw(:species) + draw(:checklist) # The priority is based upon order of creation: # first created -> highest priority. @@ -258,47 +44,6 @@ root :to => 'home#index' end end - # Keep in mind you can assign values other than :controller and :action - - # Sample of named route: - # match 'products/:id/purchase' => 'catalog#purchase', :as => :purchase - # This route can be invoked with purchase_url(:id => product.id) - - # Sample resource route (maps HTTP verbs to controller actions automatically): - # resources :products - - # Sample resource route with options: - # resources :products do - # member do - # get 'short' - # post 'toggle' - # end - # - # collection do - # get 'sold' - # end - # end - - # Sample resource route with sub-resources: - # resources :products do - # resources :comments, :sales - # resource :seller - # end - - # Sample resource route with more complex sub-resources - # resources :products do - # resources :comments - # resources :sales do - # get 'recent', :on => :collection - # end - # end - - # Sample resource route within a namespace: - # namespace :admin do - # # Directs /admin/products/* to Admin::ProductsController - # # (app/controllers/admin/products_controller.rb) - # resources :products - # end get '/', :to => 'cites_trade/home#index', :constraints => lambda { |request| diff --git a/config/routes/admin.rb b/config/routes/admin.rb new file mode 100644 index 000000000..7589285a0 --- /dev/null +++ b/config/routes/admin.rb @@ -0,0 +1,119 @@ +namespace :admin do + resources :taxonomies, :only => [:index, :create, :update, :destroy] + resources :terms, :only => [:index, :create, :update, :destroy] + resources :sources, :only => [:index, :create, :update, :destroy] + resources :purposes, :only => [:index, :create, :update, :destroy] + resources :units, :only => [:index, :create, :update, :destroy] + resources :taxon_concept_term_pairs, :only => [:index, :create, :update, :destroy] + resources :term_trade_codes_pairs, :only => [:index, :create, :update, :destroy] + resources :languages, :only => [:index, :create, :update, :destroy] + resources :users + resources :designations, :only => [:index, :create, :update, :destroy] + resources :instruments, :only => [:index, :create, :update, :destroy] + resources :species_listings, :only => [:index, :create, :update, :destroy] + resources :change_types, :only => [:index, :create, :update, :destroy] + resources :ranks, :only => [:index, :create, :update, :destroy] + resources :tags, :only => [:index, :create, :update, :destroy] + resources :eu_decision_types, :only => [:index, :create, :update, :destroy] + resources :srg_histories, only: [:index, :create, :update, :destroy] + resources :events do + resource :document_batch, :only => [:new, :create] + resources :documents, :only => [:index, :edit, :update, :destroy] do + get :show_order, on: :collection, controller: :event_documents + post :update_order, on: :collection, controller: :event_documents + end + end + resources :eu_regulations do + post :activate, :on => :member + post :deactivate, :on => :member + resources :listing_changes, :only => [:index, :destroy] + end + resources :eu_suspension_regulations do + post :activate, :on => :member + post :deactivate, :on => :member + resources :eu_suspensions, :only => [:index, :destroy] + end + resources :eu_implementing_regulations + resources :eu_council_regulations + resources :cites_cops + resources :cites_pcs + resources :cites_acs + resources :cites_tcs + resources :ec_srgs + resources :cites_extraordinary_meetings + + resource :document_batch, :only => [:new, :create] + resources :documents do + get :autocomplete, :on => :collection + end + + resources :cites_suspension_notifications + resources :references, :only => [:index, :create, :update, :destroy] do + get :autocomplete, :on => :collection + end + resources :geo_entities, :only => [:index, :create, :update, :destroy] do + resources :geo_relationships, :only => [:index, :create, :update, :destroy] + end + resources :cites_hash_annotations, :only => [:index, :create, :update, :destroy] + resources :eu_hash_annotations, :only => [:index, :create, :update, :destroy] + resources :cites_suspensions, :only => [:index, :new, :create, :edit, :update, :destroy] + + resources :quotas, :only => [:index, :destroy] do + collection do + get :duplication + post :duplicate + get :count + end + end + + resources :iucn_mappings, :only => [:index] + resources :cms_mappings, :only => [:index] + resources :ahoy_visits, :only => [:index, :show] + resources :ahoy_events, :only => [:index, :show] + + resources :taxon_concepts do + get :autocomplete, :on => :collection + resources :children, :only => [:index] + resources :taxon_relationships, :only => [:index, :create, :destroy] + resources :comments, only: [:index, :create, :update], + controller: :taxon_concept_comments + resources :designations, :only => [] do + resources :taxon_listing_changes, :as => :listing_changes + end + resources :taxon_commons, :only => [:new, :create, :edit, :update, :destroy] + resources :distributions, :only => [:index, :new, :create, :edit, :update, :destroy] + resources :synonym_relationships, :only => [:new, :create, :edit, :update, :destroy] + resources :trade_name_relationships, :only => [:new, :create, :edit, :update, :destroy] + resources :hybrid_relationships, :only => [:new, :create, :edit, :update, :destroy] + resources :taxon_concept_references, :only => [:index, :new, :create, :destroy, :edit, :update] + resources :names, :only => [:index] + resources :eu_opinions, :only => [:index, :new, :create, :edit, :update, :destroy] + + resources :taxon_quotas, :only => [:index, :new, :create, :edit, :update, :destroy], + :as => :quotas + + resources :taxon_eu_suspensions, + :only => [:index, :new, :create, :edit, :update, :destroy], + :as => :eu_suspensions + + resources :taxon_cites_suspensions, + :only => [:index, :new, :create, :edit, :update, :destroy], + :as => :cites_suspensions + resources :taxon_instruments, :only => [:index, :new, :create, :edit, :update, :destroy] + resources :cites_captivity_processes, + :only => [:index, :new, :create, :edit, :update, :destroy] + end + resources :nomenclature_changes do # TODO: look like only support :index, :show, :destroy + resources :split, controller: 'nomenclature_changes/split' + resources :lump, controller: 'nomenclature_changes/lump' + resources :status_to_accepted, + controller: 'nomenclature_changes/status_to_accepted' + resources :status_to_synonym, + controller: 'nomenclature_changes/status_to_synonym' + resources :status_swap, controller: 'nomenclature_changes/status_swap' + end + get 'exports' => 'exports#index' + get 'exports/download' => 'exports#download' # not sure about this, post?? + get 'stats' => 'statistics#index' + root :to => 'taxon_concepts#index' +end diff --git a/config/routes/api.rb b/config/routes/api.rb new file mode 100644 index 000000000..eb5903a25 --- /dev/null +++ b/config/routes/api.rb @@ -0,0 +1,37 @@ +namespace :api do + namespace :v1 do + resources :taxon_concepts, :only => [:index, :show] + resources :auto_complete_taxon_concepts, :only => [:index, :show] + resources :geo_entities, :only => [:index] + resources :terms, :only => [:index] + resources :units, :only => [:index] + resources :sources, :only => [:index] + resources :purposes, :only => [:index] + resources :trade_plus_filters, only: :index + resources :eu_decisions, only: :index + resources :documents, :only => [:index, :show] do + collection do + get 'download_zip' + end + end + resources :document_geo_entities, only: [:index] + resources :events, only: [:index] + resources :document_tags, only: [:index] + get '/dashboard_stats/:iso_code' => 'dashboard_stats#index' + get '/shipments/chart' => 'shipments#chart_query' + get '/shipments/grouped' => 'shipments#grouped_query' + get '/shipments/over_time' => 'shipments#over_time_query' + get '/shipments/aggregated_over_time' => 'shipments#aggregated_over_time_query' + get '/shipments/country' => 'shipments#country_query' + get '/shipments/search' => 'shipments#search_query' + get '/shipments/download' => 'shipments#download_data' + get '/shipments/search_download' => 'shipments#search_download_data' + get '/shipments/search_download_all' => 'shipments#search_download_all_data' + end + resources :languages, :only => [:index] + resources :designations, :only => [:index] + resources :species_listings, :only => [:index] + resources :change_types, :only => [:index] + resources :ranks, :only => [:index] + resources :trade_downloads_cache_cleanup, only: [:index] +end diff --git a/config/routes/checklist.rb b/config/routes/checklist.rb new file mode 100644 index 000000000..b1ec693e0 --- /dev/null +++ b/config/routes/checklist.rb @@ -0,0 +1,26 @@ +namespace :checklist do + resources :geo_entities, :only => [:index] # TODO: move to API + resources :downloads do + member do + get :download + end + collection do + get :download_index + get :download_history + end + end + resources :taxon_concepts, :only => [:index] do + collection do + get :autocomplete + get :summarise_filters + end + end + resources :timelines, :only => [:index] + resources :documents do + collection do + get 'download_zip' + get 'volume_download' + get 'check_doc_presence' + end + end +end diff --git a/config/routes/species.rb b/config/routes/species.rb new file mode 100644 index 000000000..8fcf04457 --- /dev/null +++ b/config/routes/species.rb @@ -0,0 +1,6 @@ +namespace :species do + get 'exports' => 'exports#index' + get 'exports/download' => 'exports#download' # not sure about this, post?? + get '*foo' => 'ember#start' + root :to => 'ember#start' +end diff --git a/config/routes/trade.rb b/config/routes/trade.rb new file mode 100644 index 000000000..f6ea911c2 --- /dev/null +++ b/config/routes/trade.rb @@ -0,0 +1,31 @@ +namespace :trade do + resources :annual_report_uploads do + resources :sandbox_shipments do + collection do + post :update_batch + post :destroy_batch + end + end + member do + post 'submit' + end + end + resources :validation_rules + resources :validation_errors, only: [:update, :show] + resources :shipments do + collection do + post :update_batch + post :destroy_batch + get :accepted_taxa_for_reported_taxon_concept + end + end + resources :geo_entities, :only => [:index] + resources :permits, :only => [:index] + get 'exports/download' => 'exports#download' # not sure about this, post?? + get 'exports/download_stats' => 'exports#download_stats', :as => :trade_download_stats + get 'stats' => 'statistics#index' + get 'summary_year' => 'statistics#summary_year' + get 'summary_creation' => 'statistics#summary_creation' + get 'trade_transactions' => 'statistics#trade_transactions' + root :to => 'ember#start' +end From 74dc59177ec2520d2e245a06ed02d224c3503a3c Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Sun, 4 Feb 2024 17:57:04 +0000 Subject: [PATCH 157/241] update rubocop --- .rubocop.yml | 4 ++-- Dockerfile.cap-deploy | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index c7689fb2b..aee3d16aa 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -6,8 +6,8 @@ require: Rails: Enabled: true AllCops: - TargetRubyVersion: 2.6 - TargetRailsVersion: 5.1 + TargetRubyVersion: 3.0.6 + TargetRailsVersion: 6.0 Exclude: - 'lib/tasks/bbgem.rake' - 'db/schema.rb' diff --git a/Dockerfile.cap-deploy b/Dockerfile.cap-deploy index d88a2d0b6..88934654e 100644 --- a/Dockerfile.cap-deploy +++ b/Dockerfile.cap-deploy @@ -1,12 +1,10 @@ # Dockerfile -FROM ubuntu:jammy +FROM ruby:3.0.6 ENV DEBIAN_FRONTEND=noninteractive # Rails and SAPI has some additional dependencies, e.g. rake requires a JS # runtime, so attempt to get these from apt, where possible RUN apt-get update && apt-get install -y --force-yes \ - # SSH - openssh-client \ # For ruby? libsodium-dev libgmp3-dev \ # For RVM @@ -47,4 +45,4 @@ ENTRYPOINT ["/bin/bash", "-l"] # source /etc/profile.d/rvm.sh # eval "$(ssh-agent -s)" # ssh-add -# cap staging deploy +# CAP_BRANCH= cap staging deploy From eb4066e34fb874f15501f3e319851a67ba46abc2 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Sun, 4 Feb 2024 20:58:31 +0000 Subject: [PATCH 158/241] Try fix Ahoy after upgrade to v5.0.2 --- app/models/ahoy/visit.rb | 9 +++++++++ config/initializers/ahoy.rb | 35 +---------------------------------- 2 files changed, 10 insertions(+), 34 deletions(-) diff --git a/app/models/ahoy/visit.rb b/app/models/ahoy/visit.rb index 437e484fd..7bed551af 100644 --- a/app/models/ahoy/visit.rb +++ b/app/models/ahoy/visit.rb @@ -32,5 +32,14 @@ class Visit < ApplicationRecord has_many :ahoy_events, class_name: 'Ahoy::Event' belongs_to :user, optional: true serialize :properties, JSON + + # https://github.com/ankane/ahoy/issues/549 + # This project start using ahoy since version 1.0.1 + # The DB migration file come with version 1.0.1 create columns `id` and `visitor_id`. + # (https://github.com/ankane/ahoy/blob/v1.0.1/lib/generators/ahoy/stores/templates/active_record_visits_migration.rb) + # However it has changed since version 1.4.0, from `id` to `visit_token`, and from `visitor_id` to `visitor_token`. + # (https://github.com/ankane/ahoy/blob/v1.4.0/lib/generators/ahoy/stores/templates/active_record_visits_migration.rb) + alias_attribute :visit_token, :id + alias_attribute :visitor_token, :visitor_id end end diff --git a/config/initializers/ahoy.rb b/config/initializers/ahoy.rb index e82c8e65d..648565ff4 100644 --- a/config/initializers/ahoy.rb +++ b/config/initializers/ahoy.rb @@ -1,44 +1,11 @@ require 'sapi/geo_i_p' class Ahoy::Store < Ahoy::DatabaseStore - UUID_NAMESPACE = UUIDTools::UUID.parse("dcd74c26-8fc9-453a-a9c2-afc445c3258d") - def authenticate(data) # https://github.com/ankane/ahoy/tree/v5.0.2?tab=readme-ov-file#gdpr-compliance-1 # disables automatic linking of visits and users (for GDPR compliance) end - def visit_model - Ahoy::Visit - end - - def event_model - Ahoy::Event - end - - # https://github.com/ankane/ahoy/issues/549 - # This project start using ahoy since version 1.0.1 - # The DB migration file come with version 1.0.1 create columns `id` and `visitor_id`. - # (https://github.com/ankane/ahoy/blob/v1.0.1/lib/generators/ahoy/stores/templates/active_record_visits_migration.rb) - # However it has changed since version 1.4.0, from `id` to `visit_token`, and from `visitor_id` to `visitor_token`. - # (https://github.com/ankane/ahoy/blob/v1.4.0/lib/generators/ahoy/stores/templates/active_record_visits_migration.rb) - # When we upgrade this gem, we need to check the source code of Ahoy::DatabaseStore, - # (5.0.2 as reference: https://github.com/ankane/ahoy/blob/v5.0.2/lib/ahoy/database_store.rb) - # see how the latest `visit` method look like, and override it with the old column names. - def visit - unless defined?(@visit) - if ahoy.send(:existing_visit_token) || ahoy.instance_variable_get(:@visit_token) - # find_by raises error by default with Mongoid when not found - @visit = visit_model.where(id: ensure_uuid(ahoy.visit_token)).take if ahoy.visit_token - elsif !Ahoy.cookies? && ahoy.visitor_token - @visit = visit_model.where(visitor_id: ensure_uuid(ahoy.visitor_token)).where(started_at: Ahoy.visit_duration.ago..).order(started_at: :desc).first - else - @visit = nil - end - end - @visit - end - def track_visit(data) # Map the new column names (since 1.4.0), to old column name (< 1.4.0). data[:id] = ensure_uuid(data.delete(:visit_token)) @@ -61,7 +28,7 @@ def track_event(data) def ensure_uuid(id) UUIDTools::UUID.parse(id).to_s rescue - UUIDTools::UUID.sha1_create(UUID_NAMESPACE, id).to_s + UUIDTools::UUID.sha1_create(UUIDTools::UUID.parse(Ahoy::Tracker::UUID_NAMESPACE), id).to_s end end From 78e23f79bf3494b2c1b6afcf7206c39b26e3f01c Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Sun, 4 Feb 2024 20:59:18 +0000 Subject: [PATCH 159/241] enable 6.1 default --- .../0_new_framework_defaults_6_1.rb | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/config/initializers/0_new_framework_defaults_6_1.rb b/config/initializers/0_new_framework_defaults_6_1.rb index 9526b835a..2edfdc842 100644 --- a/config/initializers/0_new_framework_defaults_6_1.rb +++ b/config/initializers/0_new_framework_defaults_6_1.rb @@ -7,61 +7,58 @@ # Read the Guide for Upgrading Ruby on Rails for more info on each option. # Support for inversing belongs_to -> has_many Active Record associations. -# Rails.application.config.active_record.has_many_inversing = true +Rails.application.config.active_record.has_many_inversing = true # Track Active Storage variants in the database. -# Rails.application.config.active_storage.track_variants = true +Rails.application.config.active_storage.track_variants = true # Apply random variation to the delay when retrying failed jobs. -# Rails.application.config.active_job.retry_jitter = 0.15 +Rails.application.config.active_job.retry_jitter = 0.15 # Stop executing `after_enqueue`/`after_perform` callbacks if # `before_enqueue`/`before_perform` respectively halts with `throw :abort`. -# Rails.application.config.active_job.skip_after_callbacks_if_terminated = true +Rails.application.config.active_job.skip_after_callbacks_if_terminated = true # Specify cookies SameSite protection level: either :none, :lax, or :strict. # # This change is not backwards compatible with earlier Rails versions. # It's best enabled when your entire app is migrated and stable on 6.1. -# Rails.application.config.action_dispatch.cookies_same_site_protection = :lax +Rails.application.config.action_dispatch.cookies_same_site_protection = :lax # Generate CSRF tokens that are encoded in URL-safe Base64. # # This change is not backwards compatible with earlier Rails versions. # It's best enabled when your entire app is migrated and stable on 6.1. -# Rails.application.config.action_controller.urlsafe_csrf_tokens = true +Rails.application.config.action_controller.urlsafe_csrf_tokens = true # Specify whether `ActiveSupport::TimeZone.utc_to_local` returns a time with an # UTC offset or a UTC time. -# ActiveSupport.utc_to_local_returns_utc_offset_times = true +ActiveSupport.utc_to_local_returns_utc_offset_times = true # Change the default HTTP status code to `308` when redirecting non-GET/HEAD # requests to HTTPS in `ActionDispatch::SSL` middleware. -# Rails.application.config.action_dispatch.ssl_default_redirect_status = 308 +Rails.application.config.action_dispatch.ssl_default_redirect_status = 308 # Use new connection handling API. For most applications this won't have any # effect. For applications using multiple databases, this new API provides # support for granular connection swapping. -# Rails.application.config.active_record.legacy_connection_handling = false +Rails.application.config.active_record.legacy_connection_handling = false # Make `form_with` generate non-remote forms by default. -# Rails.application.config.action_view.form_with_generates_remote_forms = false +Rails.application.config.action_view.form_with_generates_remote_forms = false # Set the default queue name for the analysis job to the queue adapter default. -# Rails.application.config.active_storage.queues.analysis = nil +Rails.application.config.active_storage.queues.analysis = nil # Set the default queue name for the purge job to the queue adapter default. -# Rails.application.config.active_storage.queues.purge = nil +Rails.application.config.active_storage.queues.purge = nil # Set the default queue name for the incineration job to the queue adapter default. -# Rails.application.config.action_mailbox.queues.incineration = nil +Rails.application.config.action_mailbox.queues.incineration = nil # Set the default queue name for the routing job to the queue adapter default. -# Rails.application.config.action_mailbox.queues.routing = nil - -# Set the default queue name for the mail deliver job to the queue adapter default. -# Rails.application.config.action_mailer.deliver_later_queue_name = nil +Rails.application.config.action_mailbox.queues.routing = nil # Generate a `Link` header that gives a hint to modern browsers about # preloading assets when using `javascript_include_tag` and `stylesheet_link_tag`. -# Rails.application.config.action_view.preload_links_header = true +Rails.application.config.action_view.preload_links_header = true From 29a8474f84aaa761228182ac5cd77cbc8f64be5f Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Sun, 4 Feb 2024 21:02:14 +0000 Subject: [PATCH 160/241] add comment --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 50108cb66..dddd5f895 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } -ruby '3.0.6' +ruby '3.0.6' # Can't upgrade to 3.1 until Rails 7.0.4 (https://stackoverflow.com/a/75007303/556780) # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' gem 'rails', '6.1.7.6' From c26ac073c2db67c802f8401fbdbc89608b2dc574 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Sun, 4 Feb 2024 21:19:11 +0000 Subject: [PATCH 161/241] Move TaxonConceptVersion to model, see if it can fix cap deploy (uninitialized constant PaperTrail::Version) --- .../paper_trail.rb => app/models/taxon_concept_version.rb | 2 -- 1 file changed, 2 deletions(-) rename config/initializers/paper_trail.rb => app/models/taxon_concept_version.rb (89%) diff --git a/config/initializers/paper_trail.rb b/app/models/taxon_concept_version.rb similarity index 89% rename from config/initializers/paper_trail.rb rename to app/models/taxon_concept_version.rb index ee89a658f..3b5f72b76 100644 --- a/config/initializers/paper_trail.rb +++ b/app/models/taxon_concept_version.rb @@ -1,5 +1,3 @@ -# config/initializers/paper_trail.rb - class TaxonConceptVersion < PaperTrail::Version # Migrated to Strong Parameters # attr_accessible :taxon_concept_id, From ab7a25b6dc1d895b0ed7dae99aac5aa2c031f625 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Sun, 4 Feb 2024 21:29:26 +0000 Subject: [PATCH 162/241] rollback Sacc-rails gem --- Gemfile | 9 ++++++++- Gemfile.lock | 18 +++++++----------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/Gemfile b/Gemfile index dddd5f895..d43b4f749 100644 --- a/Gemfile +++ b/Gemfile @@ -9,8 +9,15 @@ gem 'rails', '6.1.7.6' # gem 'sqlite3' # Use Puma as the app server gem 'puma', '~> 5.0' + # Use SCSS for stylesheets -gem 'sass-rails', '>= 6' +# TODO: Can't upgrade sass-rails to 6.0, it raise the following error when running `RAILS_ENV=staging rake assets:precompile`. +# SassC::SyntaxError: Error: Invalid CSS after "...in-bottom:-3px;": expected "}", was ".margin-bottom:-3px" +# on line 3712:5063 of stdin +# >> ction=135,Strength=3)";_margin-bottom:-3px;.margin-bottom:-3px;}/*!Add round +# gem 'sass-rails', '>= 6' +gem 'sass-rails', '~> 5.0' + # https://stackoverflow.com/questions/55213868/rails-6-how-to-disable-webpack-and-use-sprockets-instead gem 'sprockets', '3.7.2' gem 'sprockets-rails', :require => 'sprockets/railtie' diff --git a/Gemfile.lock b/Gemfile.lock index 0cb6203d6..8d9314c52 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -475,16 +475,12 @@ GEM rubyzip (2.3.2) safely_block (0.4.0) sass (3.4.25) - sass-rails (6.0.0) - sassc-rails (~> 2.1, >= 2.1.1) - sassc (2.4.0) - ffi (~> 1.9) - sassc-rails (2.1.2) - railties (>= 4.0.0) - sassc (>= 2.0) - sprockets (> 3.0) - sprockets-rails - tilt + sass-rails (5.1.0) + railties (>= 5.2.0) + sass (~> 3.1) + sprockets (>= 2.8, < 4.0) + sprockets-rails (>= 2.0, < 4.0) + tilt (>= 1.1, < 3) selenium-webdriver (4.9.0) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) @@ -650,7 +646,7 @@ DEPENDENCIES rubocop-rails rubocop-rspec rubyzip (~> 2.3, >= 2.3.2) - sass-rails (>= 6) + sass-rails (~> 5.0) selenium-webdriver (>= 4.0.0.rc1) sidekiq (< 7) sidekiq-cron (~> 1.12) From 1b0f8763d6772d4d7cb025fabe62194262e3060d Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Mon, 5 Feb 2024 10:19:47 +0000 Subject: [PATCH 163/241] Rollback to jquery_ujs instead of rails-ujs which introduce in 5.1 --- app/assets/javascripts/admin.js | 2 +- app/assets/javascripts/application.js | 2 +- app/assets/javascripts/cites_trade.js | 2 +- app/assets/javascripts/pages.js | 2 +- app/assets/javascripts/species.js | 2 +- app/assets/javascripts/trade.js | 2 +- vendor/assets/javascripts/jquery_ujs.js | 429 ++++++++++++++++++++++++ 7 files changed, 435 insertions(+), 6 deletions(-) create mode 100644 vendor/assets/javascripts/jquery_ujs.js diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js index e5cdde110..53fc1b22f 100644 --- a/app/assets/javascripts/admin.js +++ b/app/assets/javascripts/admin.js @@ -11,7 +11,7 @@ // GO AFTER THE REQUIRES BELOW. // //= require jquery -//= require rails-ujs +//= require jquery_ujs //= require bootstrap //= require bootstrap-editable //= require bootstrap-datepicker diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index e211b36c9..1deab2f65 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -11,7 +11,7 @@ // GO AFTER THE REQUIRES BELOW. // //= require jquery -//= require rails-ujs +//= require jquery_ujs //= require jquery.chained //= require bootstrap-dropdown //= require bootstrap-button diff --git a/app/assets/javascripts/cites_trade.js b/app/assets/javascripts/cites_trade.js index 48471efe4..b14f0e769 100644 --- a/app/assets/javascripts/cites_trade.js +++ b/app/assets/javascripts/cites_trade.js @@ -1,6 +1,6 @@ //= require underscore //= require jquery -//= require rails-ujs +//= require jquery_ujs //= require cites_trade/jquery-ui-1.10.4.custom //= require jquery-deparam //= require js.cookie diff --git a/app/assets/javascripts/pages.js b/app/assets/javascripts/pages.js index e42d23518..a71c20657 100644 --- a/app/assets/javascripts/pages.js +++ b/app/assets/javascripts/pages.js @@ -12,7 +12,7 @@ // //= require underscore //= require jquery -//= require rails-ujs +//= require jquery_ujs //= require_self //= require_tree ./pages //= require ./shared/login diff --git a/app/assets/javascripts/species.js b/app/assets/javascripts/species.js index b24a23296..0d6f0b3e8 100644 --- a/app/assets/javascripts/species.js +++ b/app/assets/javascripts/species.js @@ -12,7 +12,7 @@ // //= require underscore //= require jquery -//= require rails-ujs +//= require jquery_ujs //= require jquery-deparam //= require js.cookie //= require bootstrap-scrollspy diff --git a/app/assets/javascripts/trade.js b/app/assets/javascripts/trade.js index cee949934..f9f50dd78 100644 --- a/app/assets/javascripts/trade.js +++ b/app/assets/javascripts/trade.js @@ -11,7 +11,7 @@ // GO AFTER THE REQUIRES BELOW. // //= require jquery -//= require rails-ujs +//= require jquery_ujs //= require jquery-deparam //= require jquery-ui //= require jquery.mousewheel diff --git a/vendor/assets/javascripts/jquery_ujs.js b/vendor/assets/javascripts/jquery_ujs.js new file mode 100644 index 000000000..1df5eb5bd --- /dev/null +++ b/vendor/assets/javascripts/jquery_ujs.js @@ -0,0 +1,429 @@ +(function ($, undefined) { + + /** + * Unobtrusive scripting adapter for jQuery + * + * Requires jQuery 1.6.0 or later. + * https://github.com/rails/jquery-ujs + + * Uploading file using rails.js + * ============================= + * + * By default, browsers do not allow files to be uploaded via AJAX. As a result, if there are any non-blank file fields + * in the remote form, this adapter aborts the AJAX submission and allows the form to submit through standard means. + * + * The `ajax:aborted:file` event allows you to bind your own handler to process the form submission however you wish. + * + * Ex: + * $('form').live('ajax:aborted:file', function(event, elements){ + * // Implement own remote file-transfer handler here for non-blank file inputs passed in `elements`. + * // Returning false in this handler tells rails.js to disallow standard form submission + * return false; + * }); + * + * The `ajax:aborted:file` event is fired when a file-type input is detected with a non-blank value. + * + * Third-party tools can use this hook to detect when an AJAX file upload is attempted, and then use + * techniques like the iframe method to upload the file instead. + * + * Required fields in rails.js + * =========================== + * + * If any blank required inputs (required="required") are detected in the remote form, the whole form submission + * is canceled. Note that this is unlike file inputs, which still allow standard (non-AJAX) form submission. + * + * The `ajax:aborted:required` event allows you to bind your own handler to inform the user of blank required inputs. + * + * !! Note that Opera does not fire the form's submit event if there are blank required inputs, so this event may never + * get fired in Opera. This event is what causes other browsers to exhibit the same submit-aborting behavior. + * + * Ex: + * $('form').live('ajax:aborted:required', function(event, elements){ + * // Returning false in this handler tells rails.js to submit the form anyway. + * // The blank required inputs are passed to this function in `elements`. + * return ! confirm("Would you like to submit the form with missing info?"); + * }); + */ + + // Cut down on the number if issues from people inadvertently including jquery_ujs twice + // by detecting and raising an error when it happens. + var alreadyInitialized = function () { + var events = $._data(document, 'events'); + return events && events.click && $.grep(events.click, function (e) { return e.namespace === 'rails'; }).length; + } + + if (alreadyInitialized()) { + $.error('jquery-ujs has already been loaded!'); + } + + // Shorthand to make it a little easier to call public rails functions from within rails.js + var rails; + + $.rails = rails = { + // Link elements bound by jquery-ujs + linkClickSelector: 'a[data-confirm], a[data-method], a[data-remote], a[data-disable-with]', + + // Select elements bound by jquery-ujs + inputChangeSelector: 'select[data-remote], input[data-remote], textarea[data-remote]', + + // Form elements bound by jquery-ujs + formSubmitSelector: 'form', + + // Form input elements bound by jquery-ujs + formInputClickSelector: 'form input[type=submit], form input[type=image], form button[type=submit], form button:not([type])', + + // Form input elements disabled during form submission + disableSelector: 'input[data-disable-with], button[data-disable-with], textarea[data-disable-with]', + + // Form input elements re-enabled after form submission + enableSelector: 'input[data-disable-with]:disabled, button[data-disable-with]:disabled, textarea[data-disable-with]:disabled', + + // Form required input elements + requiredInputSelector: 'input[name][required]:not([disabled]),textarea[name][required]:not([disabled])', + + // Form file input elements + fileInputSelector: 'input:file', + + // Link onClick disable selector with possible reenable after remote submission + linkDisableSelector: 'a[data-disable-with]', + + // Make sure that every Ajax request sends the CSRF token + CSRFProtection: function (xhr) { + var token = $('meta[name="csrf-token"]').attr('content'); + if (token) xhr.setRequestHeader('X-CSRF-Token', token); + }, + + // Triggers an event on an element and returns false if the event result is false + fire: function (obj, name, data) { + var event = $.Event(name); + obj.trigger(event, data); + return event.result !== false; + }, + + // Default confirm dialog, may be overridden with custom confirm dialog in $.rails.confirm + confirm: function (message) { + return confirm(message); + }, + + // Default ajax function, may be overridden with custom function in $.rails.ajax + ajax: function (options) { + return $.ajax(options); + }, + + // Default way to get an element's href. May be overridden at $.rails.href. + href: function (element) { + return element.attr('href'); + }, + + // Submits "remote" forms and links with ajax + handleRemote: function (element) { + var method, url, data, elCrossDomain, crossDomain, withCredentials, dataType, options; + + if (rails.fire(element, 'ajax:before')) { + elCrossDomain = element.data('cross-domain'); + crossDomain = elCrossDomain === undefined ? null : elCrossDomain; + withCredentials = element.data('with-credentials') || null; + dataType = element.data('type') || ($.ajaxSettings && $.ajaxSettings.dataType); + + if (element.is('form')) { + method = element.attr('method'); + url = element.attr('action'); + data = element.serializeArray(); + // memoized value from clicked submit button + var button = element.data('ujs:submit-button'); + if (button) { + data.push(button); + element.data('ujs:submit-button', null); + } + } else if (element.is(rails.inputChangeSelector)) { + method = element.data('method'); + url = element.data('url'); + data = element.serialize(); + if (element.data('params')) data = data + "&" + element.data('params'); + } else { + method = element.data('method'); + url = rails.href(element); + data = element.data('params') || null; + } + + options = { + type: method || 'GET', data: data, dataType: dataType, + // stopping the "ajax:beforeSend" event will cancel the ajax request + beforeSend: function (xhr, settings) { + if (settings.dataType === undefined) { + xhr.setRequestHeader('accept', '*/*;q=0.5, ' + settings.accepts.script); + } + return rails.fire(element, 'ajax:beforeSend', [xhr, settings]); + }, + success: function (data, status, xhr) { + element.trigger('ajax:success', [data, status, xhr]); + }, + complete: function (xhr, status) { + element.trigger('ajax:complete', [xhr, status]); + }, + error: function (xhr, status, error) { + element.trigger('ajax:error', [xhr, status, error]); + }, + xhrFields: { + withCredentials: withCredentials + }, + crossDomain: crossDomain + }; + // Only pass url to `ajax` options if not blank + if (url) { options.url = url; } + + var jqxhr = rails.ajax(options); + element.trigger('ajax:send', jqxhr); + return jqxhr; + } else { + return false; + } + }, + + // Handles "data-method" on links such as: + // Delete + handleMethod: function (link) { + var href = rails.href(link), + method = link.data('method'), + target = link.attr('target'), + csrf_token = $('meta[name=csrf-token]').attr('content'), + csrf_param = $('meta[name=csrf-param]').attr('content'), + form = $('
    '), + metadata_input = ''; + + if (csrf_param !== undefined && csrf_token !== undefined) { + metadata_input += ''; + } + + if (target) { form.attr('target', target); } + + form.hide().append(metadata_input).appendTo('body'); + form.submit(); + }, + + /* Disables form elements: + - Caches element value in 'ujs:enable-with' data store + - Replaces element text with value of 'data-disable-with' attribute + - Sets disabled property to true + */ + disableFormElements: function (form) { + form.find(rails.disableSelector).each(function () { + var element = $(this), method = element.is('button') ? 'html' : 'val'; + element.data('ujs:enable-with', element[method]()); + element[method](element.data('disable-with')); + element.prop('disabled', true); + }); + }, + + /* Re-enables disabled form elements: + - Replaces element text with cached value from 'ujs:enable-with' data store (created in `disableFormElements`) + - Sets disabled property to false + */ + enableFormElements: function (form) { + form.find(rails.enableSelector).each(function () { + var element = $(this), method = element.is('button') ? 'html' : 'val'; + if (element.data('ujs:enable-with')) element[method](element.data('ujs:enable-with')); + element.prop('disabled', false); + }); + }, + + /* For 'data-confirm' attribute: + - Fires `confirm` event + - Shows the confirmation dialog + - Fires the `confirm:complete` event + + Returns `true` if no function stops the chain and user chose yes; `false` otherwise. + Attaching a handler to the element's `confirm` event that returns a `falsy` value cancels the confirmation dialog. + Attaching a handler to the element's `confirm:complete` event that returns a `falsy` value makes this function + return false. The `confirm:complete` event is fired whether or not the user answered true or false to the dialog. + */ + allowAction: function (element) { + var message = element.data('confirm'), + answer = false, callback; + if (!message) { return true; } + + if (rails.fire(element, 'confirm')) { + answer = rails.confirm(message); + callback = rails.fire(element, 'confirm:complete', [answer]); + } + return answer && callback; + }, + + // Helper function which checks for blank inputs in a form that match the specified CSS selector + blankInputs: function (form, specifiedSelector, nonBlank) { + var inputs = $(), input, valueToCheck, + selector = specifiedSelector || 'input,textarea', + allInputs = form.find(selector); + + allInputs.each(function () { + input = $(this); + valueToCheck = input.is(':checkbox,:radio') ? input.is(':checked') : input.val(); + // If nonBlank and valueToCheck are both truthy, or nonBlank and valueToCheck are both falsey + if (!valueToCheck === !nonBlank) { + + // Don't count unchecked required radio if other radio with same name is checked + if (input.is(':radio') && allInputs.filter('input:radio:checked[name="' + input.attr('name') + '"]').length) { + return true; // Skip to next input + } + + inputs = inputs.add(input); + } + }); + return inputs.length ? inputs : false; + }, + + // Helper function which checks for non-blank inputs in a form that match the specified CSS selector + nonBlankInputs: function (form, specifiedSelector) { + return rails.blankInputs(form, specifiedSelector, true); // true specifies nonBlank + }, + + // Helper function, needed to provide consistent behavior in IE + stopEverything: function (e) { + $(e.target).trigger('ujs:everythingStopped'); + e.stopImmediatePropagation(); + return false; + }, + + // find all the submit events directly bound to the form and + // manually invoke them. If anyone returns false then stop the loop + callFormSubmitBindings: function (form, event) { + var events = form.data('events'), continuePropagation = true; + if (events !== undefined && events['submit'] !== undefined) { + $.each(events['submit'], function (i, obj) { + if (typeof obj.handler === 'function') return continuePropagation = obj.handler(event); + }); + } + return continuePropagation; + }, + + // replace element's html with the 'data-disable-with' after storing original html + // and prevent clicking on it + disableElement: function (element) { + element.data('ujs:enable-with', element.html()); // store enabled state + element.html(element.data('disable-with')); // set to disabled state + element.bind('click.railsDisable', function (e) { // prevent further clicking + return rails.stopEverything(e); + }); + }, + + // restore element to its original state which was disabled by 'disableElement' above + enableElement: function (element) { + if (element.data('ujs:enable-with') !== undefined) { + element.html(element.data('ujs:enable-with')); // set to old enabled state + // this should be element.removeData('ujs:enable-with') + // but, there is currently a bug in jquery which makes hyphenated data attributes not get removed + element.data('ujs:enable-with', false); // clean up cache + } + element.unbind('click.railsDisable'); // enable element + } + + }; + + if (rails.fire($(document), 'rails:attachBindings')) { + + $.ajaxPrefilter(function (options, originalOptions, xhr) { if (!options.crossDomain) { rails.CSRFProtection(xhr); } }); + + $(document).delegate(rails.linkDisableSelector, 'ajax:complete', function () { + rails.enableElement($(this)); + }); + + $(document).delegate(rails.linkClickSelector, 'click.rails', function (e) { + var link = $(this), method = link.data('method'), data = link.data('params'); + if (!rails.allowAction(link)) return rails.stopEverything(e); + + if (link.is(rails.linkDisableSelector)) rails.disableElement(link); + + if (link.data('remote') !== undefined) { + if ((e.metaKey || e.ctrlKey) && (!method || method === 'GET') && !data) { return true; } + + var handleRemote = rails.handleRemote(link); + // response from rails.handleRemote() will either be false or a deferred object promise. + if (handleRemote === false) { + rails.enableElement(link); + } else { + handleRemote.error(function () { rails.enableElement(link); }); + } + return false; + + } else if (link.data('method')) { + rails.handleMethod(link); + return false; + } + }); + + $(document).delegate(rails.inputChangeSelector, 'change.rails', function (e) { + var link = $(this); + if (!rails.allowAction(link)) return rails.stopEverything(e); + + rails.handleRemote(link); + return false; + }); + + $(document).delegate(rails.formSubmitSelector, 'submit.rails', function (e) { + var form = $(this), + remote = form.data('remote') !== undefined, + blankRequiredInputs = rails.blankInputs(form, rails.requiredInputSelector), + nonBlankFileInputs = rails.nonBlankInputs(form, rails.fileInputSelector); + + if (!rails.allowAction(form)) return rails.stopEverything(e); + + // skip other logic when required values are missing or file upload is present + if (blankRequiredInputs && form.attr("novalidate") == undefined && rails.fire(form, 'ajax:aborted:required', [blankRequiredInputs])) { + return rails.stopEverything(e); + } + + if (remote) { + if (nonBlankFileInputs) { + // slight timeout so that the submit button gets properly serialized + // (make it easy for event handler to serialize form without disabled values) + setTimeout(function () { rails.disableFormElements(form); }, 13); + var aborted = rails.fire(form, 'ajax:aborted:file', [nonBlankFileInputs]); + + // re-enable form elements if event bindings return false (canceling normal form submission) + if (!aborted) { setTimeout(function () { rails.enableFormElements(form); }, 13); } + + return aborted; + } + + // If browser does not support submit bubbling, then this live-binding will be called before direct + // bindings. Therefore, we should directly call any direct bindings before remotely submitting form. + if (!$.support.submitBubbles && $().jquery < '1.7' && rails.callFormSubmitBindings(form, e) === false) return rails.stopEverything(e); + + rails.handleRemote(form); + return false; + + } else { + // slight timeout so that the submit button gets properly serialized + setTimeout(function () { rails.disableFormElements(form); }, 13); + } + }); + + $(document).delegate(rails.formInputClickSelector, 'click.rails', function (event) { + var button = $(this); + + if (!rails.allowAction(button)) return rails.stopEverything(event); + + // register the pressed submit button + var name = button.attr('name'), + data = name ? { name: name, value: button.val() } : null; + + button.closest('form').data('ujs:submit-button', data); + }); + + $(document).delegate(rails.formSubmitSelector, 'ajax:beforeSend.rails', function (event) { + if (this == event.target) rails.disableFormElements($(this)); + }); + + $(document).delegate(rails.formSubmitSelector, 'ajax:complete.rails', function (event) { + if (this == event.target) rails.enableFormElements($(this)); + }); + + $(function () { + // making sure that all forms have actual up-to-date token(cached forms contain old one) + csrf_token = $('meta[name=csrf-token]').attr('content'); + csrf_param = $('meta[name=csrf-param]').attr('content'); + $('form input[name="' + csrf_param + '"]').val(csrf_token); + }); + } + +})(jQuery); From 1a2fb574f46387d328cf827b98a1cbc6dc2087d3 Mon Sep 17 00:00:00 2001 From: Daniel Perrett Date: Mon, 5 Feb 2024 13:50:06 +0000 Subject: [PATCH 164/241] fix: another example of order where Arel::Sql is required to wrap literal --- .../api/v1/eu_decisions_controller.rb | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/app/controllers/api/v1/eu_decisions_controller.rb b/app/controllers/api/v1/eu_decisions_controller.rb index 25e5ee695..b6a37e07f 100644 --- a/app/controllers/api/v1/eu_decisions_controller.rb +++ b/app/controllers/api/v1/eu_decisions_controller.rb @@ -16,14 +16,20 @@ def permitted_params end def eu_decision_search(params) - list = EuDecision.from('api_eu_decisions_view AS eu_decisions'). - select(eu_decision_select_attrs). - joins('LEFT JOIN eu_suspensions_applicability_view v ON eu_decisions.id = v.id'). - order(<<-SQL - geo_entity_en->>'name' ASC, - start_date DESC + list = EuDecision.from( + 'api_eu_decisions_view AS eu_decisions' + ).select( + eu_decision_select_attrs + ).joins( + 'LEFT JOIN eu_suspensions_applicability_view v ON eu_decisions.id = v.id' + ).order( + Arel.sql( + <<-SQL + geo_entity_en->>'name' ASC, + start_date DESC SQL ) + ) list = list.where("eu_decisions.taxon_concept_id IN (?)", params['taxon_concept_ids']) if params['taxon_concept_ids'].present? list = list.where("eu_decisions.geo_entity_id IN (?)", params['geo_entity_ids']) if params['geo_entity_ids'].present? From 2baef35dfa8b528bb4a37467eb5c1d60291463f0 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Mon, 5 Feb 2024 16:08:58 +0000 Subject: [PATCH 165/241] ActiveRecord::UnknownAttributeReference --- app/serializers/species/show_taxon_concept_serializer_cms.rb | 4 ++-- app/services/checklist/csv/history.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/serializers/species/show_taxon_concept_serializer_cms.rb b/app/serializers/species/show_taxon_concept_serializer_cms.rb index a9bc8a863..33d9cb50c 100644 --- a/app/serializers/species/show_taxon_concept_serializer_cms.rb +++ b/app/serializers/species/show_taxon_concept_serializer_cms.rb @@ -72,11 +72,11 @@ def cms_listing_changes END AS subspecies_info SQL ). - order(<<-SQL + order(Arel.sql(<<-SQL effective_at DESC, subspecies_info DESC SQL - ).all + )).all end def cms_instruments diff --git a/app/services/checklist/csv/history.rb b/app/services/checklist/csv/history.rb index db81d0377..0d7eae98e 100644 --- a/app/services/checklist/csv/history.rb +++ b/app/services/checklist/csv/history.rb @@ -10,7 +10,7 @@ def prepare_main_query :"cites_listing_changes_mview.show_in_downloads" => true ). joins('LEFT JOIN geo_entities ON cites_listing_changes_mview.party_id = geo_entities.id'). - order(<<-SQL + order(Arel.sql(<<-SQL taxonomic_position, effective_at, CASE WHEN change_type_name = 'ADDITION' THEN 0 @@ -19,7 +19,7 @@ def prepare_main_query WHEN change_type_name = 'DELETION' THEN 3 END SQL - ) + )) end def select_columns From 07c58b989d7aed0fc496c2e92709917eeff95128 Mon Sep 17 00:00:00 2001 From: Daniel Perrett Date: Mon, 5 Feb 2024 16:17:08 +0000 Subject: [PATCH 166/241] fix: split rebuild job into daily and weekly, as we only want to run it weekly on production --- app/jobs/rebuild_job.rb | 7 +------ config/schedule.yml | 11 +++++++++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/app/jobs/rebuild_job.rb b/app/jobs/rebuild_job.rb index 5717de030..80bc5ae91 100644 --- a/app/jobs/rebuild_job.rb +++ b/app/jobs/rebuild_job.rb @@ -2,11 +2,6 @@ class RebuildJob < ApplicationJob queue_as :admin def perform(*args) - if Rails.env.production? - # Only run on Saturday in production - Sapi.rebuild if Date.today.saturday? - else - Sapi.rebuild - end + Sapi.rebuild end end diff --git a/config/schedule.yml b/config/schedule.yml index ce5959de4..7b8f93d44 100644 --- a/config/schedule.yml +++ b/config/schedule.yml @@ -1,7 +1,14 @@ -rebuild_job: - cron: "42 1 * * *" +rebuild_job_weekly: + cron: "42 1 * * 6" class: "RebuildJob" queue: admin + +# disable the following job on production +rebuild_job_daily: + cron: "42 1 * * 0-5" + class: "RebuildJob" + queue: admin + downloads_cache_update_job: cron: "5 4 * * *" class: "DownloadsCacheUpdateJob" From 5ae569ad1e8835186a13f70ffa3ad8d93f5a0f60 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Mon, 5 Feb 2024 16:23:42 +0000 Subject: [PATCH 167/241] Fix rake task and bg job for rebuild --- app/jobs/rebuild_job.rb | 2 +- lib/tasks/db_migrate_plpgsql.rake | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/jobs/rebuild_job.rb b/app/jobs/rebuild_job.rb index 80bc5ae91..ffa7ac484 100644 --- a/app/jobs/rebuild_job.rb +++ b/app/jobs/rebuild_job.rb @@ -2,6 +2,6 @@ class RebuildJob < ApplicationJob queue_as :admin def perform(*args) - Sapi.rebuild + Sapi::rebuild end end diff --git a/lib/tasks/db_migrate_plpgsql.rake b/lib/tasks/db_migrate_plpgsql.rake index 66c3bc072..34134dbc2 100644 --- a/lib/tasks/db_migrate_plpgsql.rake +++ b/lib/tasks/db_migrate_plpgsql.rake @@ -18,7 +18,7 @@ namespace :db do desc "Rebuild all computed values" task :rebuild => :migrate do - Sapi.rebuild + Sapi::rebuild end task :drop_indexes => :migrate do From c4804292d9bc6e37ebaedb5e95c28052d7aad436 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Mon, 5 Feb 2024 16:55:42 +0000 Subject: [PATCH 168/241] Rename `Sapi` module to `SapiModule` so it less chance to confuse with SAPI. --- .../admin/statistics_controller.rb | 2 +- .../trade/trade_data_download_logger.rb | 4 ++-- app/controllers/species/exports_controller.rb | 2 +- app/jobs/rebuild_job.rb | 2 +- config/initializers/ahoy.rb | 6 +++--- lib/modules/sapi.rb | 20 ------------------- lib/modules/sapi_module.rb | 20 +++++++++++++++++++ lib/modules/{sapi => sapi_module}/geo_i_p.rb | 2 +- lib/modules/{sapi => sapi_module}/indexes.rb | 2 +- .../stored_procedures.rb | 2 +- lib/modules/{sapi => sapi_module}/summary.rb | 2 +- lib/tasks/db_migrate_plpgsql.rake | 6 +++--- lib/tasks/import.rake | 2 +- lib/tasks/import_trade_names.rake | 4 ++-- lib/tasks/import_trade_shipments.rake | 8 ++++---- lib/tasks/resolve_host_to_country.rake | 2 +- lib/tasks/trade_db_resolve_ip_to_country.rake | 2 +- .../cites_trade/shipments_controller_spec.rb | 2 +- .../species/exports_controller_spec.rb | 16 +++++++-------- .../trade/shipments_controller_spec.rb | 10 +++++----- spec/models/sapi/geoip_spec.rb | 4 ++-- spec/models/taxon_concept/synonyms_spec.rb | 2 +- spec/models/taxon_concept/trade_names_spec.rb | 2 +- .../trade/inclusion_validation_rule_spec.rb | 4 ++-- spec/models/trade/shipment_spec.rb | 8 ++++---- ...cept_appendix_year_validation_rule_spec.rb | 4 ++-- .../checklist/appendix_population_spec.rb | 6 +++--- .../checklist/higher_taxa_injector_spec.rb | 2 +- .../pdf/history_annotations_key_spec.rb | 2 +- spec/services/checklist/pdf/history_spec.rb | 12 +++++------ .../pdf/index_annotations_key_spec.rb | 4 ++-- .../checklist/pdf/index_fetcher_spec.rb | 4 ++-- spec/services/checklist/timeline_spec.rb | 14 ++++++------- .../timelines_for_taxon_concept_spec.rb | 8 ++++---- spec/services/dashboard_stats_trade_spec.rb | 2 +- spec/services/species/listings_export_spec.rb | 2 +- .../species/trade_name_prefix_matcher_spec.rb | 2 +- spec/services/trade/filter_spec.rb | 4 ++-- spec/services/trade/sandbox_filter_spec.rb | 2 +- spec/shared/agalychnis.rb | 2 +- spec/shared/agave.rb | 2 +- spec/shared/ailuropoda.rb | 2 +- spec/shared/arctocephalus.rb | 2 +- spec/shared/boa_constrictor.rb | 2 +- spec/shared/caiman_latirostris.rb | 2 +- spec/shared/canis_lupus.rb | 2 +- spec/shared/caretta_caretta_cms.rb | 2 +- spec/shared/cedrela_montana.rb | 2 +- spec/shared/cervus_elaphus.rb | 2 +- spec/shared/cervus_elaphus_cms.rb | 2 +- spec/shared/colophon.rb | 2 +- spec/shared/dalbergia.rb | 2 +- spec/shared/diospyros.rb | 2 +- spec/shared/falconiformes.rb | 2 +- spec/shared/hirudo_medicinalis.rb | 2 +- spec/shared/loxodonta_africana.rb | 2 +- spec/shared/loxodonta_africana_cms.rb | 2 +- spec/shared/mellivora_capensis.rb | 2 +- spec/shared/moschus.rb | 2 +- spec/shared/natator_depressus.rb | 2 +- spec/shared/notomys_aquilo.rb | 2 +- spec/shared/panax_ginseng.rb | 2 +- spec/shared/pecari_tajacu.rb | 2 +- spec/shared/pereskia.rb | 2 +- spec/shared/platysternon_megacephalum.rb | 2 +- spec/shared/pristis_microdon.rb | 2 +- spec/shared/pseudomys_fieldi.rb | 2 +- spec/shared/psittaciformes.rb | 2 +- spec/shared/tapiridae.rb | 2 +- spec/shared/uroplatus.rb | 2 +- spec/shared/varanidae.rb | 2 +- 71 files changed, 134 insertions(+), 134 deletions(-) delete mode 100644 lib/modules/sapi.rb create mode 100644 lib/modules/sapi_module.rb rename lib/modules/{sapi => sapi_module}/geo_i_p.rb (98%) rename lib/modules/{sapi => sapi_module}/indexes.rb (99%) rename lib/modules/{sapi => sapi_module}/stored_procedures.rb (99%) rename lib/modules/{sapi => sapi_module}/summary.rb (99%) diff --git a/app/controllers/admin/statistics_controller.rb b/app/controllers/admin/statistics_controller.rb index 4c5e8f9c1..6b73564a3 100644 --- a/app/controllers/admin/statistics_controller.rb +++ b/app/controllers/admin/statistics_controller.rb @@ -1,6 +1,6 @@ class Admin::StatisticsController < ApplicationController def index - @stats = Sapi::Summary.database_stats + @stats = SapiModule::Summary.database_stats end end diff --git a/app/controllers/concerns/trade/trade_data_download_logger.rb b/app/controllers/concerns/trade/trade_data_download_logger.rb index ed2c46e35..7e208f0c4 100644 --- a/app/controllers/concerns/trade/trade_data_download_logger.rb +++ b/app/controllers/concerns/trade/trade_data_download_logger.rb @@ -1,4 +1,4 @@ -require 'sapi/geo_i_p' +require 'sapi_module/geo_i_p' module Trade::TradeDataDownloadLogger module_function @@ -19,7 +19,7 @@ def log_download(request, search_params, rows) data["source"] = self.get_field_values(search_params[:sources_ids], Source) data["importer"] = self.get_field_values(search_params[:importers_ids], GeoEntity) data["exporter"] = self.get_field_values(search_params[:exporters_ids], GeoEntity) - geo_ip_data = Sapi::GeoIP.instance.resolve(request.ip) + geo_ip_data = SapiModule::GeoIP.instance.resolve(request.ip) [:country, :city, :organization].each do |col| data[col] = geo_ip_data[col] end diff --git a/app/controllers/species/exports_controller.rb b/app/controllers/species/exports_controller.rb index bdb95ae04..8fd2e6abb 100644 --- a/app/controllers/species/exports_controller.rb +++ b/app/controllers/species/exports_controller.rb @@ -48,7 +48,7 @@ def set_csv_separator return else ip = request.remote_ip - separator = Sapi::GeoIP.instance.default_separator(ip) + separator = SapiModule::GeoIP.instance.default_separator(ip) cookies.permanent['speciesplus.csv_separator'] = separator end end diff --git a/app/jobs/rebuild_job.rb b/app/jobs/rebuild_job.rb index ffa7ac484..aeef46fa2 100644 --- a/app/jobs/rebuild_job.rb +++ b/app/jobs/rebuild_job.rb @@ -2,6 +2,6 @@ class RebuildJob < ApplicationJob queue_as :admin def perform(*args) - Sapi::rebuild + SapiModule::rebuild end end diff --git a/config/initializers/ahoy.rb b/config/initializers/ahoy.rb index 648565ff4..1ffed328e 100644 --- a/config/initializers/ahoy.rb +++ b/config/initializers/ahoy.rb @@ -1,4 +1,4 @@ -require 'sapi/geo_i_p' +require 'sapi_module/geo_i_p' class Ahoy::Store < Ahoy::DatabaseStore def authenticate(data) @@ -11,7 +11,7 @@ def track_visit(data) data[:id] = ensure_uuid(data.delete(:visit_token)) data[:visitor_id] = ensure_uuid(data.delete(:visitor_token)) - geo_ip_data = Sapi::GeoIP.instance.resolve(request.ip) + geo_ip_data = SapiModule::GeoIP.instance.resolve(request.ip) data[:country] = geo_ip_data[:country] data[:city] = geo_ip_data[:city] data[:organization] = geo_ip_data[:organization] @@ -38,7 +38,7 @@ def ensure_uuid(id) Ahoy.user_agent_parser = :device_detector Ahoy.bot_detection_version = 2 Ahoy.track_bots = Rails.env.test? -Ahoy.geocode = false # we use our own geocoder (Sapi::GeoIP) +Ahoy.geocode = false # we use our own geocoder (SapiModule::GeoIP) Ahoy.mask_ips = true Ahoy.cookies = :none diff --git a/lib/modules/sapi.rb b/lib/modules/sapi.rb deleted file mode 100644 index e72d04e67..000000000 --- a/lib/modules/sapi.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'sapi/stored_procedures.rb' -require 'sapi/indexes.rb' -require 'sapi/summary.rb' -module Sapi - def self.rebuild - Sapi::StoredProcedures.rebuild - end - - def self.drop_indexes - Sapi::Indexes.drop_indexes - end - - def self.create_indexes - Sapi::Indexes.create_indexes - end - - def self.database_summary - Sapi::Summary.database_summary - end -end diff --git a/lib/modules/sapi_module.rb b/lib/modules/sapi_module.rb new file mode 100644 index 000000000..3208acb96 --- /dev/null +++ b/lib/modules/sapi_module.rb @@ -0,0 +1,20 @@ +require 'sapi_module/stored_procedures.rb' +require 'sapi_module/indexes.rb' +require 'sapi_module/summary.rb' +module SapiModule + def self.rebuild + SapiModule::StoredProcedures.rebuild + end + + def self.drop_indexes + SapiModule::Indexes.drop_indexes + end + + def self.create_indexes + SapiModule::Indexes.create_indexes + end + + def self.database_summary + SapiModule::Summary.database_summary + end +end diff --git a/lib/modules/sapi/geo_i_p.rb b/lib/modules/sapi_module/geo_i_p.rb similarity index 98% rename from lib/modules/sapi/geo_i_p.rb rename to lib/modules/sapi_module/geo_i_p.rb index 039272220..0b03fe980 100644 --- a/lib/modules/sapi/geo_i_p.rb +++ b/lib/modules/sapi_module/geo_i_p.rb @@ -1,5 +1,5 @@ # Returns UTF8 -module Sapi +module SapiModule class GeoIP include Singleton diff --git a/lib/modules/sapi/indexes.rb b/lib/modules/sapi_module/indexes.rb similarity index 99% rename from lib/modules/sapi/indexes.rb rename to lib/modules/sapi_module/indexes.rb index 210b18472..67f031867 100644 --- a/lib/modules/sapi/indexes.rb +++ b/lib/modules/sapi_module/indexes.rb @@ -1,4 +1,4 @@ -module Sapi +module SapiModule module Indexes # rewrite the code below to just use add_index and add UNIQUE to the mview ids diff --git a/lib/modules/sapi/stored_procedures.rb b/lib/modules/sapi_module/stored_procedures.rb similarity index 99% rename from lib/modules/sapi/stored_procedures.rb rename to lib/modules/sapi_module/stored_procedures.rb index 06cf8b6ac..c10073e54 100644 --- a/lib/modules/sapi/stored_procedures.rb +++ b/lib/modules/sapi_module/stored_procedures.rb @@ -1,4 +1,4 @@ -module Sapi +module SapiModule module StoredProcedures def self.rebuild diff --git a/lib/modules/sapi/summary.rb b/lib/modules/sapi_module/summary.rb similarity index 99% rename from lib/modules/sapi/summary.rb rename to lib/modules/sapi_module/summary.rb index 5ccd18534..eb1db1e9a 100644 --- a/lib/modules/sapi/summary.rb +++ b/lib/modules/sapi_module/summary.rb @@ -1,4 +1,4 @@ -module Sapi +module SapiModule module Summary def self.database_stats diff --git a/lib/tasks/db_migrate_plpgsql.rake b/lib/tasks/db_migrate_plpgsql.rake index 34134dbc2..282fa9377 100644 --- a/lib/tasks/db_migrate_plpgsql.rake +++ b/lib/tasks/db_migrate_plpgsql.rake @@ -18,15 +18,15 @@ namespace :db do desc "Rebuild all computed values" task :rebuild => :migrate do - Sapi::rebuild + SapiModule::rebuild end task :drop_indexes => :migrate do - Sapi::drop_indexes + SapiModule::drop_indexes end task :create_indexes => :migrate do - Sapi::create_indexes + SapiModule::create_indexes end end diff --git a/lib/tasks/import.rake b/lib/tasks/import.rake index 96567e649..d68fc3460 100644 --- a/lib/tasks/import.rake +++ b/lib/tasks/import.rake @@ -118,7 +118,7 @@ namespace :import do desc 'Shows database summary stats' task :stats => :environment do - Sapi::database_summary + SapiModule::database_summary end desc 'Runs import tasks for cleaned files' diff --git a/lib/tasks/import_trade_names.rake b/lib/tasks/import_trade_names.rake index a37cb8344..0dd9ec8a1 100644 --- a/lib/tasks/import_trade_names.rake +++ b/lib/tasks/import_trade_names.rake @@ -127,7 +127,7 @@ namespace :import do file = "lib/files/synonyms_to_trade_names.csv" drop_table(TMP_TABLE) create_table_from_csv_headers(file, TMP_TABLE) - Sapi::Indexes.drop_indexes_on_trade_names + SapiModule::Indexes.drop_indexes_on_trade_names copy_data(file, TMP_TABLE) has_trade_name = TaxonRelationshipType. find_or_create_by(:name => TaxonRelationshipType::HAS_TRADE_NAME).id @@ -218,6 +218,6 @@ namespace :import do Diff: #{final_count_trade_relationships - count_trade_relationships}" puts "Pre-Existing synonym_relationships: #{count_synonym_relationships}; Final count synonym_relationships: #{final_count_synonym_relationships};\ Diff: #{final_count_synonym_relationships - count_synonym_relationships}" - Sapi::Indexes.create_indexes_on_trade_names + SapiModule::Indexes.create_indexes_on_trade_names end end diff --git a/lib/tasks/import_trade_shipments.rake b/lib/tasks/import_trade_shipments.rake index d6c598801..463aa5338 100644 --- a/lib/tasks/import_trade_shipments.rake +++ b/lib/tasks/import_trade_shipments.rake @@ -25,7 +25,7 @@ namespace :import do files = files_from_args(t, args) files.each do |file| - Sapi::Indexes.drop_indexes_on_shipments + SapiModule::Indexes.drop_indexes_on_shipments drop_create_and_copy_temp(TMP_TABLE, file) sql = <<-SQL DELETE FROM shipments_import WHERE shipment_number = 8122168; @@ -41,7 +41,7 @@ namespace :import do update_country_codes populate_shipments populate_shipments_for_trade_names - Sapi::Indexes.create_indexes_on_shipments + SapiModule::Indexes.create_indexes_on_shipments end end @@ -53,7 +53,7 @@ namespace :import do files = files_from_args(t, args) files.each do |file| - Sapi::Indexes.drop_indexes_on_shipments + SapiModule::Indexes.drop_indexes_on_shipments drop_create_and_copy_temp(TMP_TABLE, file) sql = <<-SQL DELETE FROM shipments_import WHERE shipment_number = 8122168; @@ -69,7 +69,7 @@ namespace :import do update_country_codes # populate_shipments populate_shipments_for_trade_names - Sapi::Indexes.create_indexes_on_shipments + SapiModule::Indexes.create_indexes_on_shipments end end end diff --git a/lib/tasks/resolve_host_to_country.rake b/lib/tasks/resolve_host_to_country.rake index 5605f8919..4f8bf5774 100644 --- a/lib/tasks/resolve_host_to_country.rake +++ b/lib/tasks/resolve_host_to_country.rake @@ -2,7 +2,7 @@ task :resolve_host_to_country => :environment do require 'csv' hosts = CSV.read('/home/agnessa/Data/hosts.csv', headers: true) hosts_and_countries = hosts.map do |row| - geo_ip_data = Sapi::GeoIP.instance.resolve(row[0]) + geo_ip_data = SapiModule::GeoIP.instance.resolve(row[0]) [row[0], geo_ip_data[:country]] end CSV.open('/home/agnessa/Data/hosts_and_countries.csv', 'w') do |csv| diff --git a/lib/tasks/trade_db_resolve_ip_to_country.rake b/lib/tasks/trade_db_resolve_ip_to_country.rake index fb08d2573..4d35d9a5f 100644 --- a/lib/tasks/trade_db_resolve_ip_to_country.rake +++ b/lib/tasks/trade_db_resolve_ip_to_country.rake @@ -4,7 +4,7 @@ task :trade_db_resolve_ip_to_country => :environment do .where('created_at > ?',"2019-03-11 10:00:55") trade_downloads.each do |td| puts "Updating Trade download #{td.id}" - geo_ip_data = Sapi::GeoIP.instance.resolve(td.user_ip) + geo_ip_data = SapiModule::GeoIP.instance.resolve(td.user_ip) td.update!(geo_ip_data) end end diff --git a/spec/controllers/cites_trade/shipments_controller_spec.rb b/spec/controllers/cites_trade/shipments_controller_spec.rb index a8fe3fc39..945140532 100644 --- a/spec/controllers/cites_trade/shipments_controller_spec.rb +++ b/spec/controllers/cites_trade/shipments_controller_spec.rb @@ -4,7 +4,7 @@ include_context 'Shipments' describe "GET index" do - before(:each) { Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings } + before(:each) { SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings } context "serializer" do it "should return comptab export when report_type invalid" do get :index, params: { filters: { diff --git a/spec/controllers/species/exports_controller_spec.rb b/spec/controllers/species/exports_controller_spec.rb index bf97136cc..eab31e031 100644 --- a/spec/controllers/species/exports_controller_spec.rb +++ b/spec/controllers/species/exports_controller_spec.rb @@ -21,7 +21,7 @@ it 'sets separator to comma with UK ip address' do allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return("194.59.188.126") - allow_any_instance_of(Sapi::GeoIP).to receive(:country_and_city).and_return({ :country => "GB", :city => "Cambridge" }) + allow_any_instance_of(SapiModule::GeoIP).to receive(:country_and_city).and_return({ :country => "GB", :city => "Cambridge" }) get :download, params: { data_type: 'EuDecisions', :filters => { 'set' => 'current', 'decision_types' => {}, :csv_separator => '' } } @@ -32,7 +32,7 @@ it 'sets separator to semicolon with AF ip address' do allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return("175.106.59.78") - allow_any_instance_of(Sapi::GeoIP).to receive(:country_and_city).and_return({ :country => "AF", :city => "Kabul" }) + allow_any_instance_of(SapiModule::GeoIP).to receive(:country_and_city).and_return({ :country => "AF", :city => "Kabul" }) get :download, params: { data_type: 'EuDecisions', :filters => { 'set' => 'current', 'decision_types' => {}, :csv_separator => '' } } @@ -79,13 +79,13 @@ # after(:each){ DownloadsCache.clear_cites_listings } # it "returns count of listed species" do # create_cites_I_addition(:taxon_concept => create_cites_eu_species, :is_current => true) -# Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings +# SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings # get :download, :data_type => 'Listings', :filters => {:designation => 'CITES'}, :format => :json # parse_json(response.body)['total'].should == 1 # end # it "returns listed species file" do # create_cites_I_addition(:taxon_concept => create_cites_eu_species, :is_current => true) -# Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings +# SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings # Species::ListingsExport.any_instance.stub(:public_file_name).and_return('cites_listings.csv') # get :download, :data_type => 'Listings', :filters => {:designation => 'CITES'} # response.content_type.should eq("text/csv") @@ -100,13 +100,13 @@ # after(:each){ DownloadsCache.clear_eu_listings } # it "returns count of listed species" do # create_eu_A_addition(:taxon_concept => create_cites_eu_species, :event =>reg2013, :effective_at => '2013-08-10', :is_current => true) -# Sapi::StoredProcedures.rebuild_eu_taxonomy_and_listings +# SapiModule::StoredProcedures.rebuild_eu_taxonomy_and_listings # get :download, :data_type => 'Listings', :filters => {:designation => 'EU'}, :format => :json # parse_json(response.body)['total'].should == 1 # end # it "returns listed species file" do # create_eu_A_addition(:taxon_concept => create_cites_eu_species, :event =>reg2013, :effective_at => '2013-08-10', :is_current => true) -# Sapi::StoredProcedures.rebuild_eu_taxonomy_and_listings +# SapiModule::StoredProcedures.rebuild_eu_taxonomy_and_listings # Species::ListingsExport.any_instance.stub(:public_file_name).and_return('eu_listings.csv') # get :download, :data_type => 'Listings', :filters => {:designation => 'EU'} # response.content_type.should eq("text/csv") @@ -121,13 +121,13 @@ # after(:each){ DownloadsCache.clear_cms_listings } # it "returns count of listed species" do # create_cms_I_addition(:taxon_concept => create_cms_species, :is_current => true) -# Sapi::StoredProcedures.rebuild_cms_taxonomy_and_listings +# SapiModule::StoredProcedures.rebuild_cms_taxonomy_and_listings # get :download, :data_type => 'Listings', :filters => {:designation => 'CMS'}, :format => :json # parse_json(response.body)['total'].should == 1 # end # it "returns listed species file" do # create_cms_I_addition(:taxon_concept => create_cms_species, :is_current => true) -# Sapi::StoredProcedures.rebuild_cms_taxonomy_and_listings +# SapiModule::StoredProcedures.rebuild_cms_taxonomy_and_listings # Species::ListingsExport.any_instance.stub(:public_file_name).and_return('cms_listings.csv') # get :download, :data_type => 'Listings', :filters => {:designation => 'CMS'} # response.content_type.should eq("text/csv") diff --git a/spec/controllers/trade/shipments_controller_spec.rb b/spec/controllers/trade/shipments_controller_spec.rb index 258f2db61..ffc4f4cd1 100644 --- a/spec/controllers/trade/shipments_controller_spec.rb +++ b/spec/controllers/trade/shipments_controller_spec.rb @@ -6,7 +6,7 @@ include_context 'Shipments' describe "GET index" do - before(:each) { Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings } + before(:each) { SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings } it "should return all shipments" do get :index, format: :json expect(response.body).to have_json_size(7).at_path('shipments') @@ -23,7 +23,7 @@ end describe "PUT update" do - before(:each) { Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings } + before(:each) { SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings } it "should auto resolve accepted taxon when blank" do put :update, params: { id: @shipment1.id, shipment: { reported_taxon_concept_id: @synonym_subspecies.id @@ -46,7 +46,7 @@ end describe "POST update_batch" do - before(:each) { Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings } + before(:each) { SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings } it "should change reporter type from I to E" do post :update_batch, params: { filters: { # shipment2 time_range_start: @shipment1.year, @@ -135,7 +135,7 @@ end describe "POST destroy_batch" do - before(:each) { Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings } + before(:each) { SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings } it "should delete 1 shipment" do post :destroy_batch, params: { time_range_start: @shipment1.year, time_range_end: @shipment2.year, reporter_type: 'E', exporters_ids: [@portugal.id.to_s, @argentina.id.to_s], importers_ids: [@portugal.id.to_s, @argentina.id.to_s], taxon_concepts_ids: [@animal_species.id] } expect(Trade::Shipment.count).to eq(6) @@ -193,7 +193,7 @@ end describe "DELETE destroy" do - before(:each) { Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings } + before(:each) { SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings } it "should delete 1 shipment" do delete :destroy, params: { id: @shipment1.id } expect(Trade::Shipment.where(id: @shipment1.id)).to be_empty diff --git a/spec/models/sapi/geoip_spec.rb b/spec/models/sapi/geoip_spec.rb index ff9c1a7c4..cc28d2364 100644 --- a/spec/models/sapi/geoip_spec.rb +++ b/spec/models/sapi/geoip_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' -describe Sapi::GeoIP do +describe SapiModule::GeoIP do describe :resolve do - subject { Sapi::GeoIP.instance } + subject { SapiModule::GeoIP.instance } before(:each) do bogota_latin1 = "Bogotá".encode('ISO-8859-1', 'UTF-8') allow(subject).to receive(:country_and_city).and_return( diff --git a/spec/models/taxon_concept/synonyms_spec.rb b/spec/models/taxon_concept/synonyms_spec.rb index 8b696ec7c..e8e7ad2f4 100644 --- a/spec/models/taxon_concept/synonyms_spec.rb +++ b/spec/models/taxon_concept/synonyms_spec.rb @@ -87,7 +87,7 @@ specify { expect(@synonym.full_name).to eq('Lolcatus lolus furiatus') } context "overnight calculations" do before(:each) do - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings end # should not modify a synonym's full name overnight specify { expect(@synonym.reload.full_name).to eq('Lolcatus lolus furiatus') } diff --git a/spec/models/taxon_concept/trade_names_spec.rb b/spec/models/taxon_concept/trade_names_spec.rb index 73fddb12f..31106a3ea 100644 --- a/spec/models/taxon_concept/trade_names_spec.rb +++ b/spec/models/taxon_concept/trade_names_spec.rb @@ -87,7 +87,7 @@ specify { expect(@trade_name.full_name).to eq('Lolcatus lolus furiatus') } context "overnight calculations" do before(:each) do - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings end # should not modify a trade_name's full name overnight specify { expect(@trade_name.reload.full_name).to eq('Lolcatus lolus furiatus') } diff --git a/spec/models/trade/inclusion_validation_rule_spec.rb b/spec/models/trade/inclusion_validation_rule_spec.rb index 4f395796f..791457292 100644 --- a/spec/models/trade/inclusion_validation_rule_spec.rb +++ b/spec/models/trade/inclusion_validation_rule_spec.rb @@ -59,7 +59,7 @@ error_message: "taxon_name Caniis lupus is invalid", error_count: 1 ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings validation_rule.refresh_errors_if_needed(annual_report_upload) end specify { @@ -96,7 +96,7 @@ error_message: "taxon_name Caniis lupus is invalid", error_count: 2 ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings validation_rule.refresh_errors_if_needed(annual_report_upload) end diff --git a/spec/models/trade/shipment_spec.rb b/spec/models/trade/shipment_spec.rb index 3d5261f86..34656f8f7 100644 --- a/spec/models/trade/shipment_spec.rb +++ b/spec/models/trade/shipment_spec.rb @@ -110,7 +110,7 @@ :is_current => true ) reg2013 # EU event - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings create_taxon_concept_appendix_year_validation end context "invalid" do @@ -150,7 +150,7 @@ :event => reg2013, :is_current => true ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings create_taxon_concept_appendix_year_validation end context "valid" do @@ -171,7 +171,7 @@ :parent => @genus ) reg2013 # EU event - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings create_taxon_concept_appendix_year_validation end context "not CITES listed and not EU listed" do @@ -431,7 +431,7 @@ create_taxon_concept_source_validation cites reg2013 # EU event - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings @taxon_concept.reload end context "invalid" do diff --git a/spec/models/trade/taxon_concept_appendix_year_validation_rule_spec.rb b/spec/models/trade/taxon_concept_appendix_year_validation_rule_spec.rb index 92e5cbbe5..57fa4bcc4 100644 --- a/spec/models/trade/taxon_concept_appendix_year_validation_rule_spec.rb +++ b/spec/models/trade/taxon_concept_appendix_year_validation_rule_spec.rb @@ -59,7 +59,7 @@ :taxon_concept => @species, :other_taxon_concept => synonym ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings end context "when split listing" do @@ -158,7 +158,7 @@ :effective_at => '1979-06-28', :is_current => true ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings @sandbox_klass.create( :taxon_name => 'Falco hybrid', :appendix => 'II', :year => '2012' ) diff --git a/spec/services/checklist/appendix_population_spec.rb b/spec/services/checklist/appendix_population_spec.rb index 26297fbf6..e3682e0e1 100644 --- a/spec/services/checklist/appendix_population_spec.rb +++ b/spec/services/checklist/appendix_population_spec.rb @@ -4,7 +4,7 @@ include_context "Canis lupus" context "search by cites populations" do - before(:each) { Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings } + before(:each) { SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings } context "when Nepal" do subject { checklist = Checklist::Checklist.new({ @@ -29,7 +29,7 @@ end end context "search by cites appendices" do - before(:each) { Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings } + before(:each) { SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings } context "when App I" do subject { checklist = Checklist::Checklist.new({ @@ -65,7 +65,7 @@ end end context "search by cites populations and appendices" do - before(:each) { Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings } + before(:each) { SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings } context "when Nepal" do context "when App I" do subject { diff --git a/spec/services/checklist/higher_taxa_injector_spec.rb b/spec/services/checklist/higher_taxa_injector_spec.rb index 22c277a4e..86c87021b 100644 --- a/spec/services/checklist/higher_taxa_injector_spec.rb +++ b/spec/services/checklist/higher_taxa_injector_spec.rb @@ -93,7 +93,7 @@ :parent => genus2_1_1_1, :taxon_name => create(:taxon_name, :scientific_name => 'fatalus') ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings @order2 = MTaxonConcept.find(order2.id) @family1 = MTaxonConcept.find(family1.id) @genus1_1 = MTaxonConcept.find(genus1_1.id) diff --git a/spec/services/checklist/pdf/history_annotations_key_spec.rb b/spec/services/checklist/pdf/history_annotations_key_spec.rb index 15d0d94d5..412325dca 100644 --- a/spec/services/checklist/pdf/history_annotations_key_spec.rb +++ b/spec/services/checklist/pdf/history_annotations_key_spec.rb @@ -53,7 +53,7 @@ ), :is_current => true ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings end subject { Checklist::Pdf::HistoryAnnotationsKey.new } specify { diff --git a/spec/services/checklist/pdf/history_spec.rb b/spec/services/checklist/pdf/history_spec.rb index f01709fc8..bc9aaa854 100644 --- a/spec/services/checklist/pdf/history_spec.rb +++ b/spec/services/checklist/pdf/history_spec.rb @@ -8,7 +8,7 @@ tc = create_cites_eu_family( :taxon_name => create(:taxon_name, :scientific_name => 'Foobaridae') ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings MTaxonConcept.find(tc.id) } let(:genus_tc) { @@ -16,7 +16,7 @@ :parent_id => family_tc.id, :taxon_name => create(:taxon_name, :scientific_name => 'Foobarus') ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings MTaxonConcept.find(tc.id) } describe :higher_taxon_name do @@ -32,7 +32,7 @@ :language => en ) ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings } subject { Checklist::Pdf::History.new(:scientific_name => tc.full_name, :show_english => true) } specify { @@ -49,7 +49,7 @@ :taxon_concept_id => tc.id, :is_current => true ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings MCitesListingChange.find(lc.id) } subject { Checklist::Pdf::History.new(:scientific_name => tc.full_name) } @@ -64,7 +64,7 @@ :taxon_concept_id => tc.id, :is_current => true ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings MCitesListingChange.find(lc.id) } subject { Checklist::Pdf::History.new(:scientific_name => tc.full_name) } @@ -92,7 +92,7 @@ :is_current => true, :nomenclature_note_en => 'Previously listed as Foobarus polonicus.' ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings MCitesListingChange.find(lc.id) } subject { Checklist::Pdf::History.new({}) } diff --git a/spec/services/checklist/pdf/index_annotations_key_spec.rb b/spec/services/checklist/pdf/index_annotations_key_spec.rb index c91a65e21..c1f9c52b7 100644 --- a/spec/services/checklist/pdf/index_annotations_key_spec.rb +++ b/spec/services/checklist/pdf/index_annotations_key_spec.rb @@ -40,7 +40,7 @@ ), :is_current => true ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings end subject { Checklist::Pdf::IndexAnnotationsKey.new } specify { @@ -96,7 +96,7 @@ ), :is_current => true ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings end subject { Checklist::Pdf::IndexAnnotationsKey.new } specify { diff --git a/spec/services/checklist/pdf/index_fetcher_spec.rb b/spec/services/checklist/pdf/index_fetcher_spec.rb index d6258bf71..0e1d71bda 100644 --- a/spec/services/checklist/pdf/index_fetcher_spec.rb +++ b/spec/services/checklist/pdf/index_fetcher_spec.rb @@ -31,7 +31,7 @@ ) tc.common_names << english_common_name tc.common_names << spanish_common_name - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings tc } let(:rel) { MTaxonConcept.by_scientific_name('Lolcatus') } @@ -63,7 +63,7 @@ :taxon_concept_id => tc.id, :other_taxon_concept_id => synonym.id ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings } let(:query) { Checklist::Pdf::IndexQuery.new( diff --git a/spec/services/checklist/timeline_spec.rb b/spec/services/checklist/timeline_spec.rb index 29fce8fd9..571d3f9e4 100644 --- a/spec/services/checklist/timeline_spec.rb +++ b/spec/services/checklist/timeline_spec.rb @@ -14,7 +14,7 @@ :effective_at => '1975-06-07', :is_current => false ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings MTaxonConcept.find(tc.id) } let(:ttc) { Checklist::TimelinesForTaxonConcept.new(tc) } @@ -74,7 +74,7 @@ :listing_change => lc4, :is_party => true ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings MTaxonConcept.find(tc.id) } let(:ttc) { Checklist::TimelinesForTaxonConcept.new(tc) } @@ -103,7 +103,7 @@ :effective_at => '1975-06-08', :is_current => true ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings MTaxonConcept.find(tc.id) } let(:ttc) { Checklist::TimelinesForTaxonConcept.new(tc) } @@ -145,7 +145,7 @@ :listing_change => w, :is_party => true ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings MTaxonConcept.find(tc.id) } let(:ttc) { Checklist::TimelinesForTaxonConcept.new(tc) } @@ -193,7 +193,7 @@ :listing_change => r2, :is_party => true ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings MTaxonConcept.find(tc.id) } let(:ttc) { Checklist::TimelinesForTaxonConcept.new(tc) } @@ -218,7 +218,7 @@ :effective_at => '1975-06-08', :is_current => true ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings MTaxonConcept.find(tc.id) } let(:ttc) { Checklist::TimelinesForTaxonConcept.new(tc) } @@ -248,7 +248,7 @@ :is_current => true ) # tc should have a cascaded ADD I from parent and an auto DEL I - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings MTaxonConcept.find(tc.id) } let(:ttc) { Checklist::TimelinesForTaxonConcept.new(tc) } diff --git a/spec/services/checklist/timelines_for_taxon_concept_spec.rb b/spec/services/checklist/timelines_for_taxon_concept_spec.rb index 1234af3c6..c0e17b450 100644 --- a/spec/services/checklist/timelines_for_taxon_concept_spec.rb +++ b/spec/services/checklist/timelines_for_taxon_concept_spec.rb @@ -18,7 +18,7 @@ :effective_at => '1975-06-06', :is_current => true ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings MTaxonConcept.find(tc.id) } subject { Checklist::TimelinesForTaxonConcept.new(tc) } @@ -39,7 +39,7 @@ :listing_change => lc, :is_party => true ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings MTaxonConcept.find(tc.id) } subject { Checklist::TimelinesForTaxonConcept.new(tc) } @@ -60,7 +60,7 @@ :listing_change => lc, :is_party => true ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings MTaxonConcept.find(tc.id) } subject { Checklist::TimelinesForTaxonConcept.new(tc) } @@ -74,7 +74,7 @@ context "when in 1990" do let(:tc) { tc = create(:taxon_concept) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings MTaxonConcept.find(tc.id) } subject { Checklist::TimelinesForTaxonConcept.new(tc).timeline_years } diff --git a/spec/services/dashboard_stats_trade_spec.rb b/spec/services/dashboard_stats_trade_spec.rb index 71e25e1ef..8909f9480 100644 --- a/spec/services/dashboard_stats_trade_spec.rb +++ b/spec/services/dashboard_stats_trade_spec.rb @@ -4,7 +4,7 @@ include_context "Shipments" describe "#trade" do before(:each) do - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings @shipment4_by_partner = create( :shipment, :taxon_concept => @animal_species, diff --git a/spec/services/species/listings_export_spec.rb b/spec/services/species/listings_export_spec.rb index 4f5908755..ef32083c3 100644 --- a/spec/services/species/listings_export_spec.rb +++ b/spec/services/species/listings_export_spec.rb @@ -100,7 +100,7 @@ create_cites_eu_subspecies( :parent_id => @species.id ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings end subject { Species::ListingsExportFactory.new({ diff --git a/spec/services/species/trade_name_prefix_matcher_spec.rb b/spec/services/species/trade_name_prefix_matcher_spec.rb index 509f0896d..1373aaf74 100644 --- a/spec/services/species/trade_name_prefix_matcher_spec.rb +++ b/spec/services/species/trade_name_prefix_matcher_spec.rb @@ -21,7 +21,7 @@ :taxon_relationship_type => trade_name_relationship_type ) create_cites_I_addition(:taxon_concept => @accepted_name) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings @accepted_name_ac = MAutoCompleteTaxonConcept.find(@accepted_name.id) @trade_name_ac = MAutoCompleteTaxonConcept.find(@trade_name.id) @status_N_species_ac = MAutoCompleteTaxonConcept.find(@status_N_species.id) diff --git a/spec/services/trade/filter_spec.rb b/spec/services/trade/filter_spec.rb index df9ef9029..65c524d83 100644 --- a/spec/services/trade/filter_spec.rb +++ b/spec/services/trade/filter_spec.rb @@ -4,7 +4,7 @@ describe :results do context "when searching by taxon concepts ids" do - before(:each) { Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings } + before(:each) { SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings } context "in the public interface" do context "at GENUS rank" do subject { Trade::Filter.new({ @@ -105,7 +105,7 @@ end end context "when searching by reported taxon concepts ids" do - before(:each) { Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings } + before(:each) { SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings } context "when trade names shipments present" do before(:each) do @shipment_of_trade_name = create( diff --git a/spec/services/trade/sandbox_filter_spec.rb b/spec/services/trade/sandbox_filter_spec.rb index 3c3cb5bca..f04323d10 100644 --- a/spec/services/trade/sandbox_filter_spec.rb +++ b/spec/services/trade/sandbox_filter_spec.rb @@ -41,7 +41,7 @@ is_current: true, event: reg2013 ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings @validation_error = create( :validation_error, annual_report_upload_id: annual_report_upload.id, diff --git a/spec/shared/agalychnis.rb b/spec/shared/agalychnis.rb index cad023edb..0a5e908ae 100644 --- a/spec/shared/agalychnis.rb +++ b/spec/shared/agalychnis.rb @@ -51,7 +51,7 @@ :excluded_taxon_concepts_ids => "{#{@genus.id}}" ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings self.instance_variables.each do |t| #Skip old sapi context let statements, #which are now instance variables starting with _ diff --git a/spec/shared/agave.rb b/spec/shared/agave.rb index 7ec67990c..d08068be9 100644 --- a/spec/shared/agave.rb +++ b/spec/shared/agave.rb @@ -72,7 +72,7 @@ def fr :is_current => true ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings self.instance_variables.each do |t| #Skip old sapi context let statements, #which are now instance variables starting with _ diff --git a/spec/shared/ailuropoda.rb b/spec/shared/ailuropoda.rb index 1def79b32..7a9b56198 100644 --- a/spec/shared/ailuropoda.rb +++ b/spec/shared/ailuropoda.rb @@ -41,7 +41,7 @@ :is_current => true ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings self.instance_variables.each do |t| #Skip old sapi context let statements, #which are now instance variables starting with _ diff --git a/spec/shared/arctocephalus.rb b/spec/shared/arctocephalus.rb index 9245bc6e1..86e4622f2 100644 --- a/spec/shared/arctocephalus.rb +++ b/spec/shared/arctocephalus.rb @@ -111,7 +111,7 @@ def fr :is_current => true ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings self.instance_variables.each do |t| #Skip old sapi context let statements, #which are now instance variables starting with _ diff --git a/spec/shared/boa_constrictor.rb b/spec/shared/boa_constrictor.rb index 1b4ffe7f5..82e7e5724 100644 --- a/spec/shared/boa_constrictor.rb +++ b/spec/shared/boa_constrictor.rb @@ -82,7 +82,7 @@ def en :is_current => true ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings self.instance_variables.each do |t| #Skip old sapi context let statements, #which are now instance variables starting with _ diff --git a/spec/shared/caiman_latirostris.rb b/spec/shared/caiman_latirostris.rb index 4123b45a1..c2059c185 100644 --- a/spec/shared/caiman_latirostris.rb +++ b/spec/shared/caiman_latirostris.rb @@ -152,7 +152,7 @@ def en :is_party => false ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings self.instance_variables.each do |t| #Skip old sapi context let statements, #which are now instance variables starting with _ diff --git a/spec/shared/canis_lupus.rb b/spec/shared/canis_lupus.rb index 4a6380675..2edc7d74b 100644 --- a/spec/shared/canis_lupus.rb +++ b/spec/shared/canis_lupus.rb @@ -127,7 +127,7 @@ ) end - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings self.instance_variables.each do |t| #Skip old sapi context let statements, #which are now instance variables starting with _ diff --git a/spec/shared/caretta_caretta_cms.rb b/spec/shared/caretta_caretta_cms.rb index 787bf33c7..f6abe7ec5 100644 --- a/spec/shared/caretta_caretta_cms.rb +++ b/spec/shared/caretta_caretta_cms.rb @@ -30,7 +30,7 @@ :is_current => true ) - Sapi::StoredProcedures.rebuild_cms_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cms_taxonomy_and_listings self.instance_variables.each do |t| #Skip old sapi context let statements, #which are now instance variables starting with _ diff --git a/spec/shared/cedrela_montana.rb b/spec/shared/cedrela_montana.rb index 421f53f1a..294d68da8 100644 --- a/spec/shared/cedrela_montana.rb +++ b/spec/shared/cedrela_montana.rb @@ -24,7 +24,7 @@ :is_current => true ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings self.instance_variables.each do |t| #Skip old sapi context let statements, #which are now instance variables starting with _ diff --git a/spec/shared/cervus_elaphus.rb b/spec/shared/cervus_elaphus.rb index 29f057b44..706cdbcb3 100644 --- a/spec/shared/cervus_elaphus.rb +++ b/spec/shared/cervus_elaphus.rb @@ -69,7 +69,7 @@ :is_current => true ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings self.instance_variables.each do |t| #Skip old sapi context let statements, #which are now instance variables starting with _ diff --git a/spec/shared/cervus_elaphus_cms.rb b/spec/shared/cervus_elaphus_cms.rb index d1dc51fcd..320c389cc 100644 --- a/spec/shared/cervus_elaphus_cms.rb +++ b/spec/shared/cervus_elaphus_cms.rb @@ -52,7 +52,7 @@ :instrument => create(:instrument, :name => 'Bukhara Deer') ) - Sapi::StoredProcedures.rebuild_cms_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cms_taxonomy_and_listings self.instance_variables.each do |t| #Skip old sapi context let statements, #which are now instance variables starting with _ diff --git a/spec/shared/colophon.rb b/spec/shared/colophon.rb index f8b699534..48c1b9f66 100644 --- a/spec/shared/colophon.rb +++ b/spec/shared/colophon.rb @@ -51,7 +51,7 @@ def south_africa :is_party => true ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings self.instance_variables.each do |t| #Skip old sapi context let statements, #which are now instance variables starting with _ diff --git a/spec/shared/dalbergia.rb b/spec/shared/dalbergia.rb index 1575eb5c1..634d03d64 100644 --- a/spec/shared/dalbergia.rb +++ b/spec/shared/dalbergia.rb @@ -72,7 +72,7 @@ def fr :is_party => false ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings self.instance_variables.each do |t| #Skip old sapi context let statements, #which are now instance variables starting with _ diff --git a/spec/shared/diospyros.rb b/spec/shared/diospyros.rb index 77a1eb44c..7cfaccc95 100644 --- a/spec/shared/diospyros.rb +++ b/spec/shared/diospyros.rb @@ -124,7 +124,7 @@ :is_party => false ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings self.instance_variables.each do |t| #Skip old sapi context let statements, #which are now instance variables starting with _ diff --git a/spec/shared/falconiformes.rb b/spec/shared/falconiformes.rb index 2258b2239..b0611d627 100644 --- a/spec/shared/falconiformes.rb +++ b/spec/shared/falconiformes.rb @@ -157,7 +157,7 @@ :is_current => true ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings self.instance_variables.each do |t| #Skip old sapi context let statements, #which are now instance variables starting with _ diff --git a/spec/shared/hirudo_medicinalis.rb b/spec/shared/hirudo_medicinalis.rb index 7e192f08a..7415b10ba 100644 --- a/spec/shared/hirudo_medicinalis.rb +++ b/spec/shared/hirudo_medicinalis.rb @@ -30,7 +30,7 @@ :is_current => true ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings self.instance_variables.each do |t| #Skip old sapi context let statements, #which are now instance variables starting with _ diff --git a/spec/shared/loxodonta_africana.rb b/spec/shared/loxodonta_africana.rb index 37f075720..5230864a7 100644 --- a/spec/shared/loxodonta_africana.rb +++ b/spec/shared/loxodonta_africana.rb @@ -92,7 +92,7 @@ ) end - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings self.instance_variables.each do |t| #Skip old sapi context let statements, #which are now instance variables starting with _ diff --git a/spec/shared/loxodonta_africana_cms.rb b/spec/shared/loxodonta_africana_cms.rb index c504d4c2a..0bc00436c 100644 --- a/spec/shared/loxodonta_africana_cms.rb +++ b/spec/shared/loxodonta_africana_cms.rb @@ -24,7 +24,7 @@ :is_current => true ) - Sapi::StoredProcedures.rebuild_cms_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cms_taxonomy_and_listings self.instance_variables.each do |t| #Skip old sapi context let statements, #which are now instance variables starting with _ diff --git a/spec/shared/mellivora_capensis.rb b/spec/shared/mellivora_capensis.rb index 086f810ce..6767a21aa 100644 --- a/spec/shared/mellivora_capensis.rb +++ b/spec/shared/mellivora_capensis.rb @@ -117,7 +117,7 @@ :listing_change => eu_lc3 ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings self.instance_variables.each do |t| #Skip old sapi context let statements, #which are now instance variables starting with _ diff --git a/spec/shared/moschus.rb b/spec/shared/moschus.rb index a06c43389..7114cf89e 100644 --- a/spec/shared/moschus.rb +++ b/spec/shared/moschus.rb @@ -143,7 +143,7 @@ :is_party => false ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings self.instance_variables.each do |t| #Skip old sapi context let statements, #which are now instance variables starting with _ diff --git a/spec/shared/natator_depressus.rb b/spec/shared/natator_depressus.rb index cf3d5e20a..e00f5404b 100644 --- a/spec/shared/natator_depressus.rb +++ b/spec/shared/natator_depressus.rb @@ -49,7 +49,7 @@ :is_current => true ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings self.instance_variables.each do |t| #Skip old sapi context let statements, #which are now instance variables starting with _ diff --git a/spec/shared/notomys_aquilo.rb b/spec/shared/notomys_aquilo.rb index 2e73de2e1..eb60a027b 100644 --- a/spec/shared/notomys_aquilo.rb +++ b/spec/shared/notomys_aquilo.rb @@ -34,7 +34,7 @@ :is_current => true ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings self.instance_variables.each do |t| #Skip old sapi context let statements, #which are now instance variables starting with _ diff --git a/spec/shared/panax_ginseng.rb b/spec/shared/panax_ginseng.rb index e2a88a758..a8503cdc2 100644 --- a/spec/shared/panax_ginseng.rb +++ b/spec/shared/panax_ginseng.rb @@ -85,7 +85,7 @@ ) end - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings self.instance_variables.each do |t| #Skip old sapi context let statements, #which are now instance variables starting with _ diff --git a/spec/shared/pecari_tajacu.rb b/spec/shared/pecari_tajacu.rb index cd05666d4..1f3c0e538 100644 --- a/spec/shared/pecari_tajacu.rb +++ b/spec/shared/pecari_tajacu.rb @@ -106,7 +106,7 @@ def america :geo_relationship_type => contains_geo_relationship_type ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings self.instance_variables.each do |t| #Skip old sapi context let statements, #which are now instance variables starting with _ diff --git a/spec/shared/pereskia.rb b/spec/shared/pereskia.rb index ab1916d91..2d088664e 100644 --- a/spec/shared/pereskia.rb +++ b/spec/shared/pereskia.rb @@ -55,7 +55,7 @@ :is_current => true ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings self.instance_variables.each do |t| #Skip old sapi context let statements, #which are now instance variables starting with _ diff --git a/spec/shared/platysternon_megacephalum.rb b/spec/shared/platysternon_megacephalum.rb index b41102c01..a5a8283aa 100644 --- a/spec/shared/platysternon_megacephalum.rb +++ b/spec/shared/platysternon_megacephalum.rb @@ -51,7 +51,7 @@ :is_current => true ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings self.instance_variables.each do |t| #Skip old sapi context let statements, #which are now instance variables starting with _ diff --git a/spec/shared/pristis_microdon.rb b/spec/shared/pristis_microdon.rb index ecc9f5680..b0c99a9e7 100644 --- a/spec/shared/pristis_microdon.rb +++ b/spec/shared/pristis_microdon.rb @@ -59,7 +59,7 @@ :is_current => true ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings self.instance_variables.each do |t| #Skip old sapi context let statements, #which are now instance variables starting with _ diff --git a/spec/shared/pseudomys_fieldi.rb b/spec/shared/pseudomys_fieldi.rb index bbb5da96d..51a9d0918 100644 --- a/spec/shared/pseudomys_fieldi.rb +++ b/spec/shared/pseudomys_fieldi.rb @@ -50,7 +50,7 @@ :is_current => true ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings self.instance_variables.each do |t| #Skip old sapi context let statements, #which are now instance variables starting with _ diff --git a/spec/shared/psittaciformes.rb b/spec/shared/psittaciformes.rb index c193913b4..58cd08399 100644 --- a/spec/shared/psittaciformes.rb +++ b/spec/shared/psittaciformes.rb @@ -251,7 +251,7 @@ def ghana :listing_change => eu_lc2 ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings self.instance_variables.each do |t| #Skip old sapi context let statements, #which are now instance variables starting with _ diff --git a/spec/shared/tapiridae.rb b/spec/shared/tapiridae.rb index 0431e84e6..e45845e23 100644 --- a/spec/shared/tapiridae.rb +++ b/spec/shared/tapiridae.rb @@ -44,7 +44,7 @@ :is_current => true ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings self.instance_variables.each do |t| #Skip old sapi context let statements, #which are now instance variables starting with _ diff --git a/spec/shared/uroplatus.rb b/spec/shared/uroplatus.rb index 1ff913d87..fa2e10ed5 100644 --- a/spec/shared/uroplatus.rb +++ b/spec/shared/uroplatus.rb @@ -51,7 +51,7 @@ :is_cascaded => true ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings self.instance_variables.each do |t| #Skip old sapi context let statements, #which are now instance variables starting with _ diff --git a/spec/shared/varanidae.rb b/spec/shared/varanidae.rb index 95e63243b..5fe76392e 100644 --- a/spec/shared/varanidae.rb +++ b/spec/shared/varanidae.rb @@ -77,7 +77,7 @@ :is_cascaded => true ) - Sapi::StoredProcedures.rebuild_cites_taxonomy_and_listings + SapiModule::StoredProcedures.rebuild_cites_taxonomy_and_listings self.instance_variables.each do |t| #Skip old sapi context let statements, #which are now instance variables starting with _ From e5f5decbd629602d2c66e44b2803b8d71b08c7db Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Mon, 5 Feb 2024 16:57:06 +0000 Subject: [PATCH 169/241] require sapi_module in initializers, after edge load in application.rb, so file like app/controllers/admin/statistics_controller.rb can use the module. --- config/initializers/0_load_sapi_module.rb | 1 + 1 file changed, 1 insertion(+) create mode 100644 config/initializers/0_load_sapi_module.rb diff --git a/config/initializers/0_load_sapi_module.rb b/config/initializers/0_load_sapi_module.rb new file mode 100644 index 000000000..f53194c54 --- /dev/null +++ b/config/initializers/0_load_sapi_module.rb @@ -0,0 +1 @@ +require 'sapi_module' From 935ebbde5be392cc21823fb7c4d13bf72e3ee361 Mon Sep 17 00:00:00 2001 From: Daniel Perrett Date: Mon, 5 Feb 2024 16:08:26 +0000 Subject: [PATCH 170/241] fix: typo s/changable/changeable/g --- .../concerns/{changable.rb => changeable.rb} | 44 +++++++++---------- app/models/distribution.rb | 2 +- app/models/eu_decision.rb | 2 +- app/models/geo_entity.rb | 2 +- app/models/language.rb | 2 +- app/models/listing_change.rb | 2 +- app/models/taxon_common.rb | 2 +- app/models/taxon_concept_reference.rb | 2 +- app/models/taxon_instrument.rb | 2 +- app/models/taxon_relationship.rb | 2 +- .../trade_restrictions/cites_suspension.rb | 2 +- app/models/trade_restrictions/quota.rb | 2 +- 12 files changed, 33 insertions(+), 33 deletions(-) rename app/models/concerns/{changable.rb => changeable.rb} (83%) diff --git a/app/models/concerns/changable.rb b/app/models/concerns/changeable.rb similarity index 83% rename from app/models/concerns/changable.rb rename to app/models/concerns/changeable.rb index 40dad5b3f..e2fa971d9 100644 --- a/app/models/concerns/changable.rb +++ b/app/models/concerns/changeable.rb @@ -1,41 +1,41 @@ -module Changable +module Changeable extend ActiveSupport::Concern included do ######################################################### ### after save - after_save :changable_after_save_callback - after_commit :changable_after_save_callback_on_commit, on: [:create, :update] + after_save :changeable_after_save_callback + after_commit :changeable_after_save_callback_on_commit, on: [:create, :update] ######################################################### ### before destroy - before_destroy :changable_before_destroy_callback - after_commit :changable_before_destroy_callback_on_commit, on: :destroy + before_destroy :changeable_before_destroy_callback + after_commit :changeable_before_destroy_callback_on_commit, on: :destroy end private - def changable_before_destroy_callback + def changeable_before_destroy_callback if respond_to?(:taxon_concept) && taxon_concept && can_be_deleted? # currently no easy means to tell who deleted the dependent object - changable_bump_dependents_timestamp_part_one(taxon_concept, nil) + changeable_bump_dependents_timestamp_part_one(taxon_concept, nil) end end - def changable_before_destroy_callback_on_commit - changable_clear_cache + def changeable_before_destroy_callback_on_commit + changeable_clear_cache if respond_to?(:taxon_concept) && taxon_concept && can_be_deleted? # currently no easy means to tell who deleted the dependent object - changable_bump_dependents_timestamp_part_two + changeable_bump_dependents_timestamp_part_two end end - def changable_after_save_callback + def changeable_after_save_callback unless respond_to?(:taxon_concept) return end if taxon_concept - changable_bump_dependents_timestamp_part_one(taxon_concept, updated_by_id) + changeable_bump_dependents_timestamp_part_one(taxon_concept, updated_by_id) end # Rails 5.1 to 5.2 # DEPRECATION WARNING: The behavior of `attribute_was` inside of after callbacks will be changing in the next version of Rails. @@ -63,13 +63,13 @@ def changable_after_save_callback taxon_concept && taxon_concept_id_before_last_save && taxon_concept_id != taxon_concept_id_before_last_save previous_taxon_concept = TaxonConcept.find_by_id(taxon_concept_id_before_last_save) if previous_taxon_concept - changable_bump_dependents_timestamp_part_one(previous_taxon_concept, updated_by_id) + changeable_bump_dependents_timestamp_part_one(previous_taxon_concept, updated_by_id) end end end - def changable_after_save_callback_on_commit - changable_clear_cache + def changeable_after_save_callback_on_commit + changeable_clear_cache # For models that are not directly related to taxon concepts # but for which is anyway preferable for the changes to be reflacted @@ -80,12 +80,12 @@ def changable_after_save_callback_on_commit # It's quite rare that updates to those objects will occur, # so there shouldn't be no harm in clearning the entire serializer cache. unless respond_to?(:taxon_concept) - changable_clear_show_tc_serializer_cache + changeable_clear_show_tc_serializer_cache return end if taxon_concept - changable_bump_dependents_timestamp_part_two + changeable_bump_dependents_timestamp_part_two end # Rails 5.1 to 5.2 # DEPRECATION WARNING: The behavior of `attribute_was` inside of after callbacks will be changing in the next version of Rails. @@ -113,18 +113,18 @@ def changable_after_save_callback_on_commit taxon_concept && taxon_concept_id_before_last_save && taxon_concept_id != taxon_concept_id_before_last_save previous_taxon_concept = TaxonConcept.find_by_id(taxon_concept_id_before_last_save) if previous_taxon_concept - changable_bump_dependents_timestamp_part_two + changeable_bump_dependents_timestamp_part_two end end end - def changable_clear_cache + def changeable_clear_cache return unless respond_to?(:taxon_concept) DownloadsCacheCleanupWorker.perform_async(self.class.to_s.tableize) end - def changable_bump_dependents_timestamp_part_one(taxon_concept, updated_by_id) + def changeable_bump_dependents_timestamp_part_one(taxon_concept, updated_by_id) return unless taxon_concept TaxonConcept.where(id: taxon_concept.id).update_all( @@ -133,13 +133,13 @@ def changable_bump_dependents_timestamp_part_one(taxon_concept, updated_by_id) ) end - def changable_bump_dependents_timestamp_part_two + def changeable_bump_dependents_timestamp_part_two return unless taxon_concept DownloadsCacheCleanupWorker.perform_async('taxon_concepts') end - def changable_clear_show_tc_serializer_cache + def changeable_clear_show_tc_serializer_cache ## # Disabling because we use memcache on production, but memcache doesn't implement this method. # For now, changes to records that appear in serializers will not change until the caches are expired, diff --git a/app/models/distribution.rb b/app/models/distribution.rb index f82d24c98..0935946db 100644 --- a/app/models/distribution.rb +++ b/app/models/distribution.rb @@ -13,7 +13,7 @@ # class Distribution < ApplicationRecord - include Changable + include Changeable include TrackWhoDoesIt # Migrated to controller (Strong Parameters) # attr_accessible :geo_entity_id, :taxon_concept_id, :tag_list, diff --git a/app/models/eu_decision.rb b/app/models/eu_decision.rb index a540b9165..da6a00c90 100644 --- a/app/models/eu_decision.rb +++ b/app/models/eu_decision.rb @@ -29,7 +29,7 @@ require 'digest/sha1' require 'csv' class EuDecision < ApplicationRecord - include Changable + include Changeable extend Mobility include TrackWhoDoesIt # Migrated to controller (Strong Parameters) diff --git a/app/models/geo_entity.rb b/app/models/geo_entity.rb index c4867f5a3..90d4b5df4 100644 --- a/app/models/geo_entity.rb +++ b/app/models/geo_entity.rb @@ -18,7 +18,7 @@ # class GeoEntity < ApplicationRecord - include Changable + include Changeable include Deletable extend Mobility # Migrated to controller (Strong Parameters) diff --git a/app/models/language.rb b/app/models/language.rb index ca9b400bf..b4743f1f5 100644 --- a/app/models/language.rb +++ b/app/models/language.rb @@ -13,7 +13,7 @@ # class Language < ApplicationRecord - include Changable + include Changeable include Deletable extend Mobility # Migrated to controller (Strong Parameters) diff --git a/app/models/listing_change.rb b/app/models/listing_change.rb index 4daa4efd7..6ed85050d 100644 --- a/app/models/listing_change.rb +++ b/app/models/listing_change.rb @@ -27,7 +27,7 @@ # class ListingChange < ApplicationRecord - include Changable + include Changeable extend Mobility include TrackWhoDoesIt # Migrated to controller (Strong Parameters) diff --git a/app/models/taxon_common.rb b/app/models/taxon_common.rb index 91fe7915c..71e168d12 100644 --- a/app/models/taxon_common.rb +++ b/app/models/taxon_common.rb @@ -12,7 +12,7 @@ # class TaxonCommon < ApplicationRecord - include Changable + include Changeable include TrackWhoDoesIt # Migrated to controller (Strong Parameters) # attr_accessible :common_name_id, :taxon_concept_id, :created_by_id, diff --git a/app/models/taxon_concept_reference.rb b/app/models/taxon_concept_reference.rb index 3149de990..8d347c9de 100644 --- a/app/models/taxon_concept_reference.rb +++ b/app/models/taxon_concept_reference.rb @@ -15,7 +15,7 @@ # class TaxonConceptReference < ApplicationRecord - include Changable + include Changeable include TrackWhoDoesIt # Migrated to controller (Strong Parameters) # attr_accessible :reference_id, :taxon_concept_id, :is_standard, :is_cascaded, diff --git a/app/models/taxon_instrument.rb b/app/models/taxon_instrument.rb index f4731b050..e175bbb36 100644 --- a/app/models/taxon_instrument.rb +++ b/app/models/taxon_instrument.rb @@ -13,7 +13,7 @@ # class TaxonInstrument < ApplicationRecord - include Changable + include Changeable include TrackWhoDoesIt # Migrated to controller (Strong Parameters) # attr_accessible :effective_from, :instrument_id, :taxon_concept_id diff --git a/app/models/taxon_relationship.rb b/app/models/taxon_relationship.rb index f79992b99..10b0935cd 100644 --- a/app/models/taxon_relationship.rb +++ b/app/models/taxon_relationship.rb @@ -13,7 +13,7 @@ # class TaxonRelationship < ApplicationRecord - include Changable + include Changeable include TrackWhoDoesIt # Migrated to controller (Strong Parameters) # attr_accessible :taxon_concept_id, :other_taxon_concept_id, :taxon_relationship_type_id, diff --git a/app/models/trade_restrictions/cites_suspension.rb b/app/models/trade_restrictions/cites_suspension.rb index d32afb9e5..1e21a4c84 100644 --- a/app/models/trade_restrictions/cites_suspension.rb +++ b/app/models/trade_restrictions/cites_suspension.rb @@ -31,7 +31,7 @@ # class CitesSuspension < TradeRestriction - include Changable + include Changeable # Migrated to controller (Strong Parameters) # attr_accessible :start_notification_id, :end_notification_id, # :cites_suspension_confirmations_attributes, diff --git a/app/models/trade_restrictions/quota.rb b/app/models/trade_restrictions/quota.rb index be9709865..12709fb2e 100644 --- a/app/models/trade_restrictions/quota.rb +++ b/app/models/trade_restrictions/quota.rb @@ -31,7 +31,7 @@ # class Quota < TradeRestriction - include Changable + include Changeable # Migrated to controller (Strong Parameters) # attr_accessible :public_display From 5b1ddf5889d8aaf6d78334afbdd1bdf2339291a9 Mon Sep 17 00:00:00 2001 From: Daniel Perrett Date: Mon, 5 Feb 2024 17:00:53 +0000 Subject: [PATCH 171/241] docs: summarise Changeable concern --- app/models/concerns/changeable.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/models/concerns/changeable.rb b/app/models/concerns/changeable.rb index e2fa971d9..498cb6c94 100644 --- a/app/models/concerns/changeable.rb +++ b/app/models/concerns/changeable.rb @@ -1,3 +1,10 @@ +## +# This module is used by models which are referenced by or depend on the +# taxon_concepts table. It is responsible for two main areas: +# +# - altering the updated timestamp on affected taxon concepts on change +# - emptying the cache of taxon_concepts + module Changeable extend ActiveSupport::Concern From d968716473ebf57d1e0b8163138299fd79b34040 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Mon, 5 Feb 2024 17:25:37 +0000 Subject: [PATCH 172/241] Try to fix SQL issue after upgrade --- app/services/country_dictionary.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/services/country_dictionary.rb b/app/services/country_dictionary.rb index f215c8d1d..5a0817324 100644 --- a/app/services/country_dictionary.rb +++ b/app/services/country_dictionary.rb @@ -3,10 +3,9 @@ class CountryDictionary def initialize @collection = GeoEntity. - select([ - :"geo_entities.id", :name_en, :name_es, :name_fr, - :"UPPER(geo_entities.iso_code2) AS iso_code2" - ]). + select( + :id, :name_en, :name_es, :name_fr, 'UPPER(geo_entities.iso_code2) AS iso_code2' + ). joins(:geo_entity_type). where(:"geo_entity_types.name" => [GeoEntityType::COUNTRY, GeoEntityType::TERRITORY]). all From 59fda57751aa29cd32ae6d00eaa987b3f71c1ef6 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Mon, 5 Feb 2024 21:38:59 +0000 Subject: [PATCH 173/241] Update gems --- Gemfile | 1 - Gemfile.lock | 50 ++++++++++++++++++++++++-------------------------- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/Gemfile b/Gemfile index d43b4f749..38034777a 100644 --- a/Gemfile +++ b/Gemfile @@ -158,7 +158,6 @@ gem 'dotenv-rails', '2.0.1' gem 'sitemap_generator', '~> 6.3' gem 'appsignal', '~> 3.5', '>= 3.5.5' -gem 'test-unit', '3.1.5' # annoyingly, rails console won't start without it in staging / production ### GEM for frontend ### # Remove the `jquery-rails` gem to eliminate any dependency issues that may block the upgrade process. diff --git a/Gemfile.lock b/Gemfile.lock index 8d9314c52..d353f0293 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -102,11 +102,12 @@ GEM barber (0.12.2) ember-source (>= 1.0, < 3.1) execjs (>= 1.2, < 3) + base64 (0.2.0) bcrypt (3.1.20) bcrypt_pbkdf (1.1.0) bigdecimal (3.1.6) bindex (0.8.1) - bootsnap (1.18.1) + bootsnap (1.18.3) msgpack (~> 1.2) bootstrap-sass (2.3.2.2) sass (~> 3.2) @@ -141,11 +142,11 @@ GEM capistrano (>= 3.9.0) capistrano-bundler sidekiq (>= 6.0) - capybara (3.39.2) + capybara (3.40.0) addressable matrix mini_mime (>= 0.1.3) - nokogiri (~> 1.8) + nokogiri (~> 1.11) rack (>= 1.6.0) rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) @@ -192,7 +193,7 @@ GEM railties (>= 4.1.0) responders warden (~> 1.2.3) - diff-lcs (1.5.0) + diff-lcs (1.5.1) docile (1.4.0) dotenv (2.0.1) dotenv-rails (2.0.1) @@ -302,7 +303,7 @@ GEM mini_magick (4.12.0) mini_mime (1.1.5) mini_portile2 (2.8.5) - minitest (5.21.2) + minitest (5.22.0) mobility (1.2.9) i18n (>= 0.6.10, < 2) request_store (~> 1.0) @@ -314,7 +315,7 @@ GEM activerecord activesupport nested_form (0.3.2) - net-imap (0.4.9.1) + net-imap (0.4.10) date net-protocol net-pop (0.1.2) @@ -329,7 +330,7 @@ GEM net-protocol net-ssh (7.2.1) nio4r (2.7.0) - nokogiri (1.16.0) + nokogiri (1.16.2) mini_portile2 (~> 2.8.2) racc (~> 1.4) numerizer (0.1.1) @@ -355,7 +356,6 @@ GEM pg_search (2.3.6) activerecord (>= 5.2) activesupport (>= 5.2) - power_assert (2.0.3) prawn (0.13.2) pdf-reader (~> 1.2) ruby-rc4 @@ -419,20 +419,20 @@ GEM actionpack (>= 5.2) railties (>= 5.2) rexml (3.2.6) - rspec (3.12.0) - rspec-core (~> 3.12.0) - rspec-expectations (~> 3.12.0) - rspec-mocks (~> 3.12.0) + rspec (3.13.0) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) rspec-collection_matchers (1.2.1) rspec-expectations (>= 2.99.0.beta1) - rspec-core (3.12.2) - rspec-support (~> 3.12.0) - rspec-expectations (3.12.3) + rspec-core (3.13.0) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-mocks (3.12.6) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) + rspec-support (~> 3.13.0) rspec-rails (6.1.1) actionpack (>= 6.1) activesupport (>= 6.1) @@ -441,7 +441,7 @@ GEM rspec-expectations (~> 3.12) rspec-mocks (~> 3.12) rspec-support (~> 3.12) - rspec-support (3.12.1) + rspec-support (3.13.0) rubocop (1.60.2) json (~> 2.3) language_server-protocol (>= 3.17.0) @@ -481,7 +481,8 @@ GEM sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) tilt (>= 1.1, < 3) - selenium-webdriver (4.9.0) + selenium-webdriver (4.17.0) + base64 (~> 0.2) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) @@ -513,7 +514,7 @@ GEM slackistrano (0.1.9) capistrano (>= 3.0.1) json - spring (2.1.1) + spring (4.1.3) sprockets (3.7.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) @@ -534,8 +535,6 @@ GEM sync (0.5.0) term-ansicolor (1.7.1) tins (~> 1.0) - test-unit (3.1.5) - power_assert thor (1.3.0) tilt (2.3.0) timeout (0.4.1) @@ -555,10 +554,10 @@ GEM activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) - webdrivers (5.3.1) + webdrivers (5.2.0) nokogiri (~> 1.6) rubyzip (>= 1.3.0) - selenium-webdriver (~> 4.0, < 4.11) + selenium-webdriver (~> 4.0) websocket (1.2.10) websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) @@ -660,7 +659,6 @@ DEPENDENCIES sprockets-rails strong_migrations (~> 1.7) susy (~> 2.2, >= 2.2.14) - test-unit (= 3.1.5) uglifier (>= 1.3.0) uuidtools (~> 2.2) web-console (>= 4.1.0) From e585afb4531886ef443edd74a6e0668728c6e2e1 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Mon, 5 Feb 2024 21:52:35 +0000 Subject: [PATCH 174/241] Fix path, so it won't generate `/users/edit.1` where 1 is user id. The form always refer to current user, so user id in the path is not needed. --- app/views/shared/_topbar.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/_topbar.html.erb b/app/views/shared/_topbar.html.erb index f5121c167..7de587344 100644 --- a/app/views/shared/_topbar.html.erb +++ b/app/views/shared/_topbar.html.erb @@ -203,7 +203,7 @@