问题
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
回答1:
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. So link_to_tip = link_to tip.name, tip_path(tip)
becomes link_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:
belongs_to :source, :polymorphic => true
The Tip model would include:
has_many :messages, :as => :source
Then you do this (using your code as an example):
m = Message.new
m.source = tip
m.save!
The view which renders the message is then responsible for creating the link, like this:
<%= "Hello #{u.name}, a new tip: #{link_to m.source.name, tip_path(m.source)}" %>
来源:https://stackoverflow.com/questions/3538821/ror-undefined-method-url-for-for-nilnilclass