I have a standard Rails application.
When a Tip is created, I would like to create a Message for each User who is interested in that Tip.
This sounds simple right? It should be...
So, we start with a Tip Observer:
class TipObserver < ActiveRecord::Observer
def after_save(tip)
# after the tip is saved, we'll create some messages to inform the users
users = User.interested_in(tip) # get the relevant users
users.each do |u|
m = Message.new
m.recipient = u
link_to_tip = tip_path(tip)
m.body = "Hello #{u.name}, a new tip: #{link_to_tip}"
m.save!
end
end
end
Errors:
tip_observer.rb:13:in `after_save': undefined method `tip_path' for #<TipObserver:0xb75ca17c> (NoMethodError)
Ok, so TipObserver needs access to the UrlWriter methods. This should be fairly straightforward to fix, right?
class TipObserver < ActiveRecord::Observer
include ActionController::UrlWriter
Now it runs(!) and Outputs:
Hello dave18, a new tip: /tips/511
Great that works!! Well it kinda, really we want that to be a click-able link. Again, that should be easy right?
link_to_tip = link_to tip.name, tip_path(tip)
Errors:
tip_observer.rb:13:in `after_save': undefined method `link_to' for #<TipObserver:0xb75f7708> (NoMethodError)
Ok, so this time TipObserver needs access to the UrlHelper methods. This should be fairly straightforward to fix, right?
class TipObserver < ActiveRecord::Observer
include ActionController::UrlWriter
include ActionView::Helpers::UrlHelper
Errors:
whiny_nil.rb:52:in `method_missing': undefined method `url_for' for nil:NilClass (NoMethodError)
Ok, it seems adding that has interfered with the url_for declaration. Lets try the includes in a different order:
class TipObserver < ActiveRecord::Observer
include ActionView::Helpers::UrlHelper
include ActionController::UrlWriter
Errors:
url_rewriter.rb:127:in `merge': can't convert String into Hash (TypeError)
Hmm, there's no obvious way around this. But after reading some clever-clogs suggestion that Sweepers are the same as Observers but have access to the url helpers. So lets convert the Observer to a Sweeper and remove the UrlHelper and UrlWriter.
class TipObserver < ActionController::Caching::Sweeper
observe Tip
#include ActionView::Helpers::UrlHelper
#include ActionController::UrlWriter
Well, that allows it to run, but here's the Output:
Hello torey39, a new tip:
So, there's no error, but the url is not generated. Further investigation with the console reveals that:
tip_path => nil
and therefore:
tip_path(tip) => nil
Ok well I have no idea how to fix that problem, so perhaps we can attack this from a different direction. If we move the content into an erb template, and render the Message.body as a view - that gives two benefits - firstly the "View" content is put in the correct location, and it might help us avoid these *_path problems.
So lets change the after_save method:
def after_save(tip)
...
template_instance = ActionView::Base.new(Rails::Configuration.new.view_path)
m.body = template_instance.render(:partial => "messages/tip", :locals => {
:user=>user,
:tip=>tip
})
m.save!
end
Errors:
undefined method `url_for' for nil:NilClass (ActionView::TemplateError)
Great, but now we're back to this bloody url_for again. So this time its the ActionView thats complaining. Lets try and fix this then:
def after_save(tip)
...
template_instance = ActionView::Base.new(Rails::Configuration.new.view_path)
template_instance.extend ActionController::UrlWriter
Errors:
undefined method `default_url_options' for ActionView::Base:Class
Great so whatever we do we end up with errors. I've tried many many way of assigning default_url_options
inside the template_instance
without success.
So far this doesn't feel very "Railsy", in fact it feels downright difficult.
So my question is:
- Am I trying to get a square peg in a round hole? If so, how should I adapt the architecture to provide this functionality? I can't believe its not something that exists in other websites.
- Should I give up trying to use an Observer or Sweeper?
- Should I be trying to create new Messages via the MessagesController, and if so, how can I invoke the MessagesController directly and multiple times from within the Observer/Sweeper?
Any tips advice or suggestions would be very gratefully recieved, I have been banging my head against this brick wall for several days now and slowly losing the will to live.
tia
Keith
Well you are right that your approach isn't very Rails-y. You are trying to mix model, controller and view methods in a way they aren't designed to and that's always a little shaky.
If I had started down your path, I probably would have given up at the
link_to
problem and (I admit it isn't "the Rails way") coded the HTML for the link manually. Solink_to_tip = link_to tip.name, tip_path(tip)
becomeslink_to_tip = '<a href="#{tip_path(tip)}">#{tip.name}</a>
- a quick and dirty solution if you're looking for one ;-)But in my experience, Rails is pretty neat until you want to do things in a non-standard way. Then it can bite you :-)
The problem is you are writing and storing text in your Message model which shouldn't be there. The Message model should
belong_to Tips
and a view should be responsible for presenting the message text, including the link to the tip. If a Message can be about something other than Tips, you can make a polymorphic association in the Message model like this:The Tip model would include:
Then you do this (using your code as an example):
The view which renders the message is then responsible for creating the link, like this: