Rails: ActiveRecord interdependent attributes setters - ruby-on-rails

In activerecord, attribute setters seems to be called in order of the param hash.
Therefore, in the following sample, "par_prio" will be empty in "par1" setter.
class MyModel < ActiveRecord::Base
def par1=(value)
Rails.logger.info("second param: #{self.par_prio}")
super(value)
end
end
MyModel.new({ :par1 => 'bla', :par_prio => 'bouh' })
Is there any way to simply define an order on attributes in the model ?

NOTE: I have a solution, but not "generic", by overriding the initialize method on "MyModel":
def initialize(attributes = {}, options = {})
if attributes[:par_prio]
value = attributes.delete(:par_prio)
attributes = { :par_prio => value }.merge(attributes)
end
super(attributes, options)
end
Moreover, it does not works if par_prio is another model that has a relation on, and is used to build MyModel:
class ParPrio < ActiveRecord::Base
has_many my_models
end
par_prio = ParPrio.create
par_prio.my_models.build(:par1 => 'blah')
The par_prio param will not be available in the initialize override.

Override assign_attributes on the specific model where you need the assignments to happen in a specific order:
attr_accessor :first_attr # Attr that needs to be assigned first
attr_accessor :second_attr # Attr that needs to be assigned second
def assign_attributes(new_attributes, options = {})
sorted_new_attributes = new_attributes.with_indifferent_access
if sorted_new_attributes.has_key?(:second_attr)
first_attr_val = sorted_new_attributes.delete :first_attr
raise ArgumentError.new('YourModel#assign_attributes :: second_attr assigned without first_attr') unless first_attr_val.present?
new_attributes = Hash[:first_attr, first_attr_val].merge(sorted_new_attributes)
end
super(new_attributes, options = {})
end

Related

Extending ActiveModel::Serializer with custom attributes method

I am trying to create my own attributes method called secure_attributes where I pass it an array of attributes and the minimum level the authorized user needs to be to view those attributes. I pass the current level of the authorized user as an instance_option. I'd like to extend the Serializer class so I can use this method in multiple serializers, but Im having issues.
This is what i have so far:
in config/initializers/secure_attributes.rb
module ActiveModel
class Serializer
def self.secure_attributes(attributes={}, minimum_level)
attributes.delete_if {|attr| attr == :attribute_name } unless has_access?(minimum_level)
attributes.each_with_object({}) do |name, hash|
unless self.class._fragmented
hash[name] = send(name)
else
hash[name] = self.class._fragmented.public_send(name)
end
end
end
end
end
and then in the individual serializer I have things like this:
secure_attributes([:id, :name, :password_hint], :guest)
and then
def has_access?(minimum_level=nil)
return false unless minimum_level
return true # based on a bunch of logic...
end
But obviously secure_attributes cannot see the has_access? method and if I put has_access inside the Serializer class, it cannot access the instance_options.
Any idea how I can accomplish what I need?
Maybe you want to do following - but I still do not get your real purpose, since you never did anything with the attributes but calling them:
module ActiveRecord
class JoshsSerializer < Serializer
class << self
def secure_attributes(attributes={}, minimum_level)
#secure_attributes = attributes
#minimum_level = minimum_level
end
attr_reader :minimum_level, :secure_attributes
end
def initialize(attr, options)
super attr, options
secure_attributes = self.class.secure_attributes.dup
secure_attributes.delete :attribute_name unless has_access?(self.class.minimum_level)
secure_attributes.each_with_object({}) do |name, hash|
if self.class._fragmented
hash[name] = self.class._fragmented.public_send(name)
else
hash[name] = send(name)
end
end
def has_access?(minimum_level=nil)
return false unless minimum_level
return true # based on a bunch of logic...
end
end
end

Is a ':methods' option in 'to_json' substitutable with an ':only' option?

The to_json option has options :only and :methods. The former is intended to accept attributes and the latter methods.
I have a model that has an attribute foo, which is overwritten:
class SomeModel < ActiveRecord::Base
...
def foo
# Overrides the original attribute `foo`
"the overwritten foo value"
end
end
The overwritten foo method seems to be called irrespective of which option I write the foo under.
SomeModel.first.to_json(only: [:foo])
# => "{..., \"foo\":\"the overwritten foo value\", ...}"
SomeModel.first.to_json(methods: [:foo])
# => "{..., \"foo\":\"the overwritten foo value\", ...}"
This seems to suggest it does not matter whether I use :only or :methods.
Is this the case? I feel something wrong with my thinking.
The source code leads to these:
File activemodel/lib/active_model/serialization.rb, line 124
def serializable_hash(options = nil)
options ||= {}
attribute_names = attributes.keys
if only = options[:only]
attribute_names &= Array(only).map(&:to_s)
elsif except = options[:except]
attribute_names -= Array(except).map(&:to_s)
end
hash = {}
attribute_names.each { |n| hash[n] = read_attribute_for_serialization(n) }
Array(options[:methods]).each { |m| hash[m.to_s] = send(m) }
serializable_add_includes(options) do |association, records, opts|
hash[association.to_s] = if records.respond_to?(:to_ary)
records.to_ary.map { |a| a.serializable_hash(opts) }
else
records.serializable_hash(opts)
end
end
hash
end
File activeresource/lib/active_resource/base.rb, line 1394
def read_attribute_for_serialization(n)
attributes[n]
end
and it seems that an :only option calls attributes[n] and :methods option calls send(m). What is the difference?

ruby on rails accessing custom class attributes from its object

I have a custom class in my application controller. Like below:
class Defaults
def initialize
#value_1 = "1234"
#value_2 = nil
#data = Data.new
end
end
class Data
def initialize
#data_1 = nil
end
end
Now in my controller method i have created an object of type Defaults
def updateDefaultValues
defaults = Defaults.new
# i am unable to update the value, it says undefined method
defaults.value_2 = Table.maximum("price")
defaults.data.data_1 = defaults.value_2 * 0.3
end
How to access value_2 from defaults object?
defaults.value_2
Also, how to access data_1 attribute from data object within defaults object?
defaults.data.data_1
You should use attr_accessor:
class Defaults
attr_accessor :value_1, :value_2, :data
# ...
end
defaults = Defaults.new
defaults.value_1 = 1
# => 1
defaults.value_1
# => 1
As you are using def as a keyword to define the method, that means def is a reserved keyword. You can't use reserved keywords as a variable.
You just need to rename your variable name from def to something_else and it should work! Your code will look like this:
def updateDefaultValues
obj = Defaults.new
obj.value_2 = Table.maximum("price")
obj.data.data_1
end
EDIT:
As per OP's comment & updated question, he had used def just as an example, here is the updated answer:
You may need attr_accessor to make attrs accessible:
class Defaults
attr_accessor :value_1, :value_2, :data
...
...
end
class Data
attr_accessor :data_1
...
...
end
Add value_2 method in Defaults class
class Defaults
def initialize
#value_1 = "1234"
#value_2 = nil
#data = Data.new
end
def value_2
#value_2
end
end
class Data
def initialize
#data_1 = nil
end
end

ActiveRecord: How to set the "changed" property of an model?

For every model in ActiveRecord, there seems to be a private property called "changed", which is an array listing all the fields that have changed since you retrieved the record from the database.
Example:
a = Article.find(1)
a.shares = 10
a.url = "TEST"
a.changed ["shares", "url"]
Is there anyway to set this "changed" property yourself? I know it sounds hacky/klunky, but I am doing something rather unordinary, which is using Redis to cache/retrieve objects.
ActiveModel::Dirty#changed returns keys of the #changed_attributes hash, which itself returns attribute names and their original values:
a = Article.find(1)
a.shares = 10
a.url = "TEST"
a.changed #=> ["shares", "url"]
a.changed_attributes #=> {"shares" => 9, "url" => "BEFORE_TEST"}
Since there is no setter method changed_attributes=, you can set the instance variable by force:
a.instance_variable_set(:#changed_attributes, {"foo" => "bar"})
a.changed #=> ["foo"]
See this example from: Dirty Attributes
class Person
include ActiveModel::Dirty
define_attribute_methods :name
def name
#name
end
def name=(val)
name_will_change! unless val == #name
#name = val
end
def save
#previously_changed = changes
#changed_attributes.clear
end
end
So, if you have a attribute foo and want to "change" that just call foo_will_change!.

How would I pass sort criteria into a method so that it can be used by sort_by

How would I pass an attribute name to method sort_by, and use it within? For example, say I wanted to create a FrequencyCounter with an array of singleton objects Foo. (It's important that they're singletons because it means that there will be a certain number of identical Foos).
class Foo
attr_accessor :arbitrary_sorter
def initialize arbitrary_sorter
#arbitrary_sorter = arbitrary_sorter
end
end
class FrequencyCounter
def initialize ary
#multiset will create a hash of frequency like {3=>obj, 2->obj, 2=>obj, etc}
#hash = Multiset.new(ary).hash
end
def sort_by params={}
Hash[#hash.sort_by {|k,v| [-1 * v, -1 * k]}]
end
end
And when I create Frequency object:
#fc = FrequencyCounter.new([Foo.get(5), Foo.get(4), Foo.get(5), Foo.get(1)])
I'd like to tell #fc what to order on:
#fc.sort_by(:arbitrary_sorter)
Is this possible?
on any enumerator, you can tell it what to sort by. So, let's take a simple example:
class Animal
attr_accessor :name, :leg_count
def initialize(name, leg_count)
#name = name
#leg_count = leg_count
end
def to_s
#name
end
end
animals = [Animal.new('human', 2), Animal.new('dog', 4), Animal.new('snake', 0)]
=> [human, dog, snake]
# sort by an attribute or method on the model
animals.sort_by{|a| a.leg_count}
=> [snake, human, dog]
Same as:
animals.sort_by(&:leg_count)
=> [snake, human, dog]
You can event do something like:
sorter_lambda = lambda {|animal| animal.name == "human" ? 0 : 999}
animals.sort_by(&sorter_lambda)
=> [human, dog, snake]
The lambda may work best for you with the complicated logic I saw.

Resources