I have the following class:
class Profile < ActiveRecord::Base
serialize :data
end
Profile has a single column data that holds a serialized hash. I would like to define accessors into that hash such that I can execute profile.name instead of profile.data['name']. Is that possible in Rails?
The simple straightforward way:
class Profile < ActiveRecord::Base
serialize :data
def name
self.data['name']
end
def some_other_attribute
self.data['some_other_attribute']
end
end
You can see how that can quickly become cumbersome if you have lots of attributes within the data hash that you want to access.
So here's a more dynamic way to do it and it would work for any such top level attribute you want to access within data:
class Profile < ActiveRecord::Base
serialize :data
def method_missing(attribute, *args, &block)
return super unless self.data.key? attribute
self.data.fetch(attribute)
end
# good practice to extend respond_to? when using method_missing
def respond_to?(attribute, include_private = false)
super || self.data.key?(attribute)
end
end
With the latter approach you can just define method_missing and then call any attribute on #profile that is a key within data. So calling #profile.name would go through method_missing and grab the value from self.data['name']. This will work for whatever keys are present in self.data. Hope that helps.
Further reading:
http://www.trottercashion.com/2011/02/08/rubys-define_method-method_missing-and-instance_eval.html
http://technicalpickles.com/posts/using-method_missing-and-respond_to-to-create-dynamic-methods/
class Profile < ActiveRecord::Base
serialize :data # always a hash or nil
def name
data[:name] if data
end
end
I'm going to answer my own question. It looks like ActiveRecord::Store is what I want:
http://api.rubyonrails.org/classes/ActiveRecord/Store.html
So my class would become:
class Profile < ActiveRecord::Base
store :data, accessors: [:name], coder: JSON
end
I'm sure everyone else's solutions work just fine, but this is so clean.
class Profile < ActiveRecord::Base
serialize :data # always a hash or nil
["name", "attr2", "attr3"].each do |method|
define_method(method) do
data[method.to_sym] if data
end
end
end
Ruby is extremely flexible and your model is just a Ruby Class. Define the "accessor" method you want and the output you desire.
class Profile < ActiveRecord::Base
serialize :data
def name
data['name'] if data
end
end
However, that approach is going to lead to a lot of repeated code. Ruby's metaprogramming features can help you solve that problem.
If every profile contains the same data structure you can use define_method
[:name, :age, :location, :email].each do |method|
define_method method do
data[method] if data
end
end
If the profile contains unique information you can use method_missing to attempt to look into the hash.
def method_missing(method, *args, &block)
if data && data.has_key?(method)
data[method]
else
super
end
end
I am pretty new to rails (and development) and have a requirement to create a change log. Let's say you have an employees table. On that table you have an employee reference number, a first name, and a last name. When either the first name or last name changes, I need to log it to a table somewhere for later reporting. I only need to log the change, so if employee ref 1 changes from Bill to Bob, then I need to put the reference number and first name into a table. The change table can have all the columns that mnight change, but most only be populated with the reference number and the changed field. I don't need the previous value either, just the new one. hope that makes sense.
Looked at gems such as paper trail, but they seem very complicated for what I need. I don't ever need to manipulate the model or move versions etc, I just need to track which fields have changed, when, and by whom.
I'd appreciate your recommendations.
If you insist on building your own changelog, based on your requirements you can do so using a few callbacks. First create your log table:
def up
create_table :employee_change_logs do |t|
t.references :employee
# as per your spec - copy all column definitions from your employees table
end
end
In your Employee model:
class Employee < ActiveRecord::Base
has_many :employee_change_logs
before_update :capture_changed_columns
after_update :log_changed_columns
# capture the changes before the update occurs
def capture_changed_columns
#changed_columns = changed
end
def log_changed_columns
return if #changed_columns.empty?
log_entry = employee_change_logs.build
#changed_columns.each{|c| log_entry.send(:"#{c}=", self.send(c))}
log_entry.save!
end
end
I recommend the gem vestal_versions.
To version an ActiveRecord model, simply add versioned to your class like so:
class User < ActiveRecord::Base
versioned
validates_presence_of :first_name, :last_name
def name
"#{first_name} #{last_name}"
end
end
And use like this:
#user.update_attributes(:last_name => "Jobs", :updated_by => "Tyler")
#user.version # => 2
#user.versions.last.user # => "Tyler"
The first thing we did was put an around filter in the application controller. This was how I get the current_employee into the employee model, which was the challenge, especially for a newbie like me!
around_filter :set_employee_for_log, :if => Proc.new { #current_account &&
#current_account.log_employee_changes? && #current_employee }
def set_employee_for_log
Thread.current[:current_employee] = #current_employee.id
begin
yield
ensure
Thread.current[:current_employee ] = nil
end
end
end
Next, in the employee model I defined which fields I was interested in monitoring
CHECK_FIELDS = ['first_name', 'last_name', 'middle_name']
then I added some hooks to actually capture the changes IF logging is enabled at the account level
before_update :capture_changed_columns
after_update :log_changed_columns, :if => Proc.new { self.account.log_employee_changes? }
def capture_changed_columns
#changed_columns = changed
#changes = changes
end
def log_changed_columns
e = EmployeeChangeLog.new
Employee::CHECK_FIELDS.each do |field|
if self.send("#{field}_changed?")
e.send("#{field}=", self.send(field))
end
end
if e.changed?
e.update_attribute(:account_id, self.account.id)
e.update_attribute(:employee_id, self.id)
e.update_attribute(:employee_ref, self.employee_ref)
e.update_attribute(:user_id, Thread.current[:current_employee])
e.save
else return
end
end
And that;s it. If the account enables it, the app keeps an eye on specific fields and then all changes to those fields are logged to a table, creating an simple audit trail.
I have a simple user table and i would like to add a new field to my user table that says permalink. This permalink would be updated with the following code for all the users
name.downcase.gsub(/[^0-9a-z]+/, ' ').strip.gsub(' ', '-'). I want to create a migration file that updates all the users permalink fields with the code above so that old users would have their permalink set and i would use an after_create method for new users.
I think you could try something like this:
class User < ActiveRecord::Base
before_create :set_permalink
def set_permalink
permalink = name.downcase.gsub(/[^0-9a-z]+/, ' ').strip.gsub(' ', '-')
end
end
This actually uses the before_create callback, which would address the deriving of the permalink field for new users. I think this is what you actually need.
And in your migration file ...
class UpdateUsersPermalink < ActiveRecord::Migration
def self.up
User.reset_column_information
User.all.each do |u|
if u.permalink.nil?
u.set_permalink
u.save!
end
end
end
...
end
.. which would take care of any existing Users that do not have this field set just yet.
I've found several solutions for this problem, for example railstat from this post:
Page views in Rails
I have a bunch of articles and reviews which I would like a hit counter filtered by unique IPs. Exactly like Stackoverflow does for this post. But I don't really care for such a solution as railstat when google analytics is already doing this for me and including a whole lot of code, keeping track of unique IPs, etc.. My present thinking is to use Garb or some other Analytics plugin to pull the pages stats if they are older than say 12 hours updating some table, but I also need a cache_column.
I'm assuming you can pull stats from Analytics for a particular page and that they update their stats every 12 hours?
I'm wondering if there are any reasons why this would be a bad idea, or if someone has a better solution?
Thanks
UPDATE
The code in this answer was used as a basis for http://github.com/charlotte-ruby/impressionist
Try it out
It would probably take you less time to code this into your app then it would to pull the data from Analytics using their API. This data would most likely be more accurate and you would not have to rely an an external dependancy.. also you would have the stats in realtime instead of waiting 12 hours on Analytics data. request.remote_ip works pretty well. Here is a solution using polymorphism. Please note that this code is untested, but it should be close.
Create a new model/migration to store your page views (impressions):
class Impressions < ActiveRecord::Base
belongs_to :impressionable, :polymorphic=>true
end
class CreateImpressionsTable < ActiveRecord::Migration
def self.up
create_table :impressions, :force => true do |t|
t.string :impressionable_type
t.integer :impressionable_id
t.integer :user_id
t.string :ip_address
t.timestamps
end
end
def self.down
drop_table :impressions
end
end
Add a line to your Article model for the association and add a method to return the impression count:
class Article < ActiveRecord::Base
has_many :impressions, :as=>:impressionable
def impression_count
impressions.size
end
def unique_impression_count
# impressions.group(:ip_address).size gives => {'127.0.0.1'=>9, '0.0.0.0'=>1}
# so getting keys from the hash and calculating the number of keys
impressions.group(:ip_address).size.keys.length #TESTED
end
end
Create a before_filter for articles_controller on the show action:
before_filter :log_impression, :only=> [:show]
def log_impression
#article = Article.find(params[:id])
# this assumes you have a current_user method in your authentication system
#article.impressions.create(ip_address: request.remote_ip,user_id:current_user.id)
end
Then you just call the unique_impression_count in your view
<%=#article.unique_impression_count %>
If you are using this on a bunch of models, you may want to DRY it up. Put the before_filter def in application_controller and use something dynamic like:
impressionable_class = controller_name.gsub("Controller","").constantize
impressionable_instance = impressionable_class.find(params[:id])
impressionable_instance.impressions.create(ip_address:request.remote_ip,user_id:current_user.id)
And also move the code in the Article model to a module that will be included in ActiveRecord::Base. You could put the send include in a config/initializer.. or if you want to get crazy, just turn the whole thing into a rails engine, so you can reuse on other apps.
module Impressionable
def is_impressionable
has_many :impressions, :as=>:impressionable
include InstanceMethods
end
module InstanceMethods
def impression_count
impressions.size
end
def unique_impression_count
impressions.group(:ip_address).size
end
end
end
ActiveRecord::Base.extend Impressionable
I'm trying to find the best way to set default values for objects in Rails.
The best I can think of is to set the default value in the new method in the controller.
Does anyone have any input if this is acceptable or if there's a better way to do it?
"Correct" is a dangerous word in Ruby. There's usually more than one way to do anything. If you know you'll always want that default value for that column on that table, setting them in a DB migration file is the easiest way:
class SetDefault < ActiveRecord::Migration
def self.up
change_column :people, :last_name, :type, :default => "Doe"
end
def self.down
# You can't currently remove default values in Rails
raise ActiveRecord::IrreversibleMigration, "Can't remove the default"
end
end
Because ActiveRecord autodiscovers your table and column properties, this will cause the same default to be set in any model using it in any standard Rails app.
However, if you only want default values set in specific cases -- say, it's an inherited model that shares a table with some others -- then another elegant way is do it directly in your Rails code when the model object is created:
class GenericPerson < Person
def initialize(attributes=nil)
attr_with_defaults = {:last_name => "Doe"}.merge(attributes)
super(attr_with_defaults)
end
end
Then, when you do a GenericPerson.new(), it'll always trickle the "Doe" attribute up to Person.new() unless you override it with something else.
Based on SFEley's answer, here is an updated/fixed one for newer Rails versions:
class SetDefault < ActiveRecord::Migration
def change
change_column :table_name, :column_name, :type, default: "Your value"
end
end
First of all you can't overload initialize(*args) as it's not called in all cases.
Your best option is to put your defaults into your migration:
add_column :accounts, :max_users, :integer, :default => 10
Second best is to place defaults into your model but this will only work with attributes that are initially nil. You may have trouble as I did with boolean columns:
def after_initialize
if new_record?
max_users ||= 10
end
end
You need the new_record? so the defaults don't override values loaded from the datbase.
You need ||= to stop Rails from overriding parameters passed into the initialize method.
You can also try change_column_default in your migrations (tested in Rails 3.2.8):
class SetDefault < ActiveRecord::Migration
def up
# Set default value
change_column_default :people, :last_name, "Smith"
end
def down
# Remove default
change_column_default :people, :last_name, nil
end
end
change_column_default Rails API docs
If you are referring to ActiveRecord objects, you have (more than) two ways of doing this:
1. Use a :default parameter in the DB
E.G.
class AddSsl < ActiveRecord::Migration
def self.up
add_column :accounts, :ssl_enabled, :boolean, :default => true
end
def self.down
remove_column :accounts, :ssl_enabled
end
end
More info here: http://api.rubyonrails.org/classes/ActiveRecord/Migration.html
2. Use a callback
E.G. before_validation_on_create
More info here: http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html#M002147
In Ruby on Rails v3.2.8, using the after_initialize ActiveRecord callback, you can call a method in your model that will assign the default values for a new object.
after_initialize callback is triggered for each object that is found and instantiated by a finder, with after_initialize being triggered after new objects are instantiated as well
(see ActiveRecord Callbacks).
So, IMO it should look something like:
class Foo < ActiveRecord::Base
after_initialize :assign_defaults_on_new_Foo
...
attr_accessible :bar
...
private
def assign_defaults_on_new_Foo
# required to check an attribute for existence to weed out existing records
self.bar = default_value unless self.attribute_whose_presence_has_been_validated
end
end
Foo.bar = default_value for this instance unless the instance contains an attribute_whose_presence_has_been_validated previously on save/update. The default_value will then be used in conjunction with your view to render the form using the default_value for the bar attribute.
At best this is hacky...
EDIT - use 'new_record?' to check if instantiating from a new call
Instead of checking an attribute value, use the new_record? built-in method with rails. So, the above example should look like:
class Foo < ActiveRecord::Base
after_initialize :assign_defaults_on_new_Foo, if: 'new_record?'
...
attr_accessible :bar
...
private
def assign_defaults_on_new_Foo
self.bar = default_value
end
end
This is much cleaner. Ah, the magic of Rails - it's smarter than me.
In case you're dealing with a Model, you can use the Attriutes API in Rails 5+
http://api.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html#method-i-attribute
just add a migration with a proper column name and then in the model set it with:
class StoreListing < ActiveRecord::Base
attribute :country, :string, default: 'PT'
end
For boolean fields in Rails 3.2.6 at least, this will work in your migration.
def change
add_column :users, :eula_accepted, :boolean, default: false
end
Putting a 1 or 0 for a default will not work here, since it is a boolean field. It must be a true or false value.
Generate a migration and use change_column_default, is succinct and reversible:
class SetDefaultAgeInPeople < ActiveRecord::Migration[5.2]
def change
change_column_default :people, :age, { from: nil, to: 0 }
end
end
If you are just setting defaults for certain attributes of a database backed model I'd consider using sql default column values - can you clarify what types of defaults you are using?
There are a number of approaches to handle it, this plugin looks like an interesting option.
The suggestion to override new/initialize is probably incomplete. Rails will (frequently) call allocate for ActiveRecord objects, and calls to allocate won't result in calls to initialize.
If you're talking about ActiveRecord objects, take a look at overriding after_initialize.
These blog posts (not mine) are useful:
Default values
Default constructors not called
[Edit: SFEley points out that Rails actually does look at the default in the database when it instantiates a new object in memory - I hadn't realized that.]
I needed to set a default just as if it was specified as default column value in DB. So it behaves like this
a = Item.new
a.published_at # => my default value
a = Item.new(:published_at => nil)
a.published_at # => nil
Because after_initialize callback is called after setting attributes from arguments, there was no way to know if the attribute is nil because it was never set or because it was intentionally set as nil. So I had to poke inside a bit and came with this simple solution.
class Item < ActiveRecord::Base
def self.column_defaults
super.merge('published_at' => Time.now)
end
end
Works great for me. (Rails 3.2.x)
A potentially even better/cleaner potential way than the answers proposed is to overwrite the accessor, like this:
def status
self['name_of_var'] || 'desired_default_value'
end
See "Overwriting default accessors" in the ActiveRecord::Base documentation and more from StackOverflow on using self.
i answered a similar question here.. a clean way to do this is using Rails attr_accessor_with_default
class SOF
attr_accessor_with_default :is_awesome,true
end
sof = SOF.new
sof.is_awesome
=> true
UPDATE
attr_accessor_with_default has been deprecated in Rails 3.2.. you could do this instead with pure Ruby
class SOF
attr_writer :is_awesome
def is_awesome
#is_awesome ||= true
end
end
sof = SOF.new
sof.is_awesome
#=> true
If you're talking about ActiveRecord objects, I use the 'attribute-defaults' gem.
Documentation & download: https://github.com/bsm/attribute-defaults
You could use the rails_default_value gem. eg:
class Foo < ActiveRecord::Base
# ...
default :bar => 'some default value'
# ...
end
https://github.com/keithrowell/rails_default_value
You can override the constructor for the ActiveRecord model.
Like this:
def initialize(*args)
super(*args)
self.attribute_that_needs_default_value ||= default_value
self.attribute_that_needs_another_default_value ||= another_default_value
#ad nauseum
end