Rails, How to render a view/partial in a model

Ruby on-RailsRubyRuby on-Rails-3

Ruby on-Rails Problem Overview


In my model I have:

after_create :push_create

I push_create I need to render a view. I'm trying to do that like so:

  def push_event(event_type)
    X["XXXXX-#{Rails.env}"].trigger(event_type, 
      {
        :content => render( :partial =>"feeds/feed_item", :locals => { :feed_item => self })
      }
    )
  end

This angers rails as it doesn't like me rendering a view in the model but I need it there.

Error:

NoMethodError (undefined method `render' for #<WallFeed:0x1039be070>):

Suggestions? Should I render it somewhere else somehow? Or how can I render in the model to set content? Thanks

Ruby on-Rails Solutions


Solution 1 - Ruby on-Rails

proper solution

Well, "they" are right. You really have to do the rendering in a controller - but it's fair game to call that controller from a model! Fortunately, AbstractController in Rails 3 makes it easier than I thought. I wound up making a simple ActionPusher class, working just like ActionMailer. Perhaps I'll get ambitious and make this a proper gem someday, but this should serve as a good start for anyone else in my shoes.

I got the most help from this link: http://www.amberbit.com/blog/2011/12/27/render-views-and-partials-outside-controllers-in-rails-3/

in lib/action_pusher.rb

class ActionPusher < AbstractController::Base
  include AbstractController::Rendering
  include AbstractController::Helpers
  include AbstractController::Translation
  include AbstractController::AssetPaths
  include Rails.application.routes.url_helpers
  helper ApplicationHelper
  self.view_paths = "app/views"

  class Pushable
    def initialize(channel, pushtext)
      @channel = channel
      @pushtext = pushtext
    end

    def push
      Pusher[@channel].trigger('rjs_push', @pushtext )
    end
  end
end

in app/pushers/users_pusher.rb. I guess the require could go somewhere more global?

require 'action_pusher'

class UsersPusher < ActionPusher
  def initialize(user)
    @user = user
  end

  def channel
    @user.pusher_key
  end

  def add_notice(notice = nil)
    @notice = notice
    Pushable.new channel, render(template: 'users_pusher/add_notice')
  end
end

Now in my model, I can just do this:

after_commit :push_add_notice

private

def push_add_notice
  UsersPusher.new(user).add_notice(self).push
end

and then you'll want a partial, e.g. app/views/users_pusher/add_notice.js.haml, which could be as simple as:

alert('#{@notice.body}')

I guess you don't really need to do it with Pushable inner class and the .push call at the end, but I wanted to make it look like ActiveMailer. I also have a pusher_key method on my user model, to make a channel for each user - but this is my first day with anything like Pusher, so I can't say for sure if that's the right strategy. There's more to be fleshed out, but this is enough for me to get started.

Good luck!

(this was my first draft answer, leaving it in because it might help someone)

I've got the general outline of a solution working. Like this, in your model:

after_create :push_new_message

private

def render_anywhere(partial, assigns = {})
  view = ActionView::Base.new(ActionController::Base.view_paths, assigns)
  view.extend ApplicationHelper
  view.render(:partial => partial)
end  

def push_new_message
  pushstring = render_anywhere('notices/push_new_message', :message_text => self.body)
  Pusher[user.pusher_key].trigger!('new_message', pushstring)
end

that is definitely working - the template is rendering, and gets eval()'ed on the client side successfully. I'm planning to clean it up, almost certainly move render_anywhere somewhere more general, and probably try something like this

I can see that pushes will need their own templates, calling the generally available ones, and I may try to collect them all in one place. One nice little problem is that I sometimes use controller_name in my partials, like to light up a menu item, but I'll obviously have to take a different tactic there. I'm guessing I might have to do something to get more helpers available, but I haven't gotten there yet.

Success! Hooray! This should answer your question, and mine - I'll add more detail if it seems appropriate later. Good luck!!!!

original non-answer from an hour ago left for clarity

I don't have an answer, but this timely question deserves more clarification, and I'm hoping to get closer to my answer by helping ask :)

I'm facing the same problem. To explain a little more clearly, Pusher asynchronously sends content to a connected user browser. A typical use case would be a showing the user they have a new message from another user. With Pusher, you can push a message to the receiver's browser, so they get an immediate notification if they are logged in. For a really great demo of what Pusher can do, check out http://wordsquared.com/

You can send any data you like, such as a JSON hash to interpret how you like it, but it would be very convenient to send RJS, just like with any other ajax call and eval() it on the client side. That way, you could (for example) render the template for your menu bar, updating it in its entirety, or just the new message count displayed to the user, using all the same partials to keep it bone-DRY. In principle, you could render the partial from the sender's controller, but that doesn't make much sense either, and there might not even be a request, it could be triggered by a cron job, for example, or some other event, like a stock price change. The sender controller just should not have to know about it - I like to keep my controllers on a starvation diet ;)

It might sound like a violation of MVC, but it's really not - and it really should be solved with something like ActionMailer, but sharing helpers and partials with the rest of the app. I know in my app, I'd like to send a Pusher event at the same time as (or instead of) an ActionMailer call. I want to render an arbitrary partial for user B based on an event from user A.

These links may point the way towards a solution:

The last one looks the most promising, offering up this tantalizing snippet:

def render_anywhere(partial, assigns)
  view = ActionView::Base.new(Rails::Configuration.new.view_path, assigns)
  ActionView::Base.helper_modules.each { |helper| view.extend helper }
  view.extend ApplicationHelper
  view.render(:partial => partial)
end

As does this link provided by another poster above.

I'll report back if I get something working

tl;dr: me too!

Solution 2 - Ruby on-Rails

I just do this:

ApplicationController.new.render_to_string(partial: 'messages/any', locals: { variable: 'value' })

Solution 3 - Ruby on-Rails

Rails 5 way

In Rails 5 rendering outside a controller became pretty straightforward due to implemented render controller class method:

# render template
ApplicationController.render 'templates/name'
# render action
FooController.render :index
# render file
ApplicationController.render file: 'path'
# render inline
ApplicationController.render inline: 'erb content'

When calling render outside of a controller, one can assign instance variables via assigns option and use any other options available from within a controller:

ApplicationController.render(
  assigns: { article: Article.take },
  template: 'articles/show',
  layout: false
)

Request environment can be tailored either through default options

ApplicationController.render inline: '<%= users_url %>'
# => 'http://default_host.com/users'

ApplicationController.renderer.defaults[:http_host] = 'custom_host.org'
# => "custom_host.org"

ApplicationController.render inline: '<%= users_url %>'
# => 'http://custom_host.org/users'

or explicitly by initializing a new renderer

renderer = ApplicationController.renderer.new(
  http_host: 'custom_host.org',
  https: true
)
renderer.render inline: '<%= users_url %>'
# => 'https://custom_host.org/users'

Hope that helps.

Solution 4 - Ruby on-Rails

You can use ActionView directly and render partials to string without having a controller. I find that pattern useful to create models that encapsulate some javascript generation, for instance.

html = ActionView::Base.new(Rails.configuration.paths['app/views']).render(
  partial: 'test', 
  formats: [:html],
  handlers: [:erb],
  locals: { variable: 'value' }
)

Then, just put your _test.html.erb in you view folder and try it out!

Solution 5 - Ruby on-Rails

Rails 6.0.0 compatible answer, since I ended up on this page while searching for a solution:

lookup_context = ActionView::LookupContext.new(Rails.configuration.paths["app/views"])
renderer = ActionView::Base.new(lookup_context)
renderer.extend(Rails.application.helpers)
renderer.render \
  template: "foo/bar",
  formats: [:html],
  handlers: [:erb],
  locals: { user: User.new }

Solution 6 - Ruby on-Rails

I'm fairly sure the answers you seek lie within Crafting Rails Applications where Jose Valim goes into great detail about how and why you would want to render views straight from your db

Sorry I can't be of more help yet because I've just started reading it myself tonight.

You might find some help here - it's a blog post about doing this sort of thing, albeit using different methods than yours

Solution 7 - Ruby on-Rails

the "proper" way to do this is to push an object in serialized form(json), and then have the view deal with it once the event is received. Perhaps you want to use Handlebars to render the object.

Edit: I originally wrote about how, despite my answer, I was going to follow your example. But I just realized there is a HUGE gotcha with your approach when it comes to push notifications.

In your problem, you are doing push notifications to one user. For me, I was broadcasting out to a set of users. So I was going to render html with a presumption of a "current_user" and all that comes with it(eg logic, permissions, etc). This is NO BUENO as each push notification will be received by a different "current user".

Therefore, really, you need to just send back the data, and let each individual view handle it.

Solution 8 - Ruby on-Rails

You should call all render methods from a controller. So, in this case, you can notify the controller that the object has been created and the controller can then render the view. Also, since you can render only once, I think you can wait for all your server side operations to complete before invoking the render.

Solution 9 - Ruby on-Rails

The render methods are defined on the ActiveController class and its progeny. Inherently you do not have access to it on the model, nor is it a class method so you can't use it without an instance of the controller.

I've never tried to instantiate a controller for the express purpose of simply stringifying a partial, but if you can get your hands on a controller, render_to_string seems to be the way to go.

I will chime in by saying that if you're going down this path you're taking RoR "off the Rails". This is a violation of MVC and fundamentally poor program design.This doesn't mean I think you're a bad person :P Sometimes life drives us off the rails, so to speak.

I can't speak to the details that have driven you to do this, but I'd strongly suggest you rethink your approach.

Solution 10 - Ruby on-Rails

I have created a gist for this.
I needed something similar, where the models don't necessarily (or in my case, ever) get updated via a controller, so the logic can't sit there.

Created a server-push based controller:
https://gist.github.com/4707055

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAnApprenticeView Question on Stackoverflow
Solution 1 - Ruby on-Railsstephan.comView Answer on Stackoverflow
Solution 2 - Ruby on-RailsKevinView Answer on Stackoverflow
Solution 3 - Ruby on-RailsOleg AfanasyevView Answer on Stackoverflow
Solution 4 - Ruby on-RailshsgubertView Answer on Stackoverflow
Solution 5 - Ruby on-RailstroelsknView Answer on Stackoverflow
Solution 6 - Ruby on-RailsstephenmurdochView Answer on Stackoverflow
Solution 7 - Ruby on-RailsPeter P.View Answer on Stackoverflow
Solution 8 - Ruby on-RailsChandranshuView Answer on Stackoverflow
Solution 9 - Ruby on-RailsjaydelView Answer on Stackoverflow
Solution 10 - Ruby on-Railscpuguy83View Answer on Stackoverflow