I need defaults (from a remote service) in ModelA to be set before the object is passed to the view from ModelAController#new. I have used after_initialize for this. However, in #create I have a problem. If I use model_b.create_model_a(some_attributes), the attributes are passed in during initialization and then overwritten by the after_initialize call:
class ModelA < ActiveRecord::Base
after_initialize :set_defaults, if: :new_record?
def set_defaults
self.c = "default"
#actually a remote call, not something can be set as database default
end
end
class ModelB < ActiveRecord::Base
belongs_to :model_a
end
class ModelAController < ApplicationController
#ModelA is nested under ModelB in routes.rb
#GET /model_bs/:model_b_id/model_as/new
def new
model_b = ModelB.find(params[:model_b_id])
#no problem
respond_with model_b.build_model_a
end
#POST /model_bs/:model_b_id/model_as
def create
model_b = ModelB.find(params[:id])
#problem:
respond_with model_b.create_model_a({c: "not default"})
#at this point the model_a instance still has attribute c set to "default"
end
...
end
I could separate the create steps:
model_b = ModelB.find(params[:id])
model_a = model_b.build_model_a #should fire after_initialize
model_a.update_attributes({c: "not default"}) #overwrite default c value
But I feel like this makes the lifecycle of ModelA a bit of a trap for other programmers. This looks like an obvious candidate for refactoring the last two lines into one, but that would create this problem again. Is there a neater solution?
Make a conditional assignment:
def set_defaults
self.c ||= "default"
end
Alternatively instead of after_initialize hook set a default in attribute reader. That way you only set the default when you actually need attribute value, so it saves you a remote call if you don't need it:
def c
super || self.c = 'default'
end
Related
In my model, I'm trying to dynamically expose objects that are inside an array as a top level attribute. Here's the code snippet:
class Widget < ActiveRecord::Base
# attr_accessor :name
end
class MyModel < ActiveRecord::Base
has_many :widgets
#attr_accessor :widgets
after_initialize :init_widgets
def init_widgets
widgets
widgets.each_with_index do |widget, index|
define_method(widget.name) do
widgets[index]
end
end
end
end
Is there any way for me to define the value of index into the new method I am creating so that it will be associated with the proper index?
I might create an accessor / assignment methods that overload the [] operator:
class BracketOperator
def initialize
#values = (1..100).to_a
end
def [](index)
#values[index]
end
def []=(index, value)
#values[index] = value
end
end
bo = BracketOperator.new
bo[3] # => 4
bo[3] = 17
bo[3] # => 17
So for reference, here's the answer I came up with. Apparently I don't understand scoping for ruby too well. The variable n somehow remains referenced to the n within the each loop. Hence for my original question, I can just use the index variable within the method and it will be mapped to what I am expecting it to be mapped to.
class Test
def setup
names = [ "foo","bar" ]
names.each do |n|
self.class.send :define_method, n do
puts "method is called #{n}!"
end
end
end
end
Given a model Orderstatus with attributes private_status:string, and private_status_history:json(I'm using Postgresql's json). I would like to record each status transition, together with the user who made the change.
Ideally it would be something like:
class Orderstatus < ActiveRecord::Base
after_save :track_changes
def track_changes
changes = self.changes
if self.private_status_changed?
self.private_status_history_will_change!
self.private_status_history.append({
type: changes[:private_status],
user: current_user.id
})
end
end
end
class OrderstatusController <ApplicationController
def update
if #status.update_attributes(white_params)
# Good response
else
# Bad response
end
end
end
#Desired behaviour (process not run with console)
status = Orderstatus.new(private_status:'one')
status.private_status #=> 'one'
status.private_status_history #=> []
status.update_attributes({:private_status=>'two'}) #=>true
status.private_status #=> 'two'
status.private_status_history #=> [{type:['one','two'],user:32]
What would be the recommended practice to achieve this? Apart from the usual one using Thread. Or maybe, any suggestion to refactor the structure of the app?
So, I finally settled for this option ( I hope it's not alarming to anyone :S)
class Orderstatus < ActiveRecord::Base
after_save :track_changes
attr_accessor :modifying_user
def track_changes
changes = self.changes
if self.private_status_changed?
newchange = {type:changes[:private_status],user: modifying_user.id}
self.update_column(:private_status_history,
self.private_status_history.append(newchange))
end
end
end
class OrderstatusController <ApplicationController
def update
#status.modifying_user = current_user # <---- HERE!
if #status.update_attributes(white_params)
# Good response
else
# Bad response
end
end
end
Notes:
- I pass the from the Controller to the Model through an instance attribute modifying_user of the class Orderstatus. That attribute is ofc not saved to the db.
- Change of method to append new changes to the history field. I.e. attr_will_change! + save to update_column + append
I have a model base_table, and I have a extended_table which has extra properties to further extend my base_table. (I would have different extended_tables, to add different properties to my base_table, but that's non-related to the question I'm asking here).
The model definition for my base_table is like:
class BaseTable < ActiveRecord::Base
module BaseTableInclude
def self.included(base)
base.belongs_to :base_table, autosave:true, dependent: :destroy
# do something more when this module is included
end
end
end
And the model definition for my extended_table is like:
class TennisQuestionaire < ActiveRecord::Base
include BaseTable::BaseTableInclude
end
Now I what I want is the code below:
params = {base_table: {name:"Songyy",age:19},tennis_ball_num:3}
t = TennisQuestionaire.new(params)
When I created my t, I want the base_table to be instantiated as well.
One fix I can come up with, is to parse the params to create the base_table object, before TennisQuestionaire.new was called upon the params. It's something like having a "before_new" filter here. But I cannot find such kind of filter when I was reading the documentation.
Additionally, I think another way is to override the 'new' method. But this is not so clean.
NOTE: There's one method called accepts_nested_attributes_for, seems to do what I want, but it doesn't work upon a belongs_to relation.
Any suggestions would be appreciated. Thanks :)
After some trails&error, the solution is something like this:
class BaseTable < ActiveRecord::Base
module BaseTableInclude
def initialize(*args,&block)
handle_res = handle_param_args(args) { |params| params[:base_table] = BaseTable.new(params[:base_table]) }
super(*args,&block)
end
private
def handle_param_args(args)
return unless block_given?
if args.length > 0
params = args[0]
if (params.is_a? Hash) and params[:base_table].is_a? Hash
yield params
end
end
end
end
end
Is there any built-in way to attach an instance variable to a record? For example, suppose I have a User class with a foo attr_accessor:
class User < ActiveRecord::Base
...
attr_accessor :foo
end
If I do
u = User.first
u.foo = "bar"
u.foo # -> "bar"
User.find(1).foo # => nil
This is the correct behavior and I understand why: the instance variable exists for the instance (in memory) and not for the record. What I want is something like a record variable, so that in the above example u.foo and User.find(1).foo return the same value within the same instance of my application. I don't want persistence: it's not appropriate for foo to be a column in the table, it's just appropriate for foo to return the same value for the same record during the life cycle of e.g., a controller action, console session, etc. Nor do I want a class variable via cattr_accessor, because there's no reason that User.find(1).foo should be the same as User.find(2).foo.
The best bet I can come up with is to fake it with a class variable array and instance methods to get/set the appropriate element of the array:
class User < ActiveRecord::Base
cattr_accessor :all_the_foos
self.all_the_foos = Array.new
def foo
self.all_the_foos[id]
end
def foo= new_foo
self.all_the_foos[id] = new_foo
end
end
This ALMOST works, but it doesn't work with un-saved records, e.g. User.new.foo = "bar" fails.
You can use Thread.current to set variables that are active within the context of a controller action invocation. Your current implementation doesn't guarantee context reset across calls.
class User < ActiveRecord::Base
after_create :set_thread_var
def foo
new_record? ? #foo : Thread.current["User-foo-#{id}"]
end
def foo=(val)
new_record? ? (#foo = val) : (Thread.current["User-foo-#{id}"] = val)
end
private
def set_thread_var
Thread.current["User-foo-#{id}"] = #foo if defined?(#foo)
end
end
I can't think of a better idea than your class variable solution. To solve your "new" problem I'd use after_initialize.
class User < ActiveRecord::Base
after_initialize :init_foos
cattr_accessor :all_the_foos
def foo
self.all_the_foos[id]
end
def foo= new_foo
self.all_the_foos[id] = new_foo
end
private
def init_foos
##all_the_foos ||= []
end
end
I have an ActiveRecord model with several virtual attribute setters. I want to build an object but not save it to the database. One setter must execute before the others. How to do?
As a workaround, I build the object in two steps
#other_model = #some_model.build_other_model
#other_model.setup(params[:other_model)
Where setup is:
class OtherModel < ActiveRecord::Base
def setup(other_params)
# execute the important_attribute= setter first
important_attribute = other_params.delete(:important_attribute)
# set the other attributes in whatever order they occur in the params hash
other_params.each { |k,v| self.send("#{k}=",v) }
end
end
This seems to work, but looks kludgy. Is there a better way?
EDIT
per neutrino's suggestion, I added a method to SomeModel:
class SomeModel < ActiveRecord::Base
def build_other_model(other_params)
other_model = OtherModel.new(:some_model=>self)
other_model.setup(other_params)
other_model
end
end
It's a good thing that you have this manipulations done in an OtherModel's method, because you can just call this method and not worry about the order of assignments. So I would leave this part but just call it from a SomeModel's method:
class SomeModel < ActiveRecord::Base
def build_other_model(other_params)
other_model = build_other_model
other_model.setup(other_params)
other_model
end
end
So then you would have
#other_model = #some_model.build_other_model(params[:other_model])
I took your idea of deleting the important attribute first in your setup method, but used alias_chain_method instead to make it more of a transparent process:
def attributes_with_set_important_attribute_first=(attributes = {})
# Make sure not to accidentally blank out the important_attribute when none is passed in
if attributes.symbolize_keys!.include?(:important_attribute)
self.important_attribute = attributes.delete(:important_attribute)
end
self.attributes_without_set_important_attribute_first = attributes
end
alias_method_chain :attributes=, :set_important_attribute_first
This way none of your code should change from the normal Rails style
#other_model = #some_model.other_models.build(params[:other_model])