Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge branch 'master' into set_binds

* master:
  don't establish a new connection when testing with `sqlite3_mem`.
  sqlite >= 3.8.0 supports partial indexes
  Don't try to get the subclass if the inheritance column doesn't exist
  Enum mappings are now exposed via class methods instead of constants.
  Fix fields_for documentation with index option [ci skip]
  quick pass through Active Record CHANGELOG. [ci skip]
  [ci skip] Grammar correction
  single quotes for controller generated routes
  [ci skip] Added alias to CSRF
  Set NameError#name
  • Loading branch information...
commit 09035e622f9876cddc558ea065929cb862ca0f13 2 parents c7cf7f4 + b233307
@tenderlove tenderlove authored
View
51 activerecord/CHANGELOG.md
@@ -1,3 +1,40 @@
+* Enable partial indexes for sqlite >= 3.8.0
+
+ See http://www.sqlite.org/partialindex.html
+
+ *Cody Cutrer*
+
+* Don't try to get the subclass if the inheritance column doesn't exist
+
+ The `subclass_from_attrs` method is called even if the column specified by
+ the `inheritance_column` setting doesn't exist. This prevents setting associations
+ via the attributes hash if the association name clashes with the value of the setting,
+ typically `:type`. This worked previously in Rails 3.2.
+
+ *Ujjwal Thaakar*
+
+* Enum mappings are now exposed via class methods instead of constants.
+
+ Example:
+
+ class Conversation < ActiveRecord::Base
+ enum status: [ :active, :archived ]
+ end
+
+ Before:
+
+ Conversation::STATUS # => { "active" => 0, "archived" => 1 }
+
+ After:
+
+ Conversation.statuses # => { "active" => 0, "archived" => 1 }
+
+ *Godfrey Chan*
+
+* Set `NameError#name` when STI-class-lookup fails.
+
+ *Chulki Lee*
+
* Fix bug in `becomes!` when changing from the base model to a STI sub-class.
Fixes #13272.
@@ -61,7 +98,7 @@
class: `ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig`.
To understand the exact behavior of this class, it is best to review the
- behavior in `activerecord/test/cases/connection_adapters/connection_handler_test.rb`
+ behavior in `activerecord/test/cases/connection_adapters/connection_handler_test.rb`.
*Richard Schneeman*
@@ -407,7 +444,7 @@
*kostya*, *Lauro Caetano*
* `type_to_sql` returns a `String` for unmapped columns. This fixes an error
- when using unmapped array types in PG
+ when using unmapped PostgreSQL array types.
Example:
@@ -446,7 +483,7 @@
* Update counter cache on a `has_many` relationship regardless of default scope.
- Fix #12952.
+ Fixes #12952.
*Uku Taht*
@@ -457,9 +494,10 @@
*Cody Cutrer*, *Yves Senn*
-* Raise `ActiveRecord::RecordNotDestroyed` when a replaced child marked with `dependent: destroy` fails to be destroyed.
+* Raise `ActiveRecord::RecordNotDestroyed` when a replaced child
+ marked with `dependent: destroy` fails to be destroyed.
- Fix #12812
+ Fixex #12812.
*Brian Thomas Storti*
@@ -1365,6 +1403,7 @@
*Yves Senn*
* Fix the `:primary_key` option for `has_many` associations.
+
Fixes #10693.
*Yves Senn*
@@ -1489,7 +1528,7 @@
* Trigger a save on `has_one association=(associate)` when the associate contents have changed.
- Fix #8856.
+ Fixes #8856.
*Chris Thompson*
View
18 activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -155,6 +155,10 @@ def supports_savepoints?
true
end
+ def supports_partial_index?
+ sqlite_version >= '3.8.0'
+ end
+
# Returns true, since this connection adapter supports prepared statement
# caching.
def supports_statement_cache?
@@ -397,13 +401,25 @@ def columns(table_name) #:nodoc:
# Returns an array of indexes for the given table.
def indexes(table_name, name = nil) #:nodoc:
exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", 'SCHEMA').map do |row|
+ sql = <<-SQL
+ SELECT sql
+ FROM sqlite_master
+ WHERE name=#{quote(row['name'])} AND type='index'
+ UNION ALL
+ SELECT sql
+ FROM sqlite_temp_master
+ WHERE name=#{quote(row['name'])} AND type='index'
+ SQL
+ index_sql = exec_query(sql).first['sql']
+ match = /\sWHERE\s+(.+)$/i.match(index_sql)
+ where = match[1] if match
IndexDefinition.new(
table_name,
row['name'],
row['unique'] != 0,
exec_query("PRAGMA index_info('#{row['name']}')", "SCHEMA").map { |col|
col['name']
- })
+ }, nil, nil, where)
end
end
View
17 activerecord/lib/active_record/enum.rb
@@ -56,21 +56,24 @@ module ActiveRecord
# In rare circumstances you might need to access the mapping directly.
# The mappings are exposed through a constant with the attributes name:
#
- # Conversation::STATUS # => { "active" => 0, "archived" => 1 }
+ # Conversation.statuses # => { "active" => 0, "archived" => 1 }
#
# Use that constant when you need to know the ordinal value of an enum:
#
- # Conversation.where("status <> ?", Conversation::STATUS[:archived])
+ # Conversation.where("status <> ?", Conversation.statuses[:archived])
module Enum
def enum(definitions)
klass = self
definitions.each do |name, values|
- # STATUS = { }
- enum_values = _enum_methods_module.const_set name.to_s.upcase, ActiveSupport::HashWithIndifferentAccess.new
+ # statuses = { }
+ enum_values = ActiveSupport::HashWithIndifferentAccess.new
name = name.to_sym
+ # def self.statuses statuses end
+ klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values }
+
_enum_methods_module.module_eval do
- # def status=(value) self[:status] = STATUS[value] end
+ # def status=(value) self[:status] = statuses[value] end
define_method("#{name}=") { |value|
if enum_values.has_key?(value) || value.blank?
self[name] = enum_values[value]
@@ -84,10 +87,10 @@ def enum(definitions)
end
}
- # def status() STATUS.key self[:status] end
+ # def status() statuses.key self[:status] end
define_method(name) { enum_values.key self[name] }
- # def status_before_type_cast() STATUS.key self[:status] end
+ # def status_before_type_cast() statuses.key self[:status] end
define_method("#{name}_before_type_cast") { enum_values.key self[name] }
pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index
View
24 activerecord/lib/active_record/inheritance.rb
@@ -18,13 +18,17 @@ def new(*args, &block)
if abstract_class? || self == Base
raise NotImplementedError, "#{self} is an abstract class and cannot be instantiated."
end
- if (attrs = args.first).is_a?(Hash)
- if subclass = subclass_from_attrs(attrs)
- return subclass.new(*args, &block)
- end
+
+ attrs = args.first
+ if subclass_from_attributes?(attrs)
+ subclass = subclass_from_attributes(attrs)
+ end
+
+ if subclass
+ subclass.new(*args, &block)
+ else
+ super
end
- # Delegate to the original .new
- super
end
# Returns +true+ if this does not need STI type condition. Returns
@@ -126,7 +130,7 @@ def compute_type(type_name)
end
end
- raise NameError, "uninitialized constant #{candidates.first}"
+ raise NameError.new("uninitialized constant #{candidates.first}", candidates.first)
end
end
@@ -172,7 +176,11 @@ def type_condition(table = arel_table)
# is not self or a valid subclass, raises ActiveRecord::SubclassNotFound
# If this is a StrongParameters hash, and access to inheritance_column is not permitted,
# this will ignore the inheritance column and return nil
- def subclass_from_attrs(attrs)
+ def subclass_from_attributes?(attrs)
+ columns_hash.include?(inheritance_column) && attrs.is_a?(Hash)
+ end
+
+ def subclass_from_attributes(attrs)
subclass_name = attrs.with_indifferent_access[inheritance_column]
if subclass_name.present? && subclass_name != self.name
View
36 activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
@@ -34,24 +34,30 @@ def test_bad_connection
end
def test_connect_with_url
- original_connection = ActiveRecord::Base.remove_connection
- tf = Tempfile.open 'whatever'
- url = "sqlite3://#{tf.path}"
- ActiveRecord::Base.establish_connection(url)
- assert ActiveRecord::Base.connection
- ensure
- tf.close
- tf.unlink
- ActiveRecord::Base.establish_connection(original_connection)
+ skip "can't establish new connection when using memory db" if in_memory_db?
+ begin
+ original_connection = ActiveRecord::Base.remove_connection
+ tf = Tempfile.open 'whatever'
+ url = "sqlite3://#{tf.path}"
+ ActiveRecord::Base.establish_connection(url)
+ assert ActiveRecord::Base.connection
+ ensure
+ tf.close
+ tf.unlink
+ ActiveRecord::Base.establish_connection(original_connection)
+ end
end
def test_connect_memory_with_url
- original_connection = ActiveRecord::Base.remove_connection
- url = "sqlite3:///:memory:"
- ActiveRecord::Base.establish_connection(url)
- assert ActiveRecord::Base.connection
- ensure
- ActiveRecord::Base.establish_connection(original_connection)
+ skip "can't establish new connection when using memory db" if in_memory_db?
+ begin
+ original_connection = ActiveRecord::Base.remove_connection
+ url = "sqlite3:///:memory:"
+ ActiveRecord::Base.establish_connection(url)
+ assert ActiveRecord::Base.connection
+ ensure
+ ActiveRecord::Base.establish_connection(original_connection)
+ end
end
def test_valid_column
View
4 activerecord/test/cases/base_test.rb
@@ -1301,9 +1301,11 @@ def test_compute_type_success
end
def test_compute_type_nonexistent_constant
- assert_raises NameError do
+ e = assert_raises NameError do
ActiveRecord::Base.send :compute_type, 'NonexistentModel'
end
+ assert_equal 'uninitialized constant ActiveRecord::Base::NonexistentModel', e.message
+ assert_equal 'ActiveRecord::Base::NonexistentModel', e.name
end
def test_compute_type_no_method_error
View
6 activerecord/test/cases/enum_test.rb
@@ -74,9 +74,9 @@ class EnumTest < ActiveRecord::TestCase
end
test "constant to access the mapping" do
- assert_equal 0, Book::STATUS[:proposed]
- assert_equal 1, Book::STATUS["written"]
- assert_equal 2, Book::STATUS[:published]
+ assert_equal 0, Book.statuses[:proposed]
+ assert_equal 1, Book.statuses["written"]
+ assert_equal 2, Book.statuses[:published]
end
test "building new objects with enum scopes" do
View
9 activerecord/test/cases/inheritance_test.rb
@@ -1,10 +1,11 @@
-require "cases/helper"
+require 'cases/helper'
require 'models/company'
require 'models/person'
require 'models/post'
require 'models/project'
require 'models/subscriber'
require 'models/vegetables'
+require 'models/shop'
class InheritanceTest < ActiveRecord::TestCase
fixtures :companies, :projects, :subscribers, :accounts, :vegetables
@@ -367,4 +368,10 @@ def test_instantiation_doesnt_try_to_require_corresponding_file
ensure
ActiveRecord::Base.store_full_sti_class = true
end
+
+ def test_sti_type_from_attributes_disabled_in_non_sti_class
+ phone = Shop::Product::Type.new(name: 'Phone')
+ product = Shop::Product.new(:type => phone)
+ assert product.save
+ end
end
View
2  activerecord/test/cases/schema_dumper_test.rb
@@ -190,6 +190,8 @@ def test_schema_dumps_partial_indices
assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)", using: :btree', index_definition
elsif current_adapter?(:MysqlAdapter) || current_adapter?(:Mysql2Adapter)
assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", using: :btree', index_definition
+ elsif current_adapter?(:SQLite3Adapter) && ActiveRecord::Base.connection.supports_partial_index?
+ assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "rating > 10"', index_definition
else
assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index"', index_definition
end
View
5 activerecord/test/models/shop.rb
@@ -5,6 +5,11 @@ class Collection < ActiveRecord::Base
class Product < ActiveRecord::Base
has_many :variants, :dependent => :delete_all
+ belongs_to :type
+
+ class Type < ActiveRecord::Base
+ has_many :products
+ end
end
class Variant < ActiveRecord::Base
View
5 activerecord/test/schema/schema.rb
@@ -557,9 +557,14 @@ def create_table(*args, &block)
create_table :products, force: true do |t|
t.references :collection
+ t.references :type
t.string :name
end
+ create_table :product_types, force: true do |t|
+ t.string :name
+ end
+
create_table :projects, force: true do |t|
t.string :name
t.string :type
View
2  activesupport/lib/active_support/core_ext/hash/conversions.rb
@@ -105,7 +105,7 @@ class << self
# hash = Hash.from_xml(xml)
# # => {"hash"=>{"foo"=>1, "bar"=>2}}
#
- # DisallowedType is raise if the XML contains attributes with <tt>type="yaml"</tt> or
+ # DisallowedType is raised if the XML contains attributes with <tt>type="yaml"</tt> or
# <tt>type="symbol"</tt>. Use <tt>Hash.from_trusted_xml</tt> to parse this XML.
def from_xml(xml, disallowed_types = nil)
ActiveSupport::XMLConverter.new(xml, disallowed_types).to_h
View
13 guides/source/form_helpers.md
@@ -751,7 +751,7 @@ You might want to render a form with a set of edit fields for each of a person's
<%= form_for @person do |person_form| %>
<%= person_form.text_field :name %>
<% @person.addresses.each do |address| %>
- <%= person_form.fields_for address, index: address do |address_form|%>
+ <%= person_form.fields_for address, index: address.id do |address_form|%>
<%= address_form.text_field :city %>
<% end %>
<% end %>
@@ -774,9 +774,16 @@ This will result in a `params` hash that looks like
{'person' => {'name' => 'Bob', 'address' => {'23' => {'city' => 'Paris'}, '45' => {'city' => 'London'}}}}
```
-Rails knows that all these inputs should be part of the person hash because you called `fields_for` on the first form builder. By specifying an `:index` option you're telling Rails that instead of naming the inputs `person[address][city]` it should insert that index surrounded by [] between the address and the city. If you pass an Active Record object as we did then Rails will call `to_param` on it, which by default returns the database id. This is often useful as it is then easy to locate which Address record should be modified. You can pass numbers with some other significance, strings or even `nil` (which will result in an array parameter being created).
+Rails knows that all these inputs should be part of the person hash because you
+called `fields_for` on the first form builder. By specifying an `:index` option
+you're telling Rails that instead of naming the inputs `person[address][city]`
+it should insert that index surrounded by [] between the address and the city.
+This is often useful as it is then easy to locate which Address record
+should be modified. You can pass numbers with some other significance,
+strings or even `nil` (which will result in an array parameter being created).
-To create more intricate nestings, you can specify the first part of the input name (`person[address]` in the previous example) explicitly, for example
+To create more intricate nestings, you can specify the first part of the input
+name (`person[address]` in the previous example) explicitly:
```erb
<%= fields_for 'person[address][primary]', address, index: address do |address_form| %>
View
10 guides/source/getting_started.md
@@ -231,7 +231,7 @@ Rails will create several files and a route for you.
```bash
create app/controllers/welcome_controller.rb
- route get "welcome/index"
+ route get 'welcome/index'
invoke erb
create app/views/welcome
create app/views/welcome/index.html.erb
@@ -272,13 +272,13 @@ Open the file `config/routes.rb` in your editor.
```ruby
Rails.application.routes.draw do
- get "welcome/index"
+ get 'welcome/index'
# The priority is based upon order of creation:
# first created -> highest priority.
#
# You can have the root of your site routed with "root"
- # root "welcome#index"
+ # root 'welcome#index'
#
# ...
```
@@ -295,7 +295,7 @@ root 'welcome#index'
```
`root 'welcome#index'` tells Rails to map requests to the root of the
-application to the welcome controller's index action and `get "welcome/index"`
+application to the welcome controller's index action and `get 'welcome/index'`
tells Rails to map requests to <http://localhost:3000/welcome/index> to the
welcome controller's index action. This was created earlier when you ran the
controller generator (`rails generate controller welcome index`).
@@ -328,7 +328,7 @@ Blog::Application.routes.draw do
resources :posts
- root "welcome#index"
+ root 'welcome#index'
end
```
View
4 guides/source/security.md
@@ -150,7 +150,7 @@ Another countermeasure is to _save user-specific properties in the session_, ver
### Session Expiry
-NOTE: _Sessions that never expire extend the time-frame for attacks such as cross-site reference forgery (CSRF), session hijacking and session fixation._
+NOTE: _Sessions that never expire extend the time-frame for attacks such as cross-site request forgery (CSRF), session hijacking and session fixation._
One possibility is to set the expiry time-stamp of the cookie with the session id. However the client can edit cookies that are stored in the web browser so expiring sessions on the server is safer. Here is an example of how to _expire sessions in a database table_. Call `Session.sweep("20 minutes")` to expire sessions that were used longer than 20 minutes ago.
@@ -354,7 +354,7 @@ Having one single place in the admin interface or Intranet, where the input has
Refer to the Injection section for countermeasures against XSS. It is _recommended to use the SafeErb plugin_ also in an Intranet or administration interface.
-**CSRF** Cross-Site Reference Forgery (CSRF) is a gigantic attack method, it allows the attacker to do everything the administrator or Intranet user may do. As you have already seen above how CSRF works, here are a few examples of what attackers can do in the Intranet or admin interface.
+**CSRF** Cross-Site Request Forgery (CSRF), also known as Cross-Site Reference Forgery (XSRF), is a gigantic attack method, it allows the attacker to do everything the administrator or Intranet user may do. As you have already seen above how CSRF works, here are a few examples of what attackers can do in the Intranet or admin interface.
A real-world example is a [router reconfiguration by CSRF](http://www.h-online.com/security/Symantec-reports-first-active-attack-on-a-DSL-router--/news/102352). The attackers sent a malicious e-mail, with CSRF in it, to Mexican users. The e-mail claimed there was an e-card waiting for them, but it also contained an image tag that resulted in a HTTP-GET request to reconfigure the user's router (which is a popular model in Mexico). The request changed the DNS-settings so that requests to a Mexico-based banking site would be mapped to the attacker's site. Everyone who accessed the banking site through that router saw the attacker's fake web site and had their credentials stolen.
View
4 railties/CHANGELOG.md
@@ -1,3 +1,7 @@
+* Write controller generated routes in routes.rb with single quotes.
+
+ *Cristian Mircea Messel*
+
* Only lookup `config.log_level` for stdlib `::Logger` instances.
Assign it as is for third party loggers like `Log4r::Logger`.
View
6 railties/lib/rails/generators/rails/controller/controller_generator.rb
@@ -23,7 +23,7 @@ def add_routes
# Will generate -
# namespace :foo do
# namespace :bar do
- # get "baz/index"
+ # get 'baz/index'
# end
# end
def generate_routing_code(action)
@@ -36,8 +36,8 @@ def generate_routing_code(action)
end.join
# Create route
- # get "baz/index"
- route = indent(%{get "#{file_name}/#{action}"\n}, depth * 2)
+ # get 'baz/index'
+ route = indent(%{get '#{file_name}/#{action}'\n}, depth * 2)
# Create `end` ladder
# end
View
4 railties/test/generators/controller_generator_test.rb
@@ -67,7 +67,7 @@ def test_invokes_default_template_engine
def test_add_routes
run_generator
- assert_file "config/routes.rb", /get "account\/foo"/, /get "account\/bar"/
+ assert_file "config/routes.rb", /get 'account\/foo'/, /get 'account\/bar'/
end
def test_invokes_default_template_engine_even_with_no_action
@@ -91,6 +91,6 @@ def test_actions_are_turned_into_methods
def test_namespaced_routes_are_created_in_routes
run_generator ["admin/dashboard", "index"]
- assert_file "config/routes.rb", /namespace :admin do\n\s+get "dashboard\/index"\n/
+ assert_file "config/routes.rb", /namespace :admin do\n\s+get 'dashboard\/index'\n/
end
end
View
2  railties/test/generators/namespaced_generators_test.rb
@@ -63,7 +63,7 @@ def test_invokes_default_template_engine
def test_routes_should_not_be_namespaced
run_generator
- assert_file "config/routes.rb", /get "account\/foo"/, /get "account\/bar"/
+ assert_file "config/routes.rb", /get 'account\/foo'/, /get 'account\/bar'/
end
def test_invokes_default_template_engine_even_with_no_action
Please sign in to comment.
Something went wrong with that request. Please try again.