rails best way to abstract deep object literal data - ruby-on-rails

I have code like this in my controller:
def home
#mainImage = []
#mainImage.push(
{:breakpoint => 1024,
:src => i_path('pages/home-d.jpg'),
:src_2x => i_path('pages/home-d_2x.jpg')}
)
#mainImage.push(
{:breakpoint => 768,
:src => i_path('pages/home-t.jpg'),
:src_2x => i_path('pages/home-t_2x.jpg')}
)
#mainImage.push(
{:breakpoint => 320,
:src => i_path('pages/home-m.jpg'),
:src_2x => i_path('pages/home-m_2x.jpg')}
)
#alt = 'An image description'
#defaultImage = i_path('pages/home-m.jpg')
end
Which in the view is rendered with help of a partial.
I now need to add similar functionality to render model attributes from a paperclip object.
Which is now looking like this:
#respImage.push(
{:breakpoint => 1024,
:src => slide.image.url(:desktop_reg),
:src_2x => slide.image.url(:desktop_retina)}
)
#respImage.push(
{:breakpoint => 768,
:src => slide.image.url(:tablet_reg),
:src_2x => slide.image.url(:tablet_retina)}
)
#...
The final objective of Is to have a slideshow, with many slides.
A slide has several attribute strings and a paperclip attribute.
The paperclip attribute has 6 styles for each image size.
What is the standard mechanism in Rails to transmit data like the above to the view?
I'm assuming this generic array is not the most flexible solution.
This is where the code ended up.
In the controller:
def home
#mainImage2 = RespImage.new(:alt => 'default homepage image')
#mainImage2.add_breakpoint(BREAKPOINTS['desktop'],i_path('pages/home-d.jpg'),i_path('pages/home-d_2x.jpg'));
#mainImage2.add_breakpoint(BREAKPOINTS['tablet'],i_path('pages/home-t.jpg'),i_path('pages/home-t_2x.jpg'));
#...
models/resp_image.rb :
class RespImage
attr_accessor :alt, :breakpoints
def initialize(attributes = {})
attributes.each do |name, value|
send("#{name}=", value)
end
end
def add_breakpoint(px,src,src_2x)
self.breakpoints ||= [];
self.breakpoints.push RespBreakpoint.new(:px => px, :src => src, :src_2x =>src_2x)
end
def add_paperclip_breakpoints(paperclip)
add_breakpoint(BREAKPOINTS('desktop'), paperclip.url(:desktop_reg), paperclip.url(:desktop_retina));
add_breakpoint(BREAKPOINTS('tablet'), paperclip.url(:tablet_reg), paperclip.url(:tablet_retina));
add_breakpoint(BREAKPOINTS('mobile'), paperclip.url(:mobile_reg), paperclip.url(:mobile_retina));
end
def default_src
self.breakpoints.sort.first.src
end
end
models/resp_breakpoint
class RespBreakpoint
include Comparable
attr_accessor :px,:src,:src_2x
def initialize(attributes = {})
attributes.each do |name, value|
send("#{name}=", value)
end
end
def better_than?(other)
self.px > other.px
end
def <=>(other)
self.px <=> other.px
end
def eql?(other)
self.px === other.px
end
end
Huge thanks to #Dave Newton below.

First, use value objects. While an array of hashes seems easy at first, VOs can help encapsulate behavior as well as data easily, and improve readability.
Second, consider making those VOs "top-level" models such that they can be rendered using an appropriately-named partial, which I'm assuming would clean up your helper as well.
The last part of your question is a bit opaque without any context: what do you mean by "add similar functionality to render model attributes from a Paperclip object"? Do the objects have the same attributes? Is the data already represented by a class, or by attributes on a class?
If the latter, then you can use duck-typing to eliminate some of the duplication.
Right now it looks like this information is stored a bit funky. I'd probably turn the model around a bit and use a hash, keyed by breakpoint (useful since it's what CSS uses to trigger the style changes) or something more semantic (e.g., :desktop, :tablet, etc.).
Your model would contain the hash keys as attributes (i.e., breakpoint, src, and src_2x).
The values could be stored anywhere, it kind of depends on how often they'd need to change. It might be enough to use constants, or a YAML file, or a Ruby config object, etc.
The array itself would be created by a utility class or service object taking some parameter that determined what information needed to go in it. You might have a method that took something that returned either an i_path or slide image URL depending on what was passed in (think Inversion of Control).
Or it could be a decorator that decorated your slide model and a fake (or real) model for the i_path ones, etc.

Related

In RoR, how do I initialize fields in my model based on a field in my model that has no underlying database column?

I’m using Rails 4.2.7. I have an attribute in my model that doesn’t have a database field underneath it
attr_accessor :division
This gets initialized when I create a new object.
my_object = MyObject.new(:name => name,
:age => get_age(data_hash),
:overall_rank => overall_rank,
:city => city,
:state => state,
:country => country,
:age_group_rank => age_group_rank,
:gender_rank => gender_rank,
:division => division)
What I would like is when this field gets set (if it is not nil), for two other fields that do have mappings in the database to get set. The other fields would be substrings of the “division” field. Where do I put that logic?
I'd probably drop the attr_accessor :division and do it by hand with:
def division=(d)
# Break up `d` as needed and assign the parts to the
# desired real attributes.
end
def division
# Combine the broken out attributes as needed and
# return the combined string.
end
With those two methods in place, the following will all call division=:
MyObject.new(:division => '...')
MyObject.create(:division => '...')
o = MyObject.find(...); o.update(:division => '...')
o = MyObject.find(...); o.division = '...'
so the division and the broken out attributes will always agree with each other.
If you try to use one of the lifecycle hooks (such as after_initialize) then things can get out of sync. Suppose division has the form 'a.b' and the broken out attributes are a and b and suppose that you're using one of the ActiveRecord hooks to break up division. Then saying:
o.division = 'x.y'
should give you o.a == 'x' but it won't because the hook won't have executed yet. Similarly, if you start with o.division == 'a.b' then
o.a = 'x'
won't give you o.division == 'x.b' so the attributes will have fallen out of sync again.
I see couple of options here
You can add it in your controller as follows
def create
if params[:example][:division]
# Set those params here
end
end
Or you can use before_save In your model
before_save :do_something
def do_something
if division
# Here!
end
end

rails 4 strong params + dynamic hstore keys

I'm having a problem overcoming the new strong params requirement in Rails 4 using Hstore and dynamic accessors
I have an Hstore column called :content which I want to use to store content in multiple languages, ie :en, :fr, etc. And I don't know which language upfront to set them in either the model or the controller.
store_accessor :content, [:en, :fr] #+226 random other il8n languages won't work.
How can I override strong params (or allow for dynamic hstore keys) in rails 4 for one column?
params.require(:article).permit(
:name, :content,
:en, :fr #+226 random translations
)
Short of...
params.require(:article).permit!
which of course does work.
If I understand correctly, you would like to whitelist a hash of dynamic keys. You can use some ruby code as follows to do this:
params.require(:article).permit(:name).tap do |whitelisted|
whitelisted[:content] = params[:article][:content]
end
This worked for me, hope it helps!
I'm doing something similar and found this to be a bit cleaner and work well.
Assuming a model called Article you can access your :content indexed stored_attributes like this: Article.stored_attributes[:content]
So your strong params looks like this:
params.require(:article).permit(:name, content: Article.stored_attributes[:content])
Assuming your params are structured like: { article => { name : "", content : [en, fr,..] } }
As people have said, it is not enough to permit the :content param - you need to permit the keys in the hash as well. Keeping things in the policy, I did that like so:
# in controller...
def model_params
params.permit(*#policy.permitted_params(params))
end
# in policy...
def permitted_params(in_params = {})
params = []
params << :foo
params << :bar
# ghetto hack support to get permitted params to handle hashes with keys or without
if in_params.has_key?(:content)
content = in_params[:content]
params << { :content => content.empty? ? {} : content.keys }
end
end

Keeping an instance variable as an instance variable, not its contents

I have only a vague idea on phrasing this, so question as needed:
I have a set of values I'm passing in my rails controller on a regular basis to a widget that differs slightly from page to page, from what I pass to it. This is is starting to get unwieldy for every controller, so I added a small class to help concatenate that process a bit (basic starting gist below).
#return dashcontroller hash from more succinct cues
module DashControl
class DashControl
attr_accessor :title, :instance, :actions
def initialize(title='default title', instance='default instance', actions={})
#title = title
#instance = instance
initialize_actions(actions)
end
def initialize_actions(actions)
actions.kind_of?(Hash) ? #actions = actions : initialize_tag(actions)
end
def initialize_tag(tag)
case tag
when :manage_default
#actions = {:statusactions => [],
:formactions => [ ['#tabaccount', 'addaccount'],
['#tabuser', 'addusers'],
['#tabadd','adddomain'] ],
:linkactions => [ [] ],
:filteractions => [ [] ] }
when :none
#actions = {}
#when
# #actions = {}
else
#actions = #actions
end
end
def dashcontroller
{:title => #title, :instance => #instance, :actions => #actions }
end
end
end
So basically I just need to pass an instance of this.dashcontroller and I get the hash I need with a lot less chaos in my controllers . The issue is with the #instance variable. I want to pass in the instance I'm using e.g. #book, #account, etc, and have it come out as #book, #account, etc. Instead, I get the contents of whatever I put into there as :instance => (contents of that instance). It doesn't seem right to me as before I was just using e.g. #account, and then using that, but looking at it might not make any sort of difference in the widget, as I juggle things and work on my code-fu.
Basically my question is how to push an instance variable through a class like this, and still have it accessibile as it went in without having to do any backflips and transformations on the other side. There is probably a better way, but this is what I'm working with at the moment.
edit: pseudo-code
DashControl::DashControl.new("Catchy Title", #book, :none).dashcontroller
#=> {:title => "Catchy Title", :instance => #book, :actions => {} }
I think I can work with it, like I said its more an issue of my understanding of how things flow than an actual bug or anything difficult. I'd like to not have to do more gymnastics on the other end with the instance stuff, though the contents are there and that is all I really need, I just need some input on thinking it through to be less of a mess. I really need to refine what I'm sending through this, or use this to further refine what I'm sending on is the bottom line lesson to take away right now.
edit:
I ended up tossing this, but it was a learning experience...I went back the widget and I know more than when I originally set up the widget, so I've been able to set that up to take only the instance variable and bootstrap what it needs without adding another class, cleaning up my controllers and handing a lot back to the widget where I suspect it should/could have been to start.
Based on your code and example, this fits:
# No need to put a class in a namespace of the same name, just make the module a class
# Also, if you inherit from a struct, it can save you a lot of typing. It defines the setters and getters for you.
class DashControl < Struct.new(:title, :instance, :actions)
# since it looks like you always access it the same way, create a class method
# which does this initialization and invocation
def self.for(*args)
new(*args).dashcontroller
end
def initialize(title='default title', instance='default instance', actions=:none)
# here, we can use our own defaults and normalization and pass the results up to the struct
super title, instance, normalize(actions)
end
# didn't make sense to call this initialize_tag, as it was initializing actions
# also there was already an initialize actions method which just checked for the case of a hash
# but then elsewhere you checked for other things. Better to just put it all in one method and return it
# (then you aren't setting it every time you want to ask it to calculate that value)
# also using guard clauses (the if statements that return early) instead of the case, as they are easier to understand
def normalize(actions)
return Hash.new if actions == :none
return actions unless actions == :manage_default
default_actions
end
# the value of default_actions is complicated and noisy, separate it out to its own method
# this prevents it from cluttering the code around it, and also allows us to access,
# and to do this without the side effects of setting values.
def default_actions
{ :statusactions => [],
:formactions => [ ['#tabaccount', 'addaccount'],
['#tabuser', 'addusers'],
['#tabadd','adddomain'] ],
:linkactions => [ [] ],
:filteractions => [ [] ] }
end
# use the getters instead of the ivars (I consider this a generally best practice -- and you could have
# done it before, since you declared the attr_accessor, even though I'm accessing it through the struct)
def dashcontroller
{:title => title, :instance => instance, :actions => actions }
end
end
DashControl.for # => {:title=>"default title", :instance=>"default instance", :actions=>{}}
DashControl.for('Catchy Title', '#book', :none) # => {:title=>"Catchy Title", :instance=>"#book", :actions=>{}}
DashControl.for('Catchy Title', '#book', :manage_default) # => {:title=>"Catchy Title", :instance=>"#book", :actions=>{:statusactions=>[], :formactions=>[["#tabaccount", "addaccount"], ["#tabuser", "addusers"], ["#tabadd", "adddomain"]], :linkactions=>[[]], :filteractions=>[[]]}}
DashControl.for('Catchy Title', '#book', a: 'b') # => {:title=>"Catchy Title", :instance=>"#book", :actions=>{:a=>"b"}}
DashControl.for('Catchy Title', '#book', 123) # => {:title=>"Catchy Title", :instance=>"#book", :actions=>123}

Add virtual attribute to json output

Let's say I have an app that handles a TODO list. The list has finished and unfinished items. Now I want to add two virtual attributes to the list object; the count of finished and unfinished items in the list. I also need these to be displayed in the json output.
I have two methods in my model which fetches the unfinished/finished items:
def unfinished_items
self.items.where("status = ?", false)
end
def finished_items
self.items.where("status = ?", true)
end
So, how can I get the count of these two methods in my json output?
I'm using Rails 3.1
The serialization of objects in Rails has two steps:
First, as_json is called to convert the object to a simplified Hash.
Then, to_json is called on the as_json return value to get the final JSON string.
You generally want to leave to_json alone so all you need to do is add your own as_json implementation sort of like this:
def as_json(options = { })
# just in case someone says as_json(nil) and bypasses
# our default...
super((options || { }).merge({
:methods => [:finished_items, :unfinished_items]
}))
end
You could also do it like this:
def as_json(options = { })
h = super(options)
h[:finished] = finished_items
h[:unfinished] = unfinished_items
h
end
if you wanted to use different names for the method-backed values.
If you care about XML and JSON, have a look at serializable_hash.
With Rails 4, you can do the following -
render json: #my_object.to_json(:methods => [:finished_items, :unfinished_items])
Hope this helps somebody who is on the later / latest version
Another way to do this is add this to your model:
def attributes
super.merge({'unfinished' => unfinished_items, 'finished' => finished_items})
end
This would also automatically work for xml serialization.
http://api.rubyonrails.org/classes/ActiveModel/Serialization.html
Be aware though, you might want use strings for the keys, since the method can not deal with symbols when sorting the keys in rails 3. But it is not sorted in rails 4, so there shouldn't be a problem anymore.
just close all of your data into one hash, like
render json: {items: items, finished: finished, unfinished: unfinished}
I just thought I'd provide this answer for anyone like myself, who was trying to integrate this into an existing as_json block:
def as_json(options={})
super(:only => [:id, :longitude, :latitude],
:include => {
:users => {:only => [:id]}
}
).merge({:premium => premium?})
Just tack .merge({}) on to the end of your super()
This will do, without having to do some ugly overridings. If you got a model List for example, you can put this in your controller:
render json: list.attributes.merge({
finished_items: list.finished_items,
unfinished_items: list.unfinished_items
})
As Aswin listed above, :methods will enable you to return a specific model's method/function as a json attribute, in case you have complex assosiations this will do the trick since it will add functions to the existing model/assossiations :D it will work like a charm if you dont want to redefine as_json
Check this code, and please notice how i'm using :methods as well as :include [N+Query is not even an option ;)]
render json: #YOUR_MODEL.to_json(:methods => [:method_1, :method_2], :include => [:company, :surveys, :customer => {:include => [:user]}])
Overwritting as_json function will be way harder in this scenario (specially because you have to add the :include assossiations manually :/
def as_json(options = { })
end
If you want to render an array of objects with their virtual attributes, you can use
render json: many_users.as_json(methods: [:first_name, :last_name])
where first_name and last_name are virtual attributes defined on your model

ruby object array... or hash

I have an object now:
class Items
attr_accessor :item_id, :name, :description, :rating
def initialize(options = {})
options.each {
|k,v|
self.send( "#{k.to_s}=".intern, v)
}
end
end
I have it being assigned as individual objects into an array...
#result = []
some loop>>
#result << Items.new(options[:name] => 'name', options[:description] => 'blah')
end loop>>
But instead of assigning my singular object to an array... how could I make the object itself a collection?
Basically want to have the object in such a way so that I can define methods such as
def self.names
#items.each do |item|
item.name
end
end
I hope that makes sense, possibly I am overlooking some grand scheme that would make my life infinitely easier in 2 lines.
A few observations before I post an example of how to rework that.
Giving a class a plural name can lead to a lot of semantic issues when declaring new objects, as in this case you'd call Items.new, implying you're creating several items when in fact actually making one. Use the singular form for individual entities.
Be careful when calling arbitrary methods, as you'll throw an exception on any misses. Either check you can call them first, or rescue from the inevitable disaster where applicable.
One way to approach your problem is to make a custom collection class specifically for Item objects where it can give you the information you need on names and such. For example:
class Item
attr_accessor :item_id, :name, :description, :rating
def initialize(options = { })
options.each do |k,v|
method = :"#{k}="
# Check that the method call is valid before making it
if (respond_to?(method))
self.send(method, v)
else
# If not, produce a meaningful error
raise "Unknown attribute #{k}"
end
end
end
end
class ItemsCollection < Array
# This collection does everything an Array does, plus
# you can add utility methods like names.
def names
collect do |i|
i.name
end
end
end
# Example
# Create a custom collection
items = ItemsCollection.new
# Build a few basic examples
[
{
:item_id => 1,
:name => 'Fastball',
:description => 'Faster than a slowball',
:rating => 2
},
{
:item_id => 2,
:name => 'Jack of Nines',
:description => 'Hypothetical playing card',
:rating => 3
},
{
:item_id => 3,
:name => 'Ruby Book',
:description => 'A book made entirely of precious gems',
:rating => 1
}
].each do |example|
items << Item.new(example)
end
puts items.names.join(', ')
# => Fastball, Jack of Nines, Ruby Book
Do you know the Ruby key word yield?
I'm not quite sure what exactly you want to do. I have two interpretations of your intentions, so I give an example that makes two completely different things, one of them hopefully answering your question:
class Items
#items = []
class << self
attr_accessor :items
end
attr_accessor :name, :description
def self.each(&args)
#items.each(&args)
end
def initialize(name, description)
#name, #description = name, description
Items.items << self
end
def each(&block)
yield name
yield description
end
end
a = Items.new('mug', 'a big cup')
b = Items.new('cup', 'a small mug')
Items.each {|x| puts x.name}
puts
a.each {|x| puts x}
This outputs
mug
cup
mug
a big cup
Did you ask for something like Items.each or a.each or for something completely different?
Answering just the additional question you asked in your comment to tadman's solution: If you replace in tadman's code the definition of the method names in the class ItemsCollection by
def method_missing(symbol_s, *arguments)
symbol, s = symbol_s.to_s[0..-2], symbol_s.to_s[-1..-1]
if s == 's' and arguments.empty?
select do |i|
i.respond_to?(symbol) && i.instance_variables.include?("##{symbol}")
end.map {|i| i.send(symbol)}
else
super
end
end
For his example data you will get following outputs:
puts items.names.join(', ')
# => Fastball, Jack of Nines, Ruby Book
puts items.descriptions.join(', ')
# => Faster than a slowball, Hypothetical playing card, A book made entirely of precious gems
As I don't know about any way to check if a method name comes from an attribute or from another method (except you redefine attr_accessor, attr, etc in the class Module) I added some sanity checks: I test if the corresponding method and an instance variable of this name exist. As the class ItemsCollection does not enforce that only objects of class Item are added, I select only the elements fulfilling both checks. You can also remove the select and put the test into the map and return nil if the checks fail.
The key is the return value. If not 'return' statement is given, the result of the last statement is returned. You last statement returns a Hash.
Add 'return self' as the last line of initialize and you're golden.
Class Item
def initialize(options = {})
## Do all kinds of stuff.
return self
end
end

Resources