Rails to_json(methods: => [...]) for different ActiveRecords - ruby-on-rails

In Rails, i have an object called values that could be 1 of 20 kinds of ActiveRecord, and in only 1 of them there's a method(may be the wrong term, rails newbie) that can add a customized field in returned JSON object where the method name is the field name and method returned value is the field value. For example
class XXXController < ApplicationController
..
if a
values = A
elsif b
values = B
elseif c
values = C
..
end
render :json => values.to_json(:methods => :type_needed)
and you will see response like
{
..
"type_needed": true,
..
}
I only have type_needed defined in A which will return true in some cases. For others like B, C, D... which in total 19, i want them to all have type_needed returned as false, is there a way i can do that in one place instead of add type_needed method in the rest 19?

I will do it as follows:
json = values.to_json(:methods => :type_needed)
# => "[{\"id\":1,\"name\":\"Aaa\"},{\"id\":\"2\",\"name\":\"Bbb\"}]" # => Representational value only
ary = JSON.parse(json)
# => [{"id"=>1, "name"=>"Aaa"}, {"id"=>2, "name"=>"Bbb"}]
ary.map! { |hash| hash[:type_needed] = false unless hash.key?(:type_needed); hash }
# => [{"id"=>1, "name"=>"Aaa", :type_needed=>false}, {"id"=>2, "name"=>"Bbb", :type_needed=>false}]
ary.to_json
# => "[{\"id\":1,\"name\":\"Aaa\",\"type_needed\":false},{\"id\":\"2\",\"name\":\"Bbb\",\"type_needed\":false}]"

If I am understanding your question correctly then you want to define type_needed method once and have it included on all your 20 models. If yes, then you can define a concern and include it in all your 20 models.
app/models/concerns/my_model_concern.rb
module MyModelConcern
extend ActiveSupport::Concern
def type_needed?
self.respond_to?(:some_method)
end
end
app/models/a.rb
class A < ApplicationRecord
include MyModelConcern
def some_method
end
end
app/models/b.rb
class B < ApplicationRecord
include MyModelConcern
end
app/models/c.rb
class C < ApplicationRecord
include MyModelConcern
end
With the above
a = A.new
a.type_needed?
=> true
b = B.new
b.type_needed?
=> false
c = C.new
c.type_needed?
=> false
See if this helps.

Related

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?

Instance Variables in a Rails Model

I have this variable opinions I want to store as an instance variable in my model... am I right in assuming I will need to add a column for it or else be re-calculating it constantly?
My other question is what is the syntax to store into a column variable instead of just a local one?
Thanks for the help, code below:
# == Schema Information
#
# Table name: simulations
#
# id :integer not null, primary key
# x_size :integer
# y_size :integer
# verdict :string
# arrangement :string
# user_id :integer
#
class Simulation < ActiveRecord::Base
belongs_to :user
serialize :arrangement, Array
validates :user_id, presence: true
validates :x_size, :y_size, presence: true, :numericality => {:only_integer => true}
validates_numericality_of :x_size, :y_size, :greater_than => 0
def self.keys
[:soft, :hard, :none]
end
def generate_arrangement
#opinions = Hash[ Simulation.keys.map { |key| [key, 0] } ]
#arrangement = Array.new(y_size) { Array.new(x_size) }
#arrangement.each_with_index do |row, y_index|
row.each_with_index do |current, x_index|
rand_opinion = Simulation.keys[rand(0..2)]
#arrangement[y_index][x_index] = rand_opinion
#opinions[rand_opinion] += 1
end
end
end
def verdict
if #opinions[:hard] > #opinions[:soft]
:hard
elsif #opinions[:soft] > #opinions[:hard]
:soft
else
:push
end
end
def state
#arrangement
end
def next
new_arrangement = Array.new(#arrangement.size) { |array| array = Array.new(#arrangement.first.size) }
#opinions = Hash[ Simulation.keys.map { |key| [key, 0] } ]
#seating_arrangement.each_with_index do |array, y_index|
array.each_with_index do |opinion, x_index|
new_arrangement[y_index][x_index] = update_opinion_for x_index, y_index
#opinions[new_arrangement[y_index][x_index]] += 1
end
end
#arrangement = new_arrangement
end
private
def in_array_range?(x, y)
((x >= 0) and (y >= 0) and (x < #arrangement[0].size) and (y < #arrangement.size))
end
def update_opinion_for(x, y)
local_opinions = Hash[ Simulation.keys.map { |key| [key, 0] } ]
for y_pos in (y-1)..(y+1)
for x_pos in (x-1)..(x+1)
if in_array_range? x_pos, y_pos and not(x == x_pos and y == y_pos)
local_opinions[#arrangement[y_pos][x_pos]] += 1
end
end
end
opinion = #arrangement[y][x]
opinionated_neighbours_count = local_opinions[:hard] + local_opinions[:soft]
if (opinion != :none) and (opinionated_neighbours_count < 2 or opinionated_neighbours_count > 3)
opinion = :none
elsif opinion == :none and opinionated_neighbours_count == 3
if local_opinions[:hard] > local_opinions[:soft]
opinion = :hard
elsif local_opinions[:soft] > local_opinions[:hard]
opinion = :soft
end
end
opinion
end
end
ActiveRecord analyzes the database tables and creates setter and getter methods with metaprogramming.
So you would create a database column with a migration:
rails g migration AddOpinionToSimulation opinion:hash
Note that not all databases support storing a hash or a similar key/value data type in a column. Postgres does. If you need to use another database such MySQL you should consider using a relation instead (storing the data in another table).
Then when you access simulation.opinion it will automatically get the database column value (if the record is persisted).
Since ActiveRecord creates a setter and getter you can access your property from within the Model as:
class Simulation < ActiveRecord::Base
# ...
def an_example_method
self.opinions # getter method
# since self is the implied receiver you can simply do
opinions
opinions = {foo: "bar"} # setter method.
end
end
The same applies when using the plain ruby attr_accessor, attr_reader and attr_writer macros.
When you assign to an attribute backed by a database column ActiveRecord marks the attribute as dirty and will include it when you save the record.
ActiveRecord has a few methods to directly update attributes: update, update_attributes and update_attribute. There are differences in the call signature and how they handle callbacks.
you can add a method like
def opinions
#opinions ||= Hash[ Simulation.keys.map { |key| [key, 0] }
end
this will cache the operation into the variable #opinions
i would also add a method like
def arrangement
#arrangement ||= Array.new(y_size) { Array.new(x_size) }
end
def rand_opinion
Simulation.keys[rand(0..2)]
end
and then replace the variables with your methods
def generate_arrangement
arrangement.each_with_index do |row, y_index|
row.each_with_index do |current, x_index|
arrangement[y_index][x_index] = rand_opinion
opinions[rand_opinion] += 1
end
end
end
now your opinions and your arrangement will be cached and the code looks better. you didn't have to add a new column in you table
you now hat to replace the #opinions variable with your opinions method

Rails: ActiveRecord interdependent attributes setters

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

access the former value of an attribute in ruby

Lets say I have a model (an ActiveRecord class):
class Sample < ActiveRecord::Base
attr_accessor :x1
end
I know that
Sample.last.x1 == 1 #true
If I set Sample.last.x1 = 3 then Sample.last.x1_was == 1 #true.
But when I set the value of x1 again: Sample.last.x1 = 8 then Sample.last.x1_was == 3 #false, but Sample.last.x1 == 1 #true
I can guess why it happens (Sample.last wasn't saved since the change), but I want to find a way to retrieve the former value (not the db value) of x1. Can you suggest a way to do it?
I can't think of a reason to do that, but if you really need to, you could override the setter to store the various changes as you go.
def x1=( value )
#previous_x1_value = x1
super
end
def previous_x1_value
#previous_x1_value || x1_was
end
IT's all built in to rails. See the documentation for [ActiveRecord#dirty][1]
person.name = 'Bob'
person.changed? # => true
person.name_changed? # => true
person.name_was # => 'uncle bob'
person.name_change # => ['uncle bob', 'Bob']
person.name = 'Bill'
person.name_change # => ['uncle bob', 'Bill']

class inheritance in ruby / rails

so this is what i want to do:
class A
ATTRS = []
def list_attrs
puts ATTRS.inspect
end
end
class B < A
ATTRS = [1,2]
end
a = A.new
b = B.new
a.list_attrs
b.list_attrs
i want to create a base class with a method that plays with the ATTRS attribute of the class. in each inherited class there will be a different ATTRS array
so when i call a.list_attrs it should print an empty array and if i call b.attrs should put [1,2].
how can this be done in ruby / ruby on rails?
It is typically done with methods:
class A
def attrs
[]
end
def list_attrs
puts attrs.inspect
end
end
class B < A
def attrs
[1,2]
end
end
modf's answer works... here's another way with variables. (ATTRS is a constant in your example)
class A
def initialize
#attributes = []
end
def list_attrs
puts #attributes.inspect
end
end
class B < A
def initialize
#attributes = [1,2]
end
end
I don't think it's a good idea to create the same array each time a method is called. It's more natural to use class instance variables.
class A
def list_attrs; p self.class.attrs end
end
class << A
attr_accessor :attrs
end
class A
#attrs = []
end
class B < A
#attrs = [1, 2]
end
A.new.list_attrs # => []
B.new.list_attrs # => [1, 2]
You can also use constants along the line suggested in the question:
class A
def list_attrs; p self.class.const_get :ATTRS end
ATTRS = []
end
class B < A
ATTRS = [1, 2]
end
A.new.list_attrs # => []
B.new.list_attrs # => [1, 2]

Resources