Say I have two classes,
Image and Credit
class Image < ActiveRecord::Base
belongs_to :credit
accepts_nested_attributes_for :credit
end
class Credit < ActiveRecord::Base
#has a field called name
has_many :images
end
I want associate a Credit when Image is created, acting a bit like a tag. Essentially, I want behavior like Credit.find_or_create_by_name, but in the client code using Credit, it would be much cleaner if it was just a Create. I can't seem to figure out a way to bake this into the model.
Try this:
class Image < ActiveRecord::Base
belongs_to :credit
attr_accessor :credit_name
after_create { Credit.associate_object(self) }
end
class Credit < ActiveRecord::Base
#has a field called name
has_many :images
def self.associate_object(object, association='images')
credit = self.find_or_create_by_name(object.credit_name)
credit.send(association) << object
credit.save
end
end
Then when you create an image what you can do is something like
Image.create(:attr1 => 'value1', :attr2 => 'value2', ..., :credit_name => 'some_name')
And it will take the name that you feed into the :credit_name value and use it in the after_create callback.
Note that if you decided to have a different object associated with Credit later on (let's say a class called Text), you could do still use this method like so:
class Text < ActiveRecord::Base
belongs_to :credit
attr_accessor :credit_name
before_create { Credit.associate_object(self, 'texts') }
end
Although at that point you probably would want to consider making a SuperClass for all of the classes that belong_to credit, and just having the superclass handle the association. You might also want to look at polymorphic relationships.
This is probably more trouble than it's worth, and is dangerous because it involves overriding the Credit class's initialize method, but I think this might work. My advice to you would be to try the solution I suggested before and ditch those gems or modify them so they can use your method. That being said, here goes nothing:
First you need a way to get at the method caller for the Credit initializer. Let's use a class I found on the web called CallChain, but we'll modify it for our purposes. You would probably want to put this in your lib folder.
class CallChain
require 'active_support'
def self.caller_class
caller_file.split('/').last.chomp('.rb').classify.constantize
end
def self.caller_file(depth=1)
parse_caller(caller(depth+1).first).first
end
private
#Stolen from ActionMailer, where this was used but was not made reusable
def self.parse_caller(at)
if /^(.+?):(\d+)(?::in `(.*)')?/ =~ at
file = Regexp.last_match[1]
line = Regexp.last_match[2].to_i
method = Regexp.last_match[3]
[file, line, method]
end
end
end
Now we need to overwrite the Credit classes initializer because when you make a call to Credit.new or Credit.create from another class (in this case your Image class), it is calling the initializer from that class. You also need to ensure that when you make a call to Credit.create or Credit.new that you feed in :caller_class_id => self.id to the attributes argument since we can't get at it from the initializer.
class Credit < ActiveRecord::Base
#has a field called name
has_many :images
attr_accessor :caller_class_id
def initialize(args = {})
super
# only screw around with this stuff if the caller_class_id has been set
if caller_class_id
caller_class = CallChain.caller_class
self.send(caller_class.to_param.tableize) << caller_class.find(caller_class_id)
end
end
end
Now that we have that setup, we can make a simple method in our Image class which will create a new Credit and setup the association properly like so:
class Image < ActiveRecord::Base
belongs_to :credit
accepts_nested_attributes_for :credit
# for building
def build_credit
Credit.new(:attr1 => 'val1', etc.., :caller_class_id => self.id)
end
# for creating
# if you wanted to have this happen automatically you could make the method get called by an 'after_create' callback on this class.
def create_credit
Credit.create(:attr1 => 'val1', etc.., :caller_class_id => self.id)
end
end
Again, I really wouldn't recommend this, but I wanted to see if it was possible. Give it a try if you don't mind overriding the initialize method on Credit, I believe it's a solution that fits all your criteria.
Related
class User
has_many :addresses
def csv_header
self.addresses.attribute_names
end
def csv_values
self.addresses.all do |addr|
addr.attributes.values
end
end
end
class Address
belongs_to :user
end
*i am trying to pull the attribute names of the address model to user model,but this method isn't working so can anyone help *
Not much needed here - I think you just need to map the addresses in csv_values.
class User
has_many :addresses
def csv_header
addresses.attribute_names
end
def csv_values
addresses.map do |addr|
addr.attributes.values
end
end
end
class Address
belongs_to :user
end
Does that fix this for you?
I'd be tempted to shift things around a little for clarity in the code and make use of delegate:
class user
...
delegate :attribute_names, to: :addresses, prefix: true, allow_nil: true
...
end
class Address
...
def self.mapped_values
all.map { |addr| addr.attributes.values }
end
...
end
Then you can just call user.addresses_attribute_names and user.addresses.mapped_values.
You can also just call Address.column_names to get the header array, if it will always stay consistent, as is likely to be the case.
Hope that helps!
Update based on comment:
To achieve the same for users, you can call the following:
Either call User.column_names or user.attribute_names to get the headers (on the class for the former, and an instance for the latter).
If you also need the users' mapped values, you can copy across the self.mapped_values method from the address model and use that. It's a little duplication, but for a pair of methods like this I wouldn't be inclined to separate these into a separate module.
Final tip - if you're calling the address methods from a collection of users (i.e. User.all) make sure you adjust it to include the addresses to avoid hitting your database in an inefficient way (User.includes(:addresses)...).
I'm trying to make a STI Base model which changes automatically to inherited class like that:
#models/source/base.rb
class Source::Base < ActiveRecord::Base
after_initialize :detect_type
private
def detect_type
if (/(rss)$/ ~= self.url)
self.type = 'Source::RSS'
end
end
end
#models/source/rss.rb
class Source::RSS < Source::Base
def get_content
puts 'Got content from RSS'
end
end
And i want such behavior:
s = Source::Base.new(:url => 'http://stackoverflow.com/rss')
s.get_content #=> Got content from RSS
s2 = Source::Base.first # url is also ending rss
s2.get_content #=> Got content from RSS
There are (at least) three ways to do this:
1. Use a Factory method
#Alejandro Babio's answer is a good example of this pattern. It has very few downsides, but you have to remember to always use the factory method. This can be challenging if third-party code is creating your objects.
2. Override Source::Base.new
Ruby (for all its sins) will let you override new.
class Source::Base < ActiveRecord::Base
def self.new(attributes)
base = super
return base if base.type == base.real_type
base.becomes(base.real_type)
end
def real_type
# type detection logic
end
end
This is "magic", with all of the super cool and super confusing baggage that can bring.
3. Wrap becomes in a conversion method
class Source::Base < ActiveRecord::Base
def become_real_type
return self if self.type == self.real_type
becomes(real_type)
end
def real_type
# type detection logic
end
end
thing = Source::Base.new(params).become_real_type
This is very similar to the factory method, but it lets you do the conversion after object creation, which can be helpful if something else is creating the object.
Another option would be to use a polymorphic association, your classes could look like this:
class Source < ActiveRecord::Base
belongs_to :content, polymorphic: true
end
class RSS < ActiveRecord::Base
has_one :source, as: :content
validates :source, :url, presence: true
end
When creating an instance you'd create the the source, then create and assign a concrete content instance, thus:
s = Source.create
s.content = RSS.create url: exmaple.com
You'd probably want to accepts_nested_attributes_for to keep things simpler.
Your detect_type logic would sit either in a controller, or a service object. It could return the correct class for the content, e.g. return RSS if /(rss)$/ ~= self.url.
With this approach you could ask for Source.all includes: :content, and when you load the content for each Source instance, Rails' polymorphism will instanciate it to the correct type.
If I were you I would add a class method that returns the right instance.
class Source::Base < ActiveRecord::Base
def self.new_by_url(params)
type = if (/(rss)$/ ~= params[:url])
'Source::RSS'
end
raise 'invalid type' unless type
type.constantize.new(params)
end
end
Then you will get the behavior needed:
s = Source::Base.new_by_url(:url => 'http://stackoverflow.com/rss')
s.get_content #=> Got content from RSS
And s will be an instance of Source::RSS.
Note: after read your comment about becomes: its code uses klass.new. And new is a class method. After initialize, your object is done and it is a Source::Base, and there are no way to change it.
I have a model which has a dependency on a separate, joined model.
class Magazine < ActiveRecord::Base
has_one :cover_image, dependent: :destroy, as: :imageable
end
class Image < ActiveRecord::Base
belongs_to :imageable, polymorphic: true
end
Images are polymorphic and can be attached to many objects (pages and articles) not just magazines.
The magazine needs to update itself when anything about its associated image changes
The magazine also stores a screenshot of itself that can be used for publicising it:
class Magazine < ActiveRecord::Base
has_one :cover_image, dependent: :destroy, as: :imageable
has_one :screenshot
def generate_screenshot
# go and create a screenshot of the magazine
end
end
Now if the image changes, the magazine also needs to update its screenshot. So the magazine really needs to know when something happens to the image.
So we could naively trigger screenshot updates directly from the cover image
class Image < ActiveRecord::Base
belongs_to :imageable, polymorphic: true
after_save { update_any_associated_magazine }
def update_any_associated_magazine
# figure out if this belongs to a magazine and trigger
# screenshot to regenerate
end
end
...however the image shouldn't be doing stuff on behalf of the magazine
However the image could be used in lots of different objects and really shouldn't be doing actions specific to the Magazine as it's not the Image's responsibility to worry about. The image might be attached to pages or articles as well and doesn't need to be doing all sorts of stuff for them.
The 'normal' rails approach is to use an observer
If we were to take a Rails(y) approach then we could create a third party observer that would then trigger an event on the associated magazine:
class ImageObserver < ActiveRecord::Observer
observe :image
def after_save image
Magazine.update_magazine_if_includes_image image
end
end
However this feels like a bit of a crappy solution to me.
We've avoided the Image being burdened by updating the magazine which was great but we've really just punted the problem downstream. It's not obvious that this observer exists, it's not clear inside the Magazine object that the update to the Image will in fact trigger an update to the magazine and we've got a weird floating object which has logic that really just belongs in Magazine.
I don't want an observer - I just want one object to be able to subscribe to events on another object.
Is there any way to subscribe to one model's changes directly from another?
What I would much rather do is have the magazine subscribe directly to events on the image. So the code would instead look like:
class Magazine < ActiveRecord::Base
...
Image.add_after_save_listener Magazine, :handle_image_after_save
def self.handle_image_after_save image
# determine if image belongs to magazine and if so update it
end
end
class Image < ActiveRecord::Base
...
def self.add_after_save_listener class_name, method
##after_save_listeners << [class_name, method]
end
after_save :notify_after_save_listeners
def notify_after_save_listeners
##after_save_listeners.map{ |listener|
class_name = listener[0]
listener_method = listener[1]
class_name.send listener_method
}
end
Is this a valid approach and if not why not?
This pattern seems sensible to me. It uses class variables and methods so doesn't make any assumptions of particular instances being available.
However, I'm old enough and wise enough now to know that if something seemingly obvious hasn't been done already in Rails there's probably a good reason for it.
This seems cool to me. What's wrong with it though? Why do all the other solutions I see all draft in a third party object to deal with things? Would this work?
I use Redis:
In an initializer I set up Redis:
# config/initializers/redis.rb
uri = URI.parse ENV.fetch("REDISTOGO_URL", 'http://127.0.0.1:6379')
REDIS_CONFIG = { host: uri.host, port: uri.port, password: uri.password }
REDIS = Redis.new REDIS_CONFIG
It'll default to my local redis installation in development but on Heroku it'll use Redis To Go.
Then I publish using model callbacks:
class MyModel < ActiveRecord::Base
after_save { REDIS.publish 'my_channel', to_json }
end
Then I can subscribe from anywhere, such as a controller I'm using to push events using Event Source
class Admin::EventsController < Admin::BaseController
include ActionController::Live
def show
response.headers["Content-Type"] = "text/event-stream"
REDIS.psubscribe params[:event] do |on|
on.pmessage do |pattern, event, data|
response.stream.write "event: #{event}\n"
response.stream.write "data: #{data}\n\n"
end
end
rescue IOError => e
logger.info "Stream closed: #{e.message}"
ensure
redis.quit
response.stream.close
end
end
Redis is great for flexible pub/sub. That code I have in the controller can be placed anywhere, let's say in an initializer:
# config/initializers/subscribers.rb
REDIS.psubscribe "image_update_channel" do |on|
on.pmessage do |pattern, event, data|
image = Image.find data['id']
image.imageable # update that shiz
end
end
Now that will handle messages when you update your image:
class Image < ActiveRecord::Base
belongs_to :imageable, polymorphic: true
after_save { REDIS.publish 'image_update_channel', to_json }
end
There is ActiveSupport Notifications mechanism for implementing pub/sub in Rails.
First, you should define instrument which will publish events:
class Image < ActiveRecord::Base
...
after_save :publish_image_changed
private
def publish_image_changed
ActiveSupport::Notifications.instrument('image.changed', image: self)
end
end
Then you should subscribe for this event (you can put this code in initializer):
ActiveSupport::Notifications.subscribe('image.changed') do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
image = event.payload[:image]
# If you have no other cases than magazine, you can check it when you publish event.
return unless image.imageable.is_a?(Magazine)
MagazineImageUpdater.new(image.imageable).run
end
I'll give it a shot...
Use public_send to notify the parent class of a change:
class BaseModel < ActiveRecord::Base
has_one :child_model
def respond_to_child
# now generate the screenshot
end
end
class ChildModel < ActiveRecord::Base
belongs_to :base_model
after_update :alert_base
def alert_base
self.base_model.public_send( :respond_to_child )
end
end
I've been using a model of my application as a proxy to other objects that define behavior.
class Box < ActiveRecord::Base
belongs_to :box_behavior, :polymorphic => true, :validate => true, :foreign_key => 'box_behavior_id', :dependent => :destroy
[...]
def initialize(opts = {})
super(opts)
self.box_behavior = BoxBehaviorDefault.new if self.box_behavior.blank?
end
private
def method_missing(method, *args, &block)
super
rescue NoMethodError
return self.box_behavior.send(method,*args,&block)
end
end
So I implement all the methods on my BoxBehavior objects, and when I call a method on a box instance then it redirects the call to the associated boxbehavior object. It all works fine except when i tried to create a hook on my purchase model where it gets the total from its box object and saves it:
class Purchase < ActiveRecord::Base
belongs_to :box
before_validation_on_create { |r| r.total = r.box.total }
end
When I try to save any purchase object that has a box associated, I get this error:
undefined method `total' for #<ActiveRecord::Associations::BelongsToAssociation:0x7fe944320390>
And I don't have a clue on what to do next... When I implement the total method directly in the box class then it works fine... what can I do to solve this? Isn't the proxy working properly?
I found out that Rails doesn't always use initialize to create a new instance of a model. So i used the hook after_initialize and solved the problem!
So let's say you have
line_items
and line_items belong to a make and a model
a make has many models and line items
a model belongs to a make
For the bare example idea LineItem.new(:make => "Apple", :model => "Mac Book Pro")
When creating a LinteItem you want a text_field box for a make and a model. Makes and models shouldn't exist more than once.
So I used the following implementation:
before_save :find_or_create_make, :if => Proc.new {|line_item| line_item.make_title.present? }
before_save :find_or_create_model
def find_or_create_make
make = Make.find_or_create_by_title(self.make_title)
self.make = make
end
def find_or_create_model
model = Model.find_or_create_by_title(self.model_title) {|u| u.make = self.make}
self.model = model
end
However using this method means I have to run custom validations instead of a #validates_presence_of :make due to the associations happening off a virtual attribute
validate :require_make_or_make_title, :require_model_or_model_title
def require_make_or_make_title
errors.add_to_base("Must enter a make") unless (self.make || self.make_title)
end
def require_model_or_model_title
errors.add_to_base("Must enter a model") unless (self.model || self.model_title)
end
Meh, this is starting to suck. Now where it really sucks is editing with forms. Considering my form fields are a partial, my edit is rendering the same form as new. This means that :make_title and :model_title are blank on the form.
I'm not really sure what the best way to rectify the immediately above problem is, which was the final turning point on me thinking this needs to be refactored entirely.
If anyone can provide any feedback that would be great.
Thanks!
I don't think line_items should belong to a make, they should only belong to a model. And a model should have many line items. A make could have many line items through a model. You are missing a couple of methods to have your fields appear.
class LineItem
belongs_to :model
after_save :connect_model_and_make
def model_title
self.model.title
end
def model_title=(value)
self.model = Model.find_or_create_by_title(value)
end
def make_title
self.model.make.title
end
def make_title=(value)
#make = Make.find_or_create_by_title(value)
end
def connect_model_and_make
self.model.make = #make
end
end
class Model
has_many :line_items
belongs_to :make
end
class Make
has_many :models
has_many :line_items, :through => :models
end
It's really not that bad, there's just not super easy way to do it. I hope you put an autocomplete on those text fields at some point.