Is a ':methods' option in 'to_json' substitutable with an ':only' option? - ruby-on-rails

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?

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

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

Loading custom fixtures in Rails

We are trying to load fixtures for a group of models that uses a different database connection than ActiveRecord::Base (inheriting from Foo::Base in this example).
We've created this module that we include in ActiveSupport::TestCase and the specify a path to the .yml files and e.g. foo_fitures :all. This works great for the first test that runs. Fixture accessors are defined and records are found in the database. But for subsequent tests there are no records in the database.
module Foo::Fixtures
extend ActiveSupport::Concern
included do
setup :setup_foo_fixtures
teardown :teardown_foo_fixtures
class_attribute :foo_fixture_path
class_attribute :foo_fixture_table_names
self.foo_fixture_table_names = []
end
module ClassMethods
def foo_fixtures(*fixture_names)
if fixture_names.first == :all
fixture_names = Dir[foo_fixture_path.join("**/*.yml")].map { |f| File.basename(f, ".yml") }
else
fixture_names = fixture_names.flatten.map { |n| n.to_s }
end
self.foo_fixture_table_names |= fixture_names
require_fixture_classes(fixture_names)
setup_fixture_accessors(fixture_names)
end
end
def setup_foo_fixtures
#loaded_fixtures.merge!(load_foo_fixtures)
end
def teardown_foo_fixtures
Foo::Base.clear_active_connections!
end
private
def load_foo_fixtures
foo_classes = Foo::Base.subclasses.flat_map { |klass| klass.abstract_class ? klass.subclasses : klass }
class_names = foo_classes.each_with_object({}) do |klass, memo|
memo[klass.table_name.to_sym] = klass if klass.table_name.present? && foo_fixture_table_names.include?(klass.table_name)
end
foo_fixtures = ActiveRecord::Fixtures.create_fixtures(foo_fixture_path, foo_fixture_table_names, class_names) do
Foo::Base.connection
end
Hash[foo_fixtures.map { |f| [f.name, f] }]
end
end
Rails' fixture system is a bit convoluted, and I'm not able to figure out what we are missing to make sure that our extra fixtures are loaded.
OK. It looks like it might be the transactions that are removing our fixtures from the database. My guess is that the transaction has started before our code loads in the fixtures, so that's why they are there for the first test, but gone at the second.
So we changed strategy, and now we just hook into load_fixtures and fixtures. This works just fine.
module FooFixtures
module ClassMethods
def foo_fixture_classes
collect_subclasses = ->(k) { k.abstract_class ? k.subclasses.flat_map(&collect_subclasses) : k }
Foo::Base.subclasses.flat_map(&collect_subclasses)
end
def foo_fixture_path
Rails.root.join("test/foo_fixtures")
end
def foo_fixture_table_names
Dir[foo_fixture_path.join("**/*.yml")].map { |f| File.basename(f, ".yml") }
end
def fixtures(*fixture_names)
super
if fixture_names.first == :all
require_fixture_classes(foo_fixture_table_names)
setup_fixture_accessors(foo_fixture_table_names)
end
end
end
private
def load_fixtures
foo_fixture_path = self.class.foo_fixture_path
foo_fixture_table_names = self.class.foo_fixture_table_names
class_names = self.class.foo_fixture_classes.each_with_object({}) do |klass, memo|
memo[klass.table_name.to_sym] = klass if klass.table_name.present? && foo_fixture_table_names.include?(klass.table_name)
end
foo_fixtures = ActiveRecord::Fixtures.create_fixtures(foo_fixture_path, foo_fixture_table_names, class_names) do
Foo::Base.connection
end
super.merge(Hash[foo_fixtures.map { |f| [f.name, f] }])
end
end
class ActiveSupport::TestCase
extend FooFixtures::ClassMethods
prepend FooFixtures
self.foo_fixture_classes.each do |fixture_class|
set_fixture_class fixture_class.table_name.to_sym => fixture_class
end
...
end

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

Dynamically defined setter methods using define_method?

I use a lot of iterations to define convenience methods in my models, stuff like:
PET_NAMES.each do |pn|
define_method(pn) do
...
...
end
but I've never been able to dynamically define setter methods, ie:
def pet_name=(name)
...
end
using define_method like so:
define_method("pet_name=(name)") do
...
end
Any ideas? Thanks in advance.
Here's a fairly full example of using define_method in a module that you use to extend your class:
module VerboseSetter
def make_verbose_setter(*names)
names.each do |name|
define_method("#{name}=") do |val|
puts "##{name} was set to #{val}"
instance_variable_set("##{name}", val)
end
end
end
end
class Foo
extend VerboseSetter
make_verbose_setter :bar, :quux
end
f = Foo.new
f.bar = 5
f.quux = 10
Output:
#bar was set to 5
#quux was set to 10
You were close, but you don't want to include the argument of the method inside the arguments of your call to define_method. The arguments go in the block you pass to define_method.
Shoertly if you need it inside one class/module:
I use hash but you can put there array of elements etc.
PETS = {
"cat" => "meyow",
"cow" => "moo",
"dog" => "ruff"
}
def do_smth1(v)
...
end
def do_smth(sound,v)
...
end
#getter
PETS.each{ |k,v| define_method(k){ do_smth1(v) } }
#setter
PETS.each{ |k,v| define_method("#{k}="){|sound| do_smth2(sound, v) }

Resources