Rails: Use URL Helper in Observer - ruby-on-rails

I have an observer which looks like this:
class CommentObserver < ActiveRecord::Observer
include ActionView::Helpers::UrlHelper
def after_create(comment)
message = "#{link_to comment.user.full_name, user_path(comment.user)} commented on #{link_to 'your photo',photo_path(comment.photo)} of #{comment.photo.location(:min)}"
Notification.create(:user=>comment.photo.user,:message=>message)
end
end
Basically all I'm using it to do is create a simple notification message for a certain user when someone posts a comment on one of their photos.
This fails with an error message:
NoMethodError (undefined method `link_to' for #<CommentObserver:0x00000102fe9810>):
I would have expected including ActionView::Helpers::UrlHelper would solve that, but it seems to have no effect.
So, how can I include the URL helper in my observer, or else render this some other way? I would happily move the "message view" into a partial or something, but an observer has no associated views to move this to...

Why aren't you building the message when it's rendered out to the page and then caching it using something like this?
<% cache do %>
<%= render user.notifications %>
<% end %>
This would save you having to do a hack in the observer and would be more "standards compliant" in Rails.

To handle this type of thing, I made an AbstractController to generate the body of the email, then I pass that in as a variable to the mailer class:
class AbstractEmailController < AbstractController::Base
include AbstractController::Rendering
include AbstractController::Layouts
include AbstractController::Helpers
include AbstractController::Translation
include AbstractController::AssetPaths
include Rails.application.routes.url_helpers
include ActionView::Helpers::AssetTagHelper
# Uncomment if you want to use helpers
# defined in ApplicationHelper in your views
# helper ApplicationHelper
# Make sure your controller can find views
self.view_paths = "app/views"
self.assets_dir = '/app/public'
# You can define custom helper methods to be used in views here
# helper_method :current_admin
# def current_admin; nil; end
# for the requester to know that the acceptance email was sent
def generate_comment_notification(comment, host = ENV['RAILS_SERVER'])
render :partial => "photos/comment_notification", :locals => { :comment => comment, :host => host }
end
end
In my observer:
def after_create(comment)
email_body = AbstractEmailController.new.generate_comment_notification(comment)
MyMailer.new(comment.id, email_body)
end

So, it turns out this cannot be done for the same reason you can't use link_to in a mailer view. The observer has no information about the current request, and therefore cannot use the link helpers. You have to do it a different way.

Related

Rails - undefined method `model_name' when calling edit

I have a form to edit a page, while it tells it's not a variable from what I have known from related questions. In view, an error is raised from this line:
<%= form_for #wiki, :url => giki_path(#wiki.name), :html => { :method => :put } do |f| %>
Where the #wiki does seem to be an instance, which can be confirmed by:
$ rails console
> #wiki
#<Gollum::Page:70026260995800 Home (markdown) #wiki="path/to/git/wiki/.git">
> #wiki.name
"/wiki/Home"
So I don't understand what is causing the problem:
undefined method `model_name' for #<Gollum::Page:0x007f6084d2bdb0>
Edit:
In controller:
# giki_controller.rb
def edit
#wiki = Wiki.find(params[:id])
end
# the same method, worked fine
def show
#wiki = Wiki.find(params[:id])
end
In model:
# wiki.rb
class Wiki
include ActiveModel::AttributeMethods
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Naming
attr_accessor :name, :raw_data, :formatted_data, :title, :path, :change_desc, :versions
# Gollum Init
WIKI = Gollum::Wiki.new(Settings.wiki_repo, :base_path => "/wiki")
# initialize
def initialize(attributes = {})
attributes.each do |key, value|
send("#{key}=", value)
end
end
# no database
def persisted?
false
end
def self.find(name)
WIKI.page(name) # find a page by name
end
First lines from logger:
NoMethodError - undefined method `model_name' for #<Gollum::Page:0x007f607dfec4e8>:
actionpack (4.2.6) lib/action_controller/model_naming.rb:9:in `model_name_from_record_or_class'
actionview (4.2.6) lib/action_view/record_identifier.rb:47:in `dom_class'
Full traceback: I created a gist.
Your backtrace says that model_name is undefined in <Gollum::Page:0x007f607dfec4e8> which is an instance of Gollum::Page.
Reason
form_for method internally calls model_name method. This is actually a valid method name in ActiveRecord's instance.
Try
User.first.model_name
This model_name is not present in #wiki since this is not an instance of Wiki its rather the instance of Gollum::Page.
How can I say that?
Well, I saw you have overridden the self.find method in Wiki
def self.find(name)
WIKI.page(name) # find a page by name
end
so in your edit action, you have used find method to get the persisted instance, which will hand you over an instance Gollum::Page and this is not expected by form_for helper method.
Solution (Edited)
Well, if you were using ActiveRecord and wanted to continue the overridden self.find method then you can use where or find_by_x method instead in edit action. Like
def edit
#wiki = Wiki.find_by_id(params[:id])
end
But looks like you are not using ActiveRecord or your model is not derived from it, so you have to use the form_for method in different fashion.
If you don't need to attach a form to a model instance, then check out ActionView::Helpers::FormTagHelper#form_tag.
form_tag(giki_path(#wiki.name), method: :put)

How to define a controller function that works for instances of any model

Implementing versioning for a Rails app I'd like to have a view that displays all versions of a model with some extra functionality like reverting etc.
I use the paper_trail gem for the versioning.
I know that I could do that by writing a controller function like versions and a view for every model but I'd like to do it for all models at once. This should be possible because the model.versions attribute is always structured identically.
Ideally the URL should look like /pages/testpage/versions while testpage is the page id.
This seems similar to the concept of nested routes in rails.
resources :pages do
resources :versions
end
The problems with nested routes however are:
Needs extra configuration per model
I cannot access the testpage object without knowing of which model it is an instance.
I also wasn't able to find a way to determine the model since the only thing that is provided to my versions controller is the params hash.
I'm completely open to alternative solutions that might not follow my initial ideas.
Write it in your ApplicationController and define it as a helper_method.
For example
class ApplicationController < ActionController::Base
helper_method :current_time
def current_time
Time.now
end
end
Now you can cal current_time everywhere in controllers or views.
Also you can write separate Module/Class and define there your helpers methods. Than you should include this file into your ApplicationController as well
UPD after theme is changed
I didn't think about your actual question. But I can say that your approach is nod the best here.
You should create new resource instead of creating new functionality which will hard to be tested. So create new resource (controller): versions and play around this controller.
For example how it can work:
/versions/pages/132
/versions/comments/1003
How to realize it:
match "/versions/:model/:id", :to => "versions#index"
In your controller:
class VersionsController < ActionController::Base
def index
#object = my_type.find(params[:id])
#versions = #object.versions
end
private
def my_type
params[:model].constantize
end
end
Of course you can change routes the way you want:
match "/:model/:id/versions", :to => "versions#show"
So now your pretty /pages/testpage/versions will work fine for you without any new strange logic.
UPD 2
Imagine you have got this route:
match "/:model/:id/versions", :to => "versions#index", :as => :versions
And this objects:
#page = Page.last
#hotel = Hotel.find(123)
#comment = #page.comments.first
How will we create links for versions:
<%= link_to "Versions of this page", versions_path(:model => #page.class.to_s, :id => #page.id) %>
<%= link_to "Versions of this hotel", versions_path(:model => #hotel.class.to_s, :id => #hotel.id) %>
<%= link_to "Versions of this comment", versions_path(:model => #comment.class.to_s, :id => #comment.id) %>
I would suggest passing a param such as 'type' and stuff the model name there. Then in your controller you can do:
class VersionsController < ApplicationController
def index
model = params[:type].classify.constantize
#obj = model.find(params[:id])
end
end
For your links, you can pass queries to the link_to helper
<%= link_to versions_path(#model, :type => #model.class) %>
Or something along those lines.

Trouble rendering a view inside a generic class

I'm trying to encapsulate the logic for generating my sitemap in a separate class so I can use Delayed::Job to generate it out of band:
class ViewCacher
include ActionController::UrlWriter
def initialize
#av = ActionView::Base.new(Rails::Configuration.new.view_path)
#av.class_eval do
include ApplicationHelper
end
end
def cache_sitemap
songs = Song.all
sitemap = #av.render 'sitemap/sitemap', :songs => songs
Rails.cache.write('sitemap', sitemap)
end
end
But whenever I try ViewCacher.new.cache_sitemap I get this error:
ActionView::TemplateError:
ActionView::TemplateError (You have a nil object when you didn't expect it!
The error occurred while evaluating nil.url_for) on line #5 of app/views/sitemap/_sitemap.builder:
I assume this means that ActionController::UrlWriter is not included in the right place, but I really don't know
Does this do what you're trying to do? This is untested, just an idea.
in lib/view_cacher.rb
module ViewCacher
def self.included(base)
base.class_eval do
#you probably don't even need to include this
include ActionController::UrlWriter
attr_accessor :sitemap
def initialize
#av = ActionView::Base.new(Rails::Configuration.new.view_path)
#av.class_eval do
include ApplicationHelper
end
cache_sitemap
super
end
def cache_sitemap
songs = Song.all
sitemap = #av.render 'sitemap/sitemap', :songs => songs
Rails.cache.write('sitemap', sitemap)
end
end
end
end
then wherever you want to render (I think your probably in your SitemapController):
in app/controllers/sitemap_controller.rb
class SitemapController < ApplicationController
include ViewCacher
# action to render the cached view
def index
#sitemap is a string containing the rendered text from the partial located on
#the disk at Rails::Configuration.new.view_path
# you really wouldn't want to do this, I'm just demonstrating that the cached
# render and the uncached render will be the same format, but the data could be
# different depending on when the last update to the the Songs table happened
if params[:cached]
#songs = Song.all
# cached render
render :text => sitemap
else
# uncached render
render 'sitemap/sitemap', :songs => #songs
end
end
end

undefined method 'link_to'

I'm writing a ruby-on-rails library module:
module Facets
class Facet
attr_accessor :name, :display_name, :category, :group, :special
...
URI = {:controller => 'wiki', :action => 'plants'}
SEARCH = {:status => WikiLink::CURRENT}
#Parameters is an hash of {:field => "1"} values
def render_for_search(parameters)
result = link_to(display_name, URI.merge(parameters).merge({name => "1"}))
count = WikiPlant.count(:conditions => (SEARCH.merge(parameters.merge({name => "1"}))))
result << "(#{count})"
end
end
...
end
when I call render_for_search I get the error
undefined method 'link_to'
I've tried requiring url_helper directly but can't figure out what's going wrong.
Try this:
ActionController::Base.helpers.link_to
This is because, ActionView urlhelpers are only available to the Views, not in your lib directory.
the link_to method is found in the ActionView::Helpers::UrlHelper module, plus you wou
so try this.
class Facet
include ActionView::Helpers::UrlHelper
...
end
Simply including the helper doesn't get you much further. The helpers assume that they are in the context of a request, so that they can read out the domain name and so on.
Do it the other way around; include your modules in the application helper, or something like that.
# lib/my_custom_helper.rb
module MyCustomHelper
def do_stuff
# use link_to and so on
end
end
# app/helpers/application_helper.rb
module ApplicationHelper
include MyCustomHelper
end

Invoke action to send email in ruby on rails

I am attempting to send an email to the present borrower of a book. I've created an ActionMailer called ReturnRequestMailer which has a method called please_return.
class ReturnRequestMailer < ActionMailer::Base
def please_return(book_loan)
subject 'Book Return Request'
recipients book_loan.person.email
from 'andrew.steele#west.cmu.edu'
sent_on Time.now
body :book_loan => book_loan
end
end
I am attempting to call this method from an action inside of my BooksController
def request_return
#book = Book.find(params[:id])
ReturnRequestMailer.please_return(#book.current_loan)
end
Which I invoke from my books index with the following link_to (ignoring for the time being that doing this in this manner probably isn't the smartest permanent solution).
<%= link_to 'Request Return', {:action => 'request_return' , :id => book} %>
Everything links up correctly but I get a NoMethodError in BooksController#request_return stating that it cannot find the method please_return for ReturnRequestMailer. What is going on that is preventing the please_return method from being visible to the BooksController?
add a 'deliver_' in front of your method so it will be :
def request_return
#book = Book.find(params[:id])
ReturnRequestMailer.deliver_please_return(#book.current_loan)
end
You don't need to define 'deliver_please_return' method, The method_missing method in ActionMailer will know to call please_return.
The Mailer in rails is usually used like this:
class ReturnRequestMailer < ActionMailer::Base
def please_return(book_loan)
subject 'Book Return Request'
recipients book_loan.person.email
from 'andrew.steele#west.cmu.edu'
sent_on Time.now
body :book_loan => book_loan
end
end
Then in the controller out deliver_ in front of the method name and call it as a class Method:
def request_return
#book = Book.find(params[:id])
NewsletterMailer.deliver_please_return(#book.current_loan)
end
Looking at your code it looks like the please_return method has been called as a class method, but you have defined it as an instance method. (for more detail on this see To use self. or not.. in Rails )
class ReturnRequestMailer < ActionMailer::Base
def self.please_return(book_loan)
...
should fix it.
Note this won't actual make it send the email, but will stop the NoMethodFound error.
As nasmorn states, you need to call ReturnRequestMailer.deliver_please_return to have the mail delivered.

Resources