Why isnt' this multiple field key working in Mongoid? - ruby-on-rails

I've added this to my model:
key :name, :random_number
And I am using this callback:
before_create :create_random_number
But random_number is not getting appended to the _id using a method like this:
def create_random_number
rand(99999999999999999999)
end
This is the result that I get:
>> Product.create(name: "foo")
=> <Product _id: foo,

It turns out that you need to use after_initialize. This works for me:
key :slug
after_initialize :create_slug
def create_slug
name = self.name.gsub(' ', '-')
self.slug = "#{name}-#{rand(36**20).to_s(36)}"
end

Related

Rails serialization not dumping hash correctly

I have an active record model that has a column called configuration of type text. That column is serialized with a custom class, like so:
class MyModel < ApplicationRecord
serialize :configuration, MySerializer
end
The class MySerializer has the following class methods:
def dump(configuration)
configuration.to_json if configuration
end
def load(configuration)
obj = new
obj.json_hash = JSON.parse(configuration) if configuration.present?
obj
end
This instantiates an instance of the class MySerializer with the attr accessor json_hash.
Now, here's the problem, I'm doing:
MyModel.create(configuration: {"key" => 1})
And once I do MyModel.first, i get this:
...
configuration:
#<MySerializer:0x00000007faa558
#json_hash={"json_hash"=>{"key" => 1}
I was expecting getting something like:
#json_hash = {"key" => 1}
Any idea why I'd get the repeated key json_hash inside the attr accessor #json_hash ?
Thanks!
Why do you want to use MySerializer class?
Instead you can simply use as below:
serialize :configuration, Hash
Now do,
MyModel.create(configuration: {"key" => 1})
And try
MyModel.first

Ruby - Ignore protected attributes

How can I tell Ruby (Rails) to ignore protected variables which are present when mass-assigning?
class MyClass < ActiveRecord::Base
attr_accessible :name, :age
end
Now I will mass-assign a hash to create a new MyClass.
MyClass.create!({:name => "John", :age => 25, :id => 2})
This will give me an exception:
ActiveModel::MassAssignmentSecurity::Error: Can't mass-assign protected attributes: id
I want it to create a new MyClass with the specified (unprotected) attributes and ignore the id attribute.
On the side note: How can I also ignore unknown attributes. For example, MyClass doesn't have a location attribute. If I try to mass-assign it, just ignore it.
Use Hash#slice to only select the keys you're actually interested in assigning:
# Pass only :name and :age to create!
MyClass.create!(params.slice(:name, :age))
Typically, I'll add wrapper method for params to my controller which filters it down to only the fields that I know I want assigned:
class MyController
# ...
def create
#my_instance = MyClass.create!(create_params)
end
protected
def create_params
params.slice(:name, :age)
end
end
Setting mass_assignment_sanitizer to :logger solved the issue in development and test.
config.active_record.mass_assignment_sanitizer = :logger
You can use strong_parameters gem, that will be in rails 4.
See the documentation here.
This way you can specify the params you want by action or role, for example.
If you want to get down and dirty with it, and dynamically let only a model's attributes through, without disabling ActiveModel::MassAssignmentSecurity::Errors globally:
params = {:name => "John", :age => 25, :id => 2}
MyClass.create!(params.slice(*MyClass.new.attributes.symbolize_keys.keys)
The .symbolize_keys is required if you are using symbols in your hash, like in this situation, but you might not need that.
Personally, I like to keep things in the model by overriding assign_attributes.
def assign_attributes(new_attributes, options = {})
if options[:safe_assign]
authorizer = mass_assignment_authorizer(options[:as])
new_attributes = new_attributes.reject { |key|
!has_attribute?(key) || authorizer.deny?(key)
}
end
super(new_attributes, options)
end
Use it similarly to :without_protection, but for when you want to ignore unknown or protected attributes:
MyModel.create!(
{ :asdf => "invalid", :admin_field => "protected", :actual_data => 'hello world!' },
:safe_assign => true
)
# => #<MyModel actual_data: "hello world!">

ActiveRecord::Store with default values

Using the new ActiveRecord::Store for serialization, the docs give the following example implementation:
class User < ActiveRecord::Base
store :settings, accessors: [ :color, :homepage ]
end
Is it possible to declare attributes with default values, something akin to:
store :settings, accessors: { color: 'blue', homepage: 'rubyonrails.org' }
?
No, there's no way to supply defaults inside the store call. The store macro is quite simple:
def store(store_attribute, options = {})
serialize store_attribute, Hash
store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors
end
And all store_accessor does is iterate through the :accessors and create accessor and mutator methods for each one. If you try to use a Hash with :accessors you'll end up adding some things to your store that you didn't mean to.
If you want to supply defaults then you could use an after_initialize hook:
class User < ActiveRecord::Base
store :settings, accessors: [ :color, :homepage ]
after_initialize :initialize_defaults, :if => :new_record?
private
def initialize_defaults
self.color = 'blue' unless(color_changed?)
self.homepage = 'rubyonrails.org' unless(homepage_changed?)
end
end
I wanted to solve this too and ended up contributing to Storext:
class Book < ActiveRecord::Base
include Storext.model
# You can define attributes on the :data hstore column like this:
store_attributes :data do
author String
title String, default: "Great Voyage"
available Boolean, default: true
copies Integer, default: 0
end
end
try to use https://github.com/byroot/activerecord-typedstore gem. It allows you to set default value, use validation end other.
The following code has advantage of the defaults not being saved on every user record which reduces database storage usage and makes it easy in case if you want to change the defaults
class User < ApplicationRecord
DEFAULT_SETTINGS = { color: 'blue', homepage: 'rubyonrails.org' }
store :settings, accessors: DEFAULT_SETTINGS.keys
DEFAULT_SETTINGS.each do |key,value|
define_method(key) {
settings[key] or value
}
end
end
Here's what I just hacked together to solve this problem:
# migration
def change
add_column :my_objects, :settings, :text
end
# app/models/concerns/settings_accessors_with_defaults.rb
module SettingsAccessorsWithDefaults
extend ActiveSupport::Concern
included do
serialize :settings, Hash
cattr_reader :default_settings
end
def settings
self.class.default_settings.merge(self[:settings])
end
def restore_setting_to_default(key)
self[:settings].delete key
end
module ClassMethods
def load_default_settings(accessors_and_values)
self.class_variable_set '##default_settings', accessors_and_values
self.default_settings.keys.each do |key|
define_method("#{key}=") do |value|
self[:settings][key.to_sym] = value
end
define_method(key) do
self.settings[key.to_sym]
end
end
end
end
end
# app/models/my_object.rb
include SettingsAccessorsWithDefaults
load_default_settings(
attribute_1: 'default_value',
attribute_2: 'default_value_2'
)
validates :attribute_1, presence: true
irb(main):004:0> MyObject.default_settings
=> {:attribute_1=>'default_value', :attribute_2=>'default_value_2'}
irb(main):005:0> m = MyObject.last
=> #<MyObject ..., settings: {}>
irb(main):005:0> m.settings
=> {:attribute_1=>'default_value', :attribute_2=>'default_value_2'}
irb(main):007:0> m.attribute_1 = 'foo'
=> "foo"
irb(main):008:0> m.settings
=> {:attribute_1=>"foo", :attribute_2=>'default_value_2'}
irb(main):009:0> m
=> #<MyObject ..., settings: {:attribute_1=>"foo"}>

Strange behaviour when returning an array from class_eval'ed method

With Ruby 1.9.2, I'm using class_eval to extend a class.
def slugged(fields)
# assign string to variable only for easier debugging
method = <<-EOS
def slug_fields
#{ fields.is_a?(Array) ? fields.inspect : ":#{ fields }" }
end
EOS
class_eval method
end
This works fine as long as fields is a symbol (e.g. after slugged :name, slug_fields returns :name).
However, calling slugged with an array makes slug_fields returns nil (e.g. after slugged [:kicker, :headline], slug_fields returns nil).
Strangely, when debugging slugged, the string containing the to-be-created method looks exactly the way you would expect them to:
" def slug_fields\n [:kicker, :headline]\n end\n"
" def slug_fields\n :name\n end\n"
edit: as requested, a more complete version of what breaks for me:
module Extensions
module Slugged
extend ActiveSupport::Concern
included do
before_validation { |record| record.slug ||= record.sluggerize }
end
module ClassMethods
def slugged(fields)
# assign string to variable only for easier debugging
method = <<-EOS
def slug_fields
#{ fields.is_a?(Array) ? fields.inspect : ":#{ fields }" }
end
EOS
class_eval method
end
end
module InstanceMethods
def sluggerize
fields = slug_fields
slug_string = case
when fields.is_a?(Array)
fields.map { |f| self.send(f) }.join('-')
else
self.send fields
end
slug_string.parameterize
end
end
end
end
class Article < ActiveRecord::Base
include Extensions::Slugged
slugged [:kicker, :headline]
end
class Station < ActiveRecord::Base
include Extensions::Slugged
slugged :name
end
a = Article.new :headline => "this is a great headline!", :kicker => "attention-drawing kicker"
a.save # works, slug is set
s = Station.new :name => "Great Music"
s.save # TypeError: nil is not a symbol (in sluggerize where "self.send fields" is called)
Your code works fine for me under 1.9.2:
class Foo
class << self
def slugged(fields)
method = <<-EOS
def slug_fields
#{ fields.is_a?(Array) ? fields.inspect : ":#{ fields }" }
end
EOS
class_eval method
end
end
end
Foo.slugged :a
p Foo.new.slug_fields
#=> :a
Foo.slugged [:a,:b]
p Foo.new.slug_fields
#=> [:a, :b]
p RUBY_DESCRIPTION
#=> "ruby 1.9.2p180 (2011-02-18) [i386-mingw32]"
Can you please provide a complete, runnable, standalone test case that breaks for you?

How to update counter_cache when updating a model?

I have a simple relationship:
class Item
belongs_to :container, :counter_cache => true
end
class Container
has_many :items
end
Let's say I have two containers. I create an item and associate it with the first container. The counter is increased.
Then I decide to associate it with the other container instead. How to update the items_count column of both containers?
I found a possible solution at http://railsforum.com/viewtopic.php?id=39285 .. however I'm a beginner and I don't understand it. Is this the only way to do it?
It should work automatically. When you are updating items.container_id it will decreament old container's counter and increament new one. But if it isn't works - it is strange. You can try this callback:
class Item
belongs_to :container, :counter_cache => true
before_save :update_counters
private
def update_counters
new_container = Container.find self.container_id
old_container = Container.find self.container_id_was
new_container.increament(:items_count)
old_container.decreament(:items_count)
end
end
UPD
To demonstrate native behavior:
container1 = Container.create :title => "container 1"
#=> #<Container title: "container 1", :items_count: nil>
container2 = Container.create :title => "container 2"
#=> #<Container title: "container 2", :items_count: nil>
item = container1.items.create(:title => "item 1")
Container.first
#=> #<Container title: "container 1", :items_count: 1>
Container.last
#=> #<Container title: "container 1", :items_count: nil>
item.container = Container.last
item.save
Container.first
#=> #<Container title: "container 1", :items_count: 0>
Container.last
#=> #<Container title: "container 1", :items_count: 1>
So it should work without any hacking. From the box.
Modified it a bit to handle custom counter cache names
(Don't forget to add after_update :fix_updated_counter to the models using counter_cache)
module FixUpdateCounters
def fix_updated_counters
self.changes.each { |key, (old_value, new_value)|
# key should match /master_files_id/ or /bibls_id/
# value should be an array ['old value', 'new value']
if key =~ /_id/
changed_class = key.sub /_id$/, ''
association = self.association changed_class.to_sym
case option = association.options[ :counter_cache ]
when TrueClass
counter_name = "#{self.class.name.tableize}_count"
when Symbol
counter_name = option.to_s
end
next unless counter_name
association.klass.decrement_counter(counter_name, old_value) if old_value
association.klass.increment_counter(counter_name, new_value) if new_value
end
} end end
ActiveRecord::Base.send(:include, FixUpdateCounters)
For rails 3.1 users.
With rails 3.1, the answer doesn't work.
The following works for me.
private
def update_counters
new_container = Container.find self.container_id
Container.increment_counter(:items_count, new_container)
if self.container_id_was.present?
old_container = Container.find self.container_id_was
Container.decrement_counter(:items_count, old_container)
end
end
here is an approach that works well for me in similar situations
class Item < ActiveRecord::Base
after_update :update_items_counts, if: Proc.new { |item| item.collection_id_changed? }
private
# update the counter_cache column on the changed collections
def update_items_counts
self.collection_id_change.each do |id|
Collection.reset_counters id, :items
end
end
end
additional information on dirty object module http://api.rubyonrails.org/classes/ActiveModel/Dirty.html and an old video about them http://railscasts.com/episodes/109-tracking-attribute-changes and documentation on reset_counters http://apidock.com/rails/v3.2.8/ActiveRecord/CounterCache/reset_counters
Updates to #fl00r Answer
class Container
has_many :items_count
end
class Item
belongs_to :container, :counter_cache => true
after_update :update_counters
private
def update_counters
if container_id_changed?
Container.increment_counter(:items_count, container_id)
Container.decrement_counter(:items_count, container_id_was)
end
# other counters if any
...
...
end
end
I recently came across this same problem (Rails 3.2.3). Looks like it has yet to be fixed, so I had to go ahead and make a fix. Below is how I amended ActiveRecord::Base and utilize after_update callback to keep my counter_caches in sync.
Extend ActiveRecord::Base
Create a new file lib/fix_counters_update.rb with the following:
module FixUpdateCounters
def fix_updated_counters
self.changes.each {|key, value|
# key should match /master_files_id/ or /bibls_id/
# value should be an array ['old value', 'new value']
if key =~ /_id/
changed_class = key.sub(/_id/, '')
changed_class.camelcase.constantize.decrement_counter(:"#{self.class.name.underscore.pluralize}_count", value[0]) unless value[0] == nil
changed_class.camelcase.constantize.increment_counter(:"#{self.class.name.underscore.pluralize}_count", value[1]) unless value[1] == nil
end
}
end
end
ActiveRecord::Base.send(:include, FixUpdateCounters)
The above code uses the ActiveModel::Dirty method changes which returns a hash containing the attribute changed and an array of both the old value and new value. By testing the attribute to see if it is a relationship (i.e. ends with /_id/), you can conditionally determine whether decrement_counter and/or increment_counter need be run. It is essnetial to test for the presence of nil in the array, otherwise errors will result.
Add to Initializers
Create a new file config/initializers/active_record_extensions.rb with the following:
require 'fix_update_counters'
Add to models
For each model you want the counter caches updated add the callback:
class Comment < ActiveRecord::Base
after_update :fix_updated_counters
....
end
Here the #Curley fix to work with namespaced models.
module FixUpdateCounters
def fix_updated_counters
self.changes.each {|key, value|
# key should match /master_files_id/ or /bibls_id/
# value should be an array ['old value', 'new value']
if key =~ /_id/
changed_class = key.sub(/_id/, '')
# Get real class of changed attribute, so work both with namespaced/normal models
klass = self.association(changed_class.to_sym).klass
# Namespaced model return a slash, split it.
unless (counter_name = "#{self.class.name.underscore.pluralize.split("/")[1]}_count".to_sym)
counter_name = "#{self.class.name.underscore.pluralize}_count".to_sym
end
klass.decrement_counter(counter_name, value[0]) unless value[0] == nil
klass.increment_counter(counter_name, value[1]) unless value[1] == nil
end
}
end
end
ActiveRecord::Base.send(:include, FixUpdateCounters)
Sorry I don't have enough reputation to comment the answers.
About fl00r, I may see a problem if there is an error and save return "false", the counter has already been updated but it should have not been updated.
So I'm wondering if "after_update :update_counters" is more appropriate.
Curley's answer works but if you are in my case, be careful because it will check all the columns with "_id". In my case it is automatically updating a field that I don't want to be updated.
Here is another suggestion (almost similar to Satish):
def update_counters
if container_id_changed?
Container.increment_counter(:items_count, container_id) unless container_id.nil?
Container.decrement_counter(:items_count, container_id_was) unless container_id_was.nil?
end
end

Resources