render_to_string from a rake task - ruby-on-rails

I want to use a Rake task to cache my sitemap so that requests for sitemap.xml won't take forever. Here's what I have so far:
#posts = Post.all
sitemap = render_to_string :template => 'sitemap/sitemap', :locals => {:posts => #posts}, :layout => false
Rails.cache.write('sitemap', sitemap)
But when I try to run this, I get an error:
undefined local variable or method `headers' for #<Object:0x100177298>
How can I render a template to a string from within Rake?

Here's how I did it:
av = ActionView::Base.new(Rails::Configuration.new.view_path)
av.class_eval do
include ApplicationHelper
end
include ActionController::UrlWriter
default_url_options[:host] = 'mysite.com'
posts = Post.all
sitemap = av.render 'sitemap/sitemap', :posts => posts
Rails.cache.write('sitemap', sitemap)
Note that I converted my template to a partial to make this work

There is a post about how to be able to access ActionView::Base methods and context from rake task.
However, this is a monkeypatch. Why not use the rails' cache mechanism to accomplish caching? :)
Later edit:
The render_to_string function is defined in ActionController::Base context.
Below is a solution on how to make it work from rake tasks, taken from omninerd.
# In a rake task:
av = ActionView::Base.new(Rails::Configuration.new.view_path)
Rails.cache.write(
"cache_var",
av.render(
:partial => "view_folder/some_partial",
:locals => {:a_var => #some_var}
)
)

Recently I wanted to take a rake task defined like Horace Loeb mentioned and translate it into a self contained background job, but it didn't easily translate.
Here is my implementation for Rails 2.3.x because the Rails 3 implementation I found wouldn't work.
# Public: Template to render views outside the context of a controller.
#
# Useful for rendering views in rake tasks or background jobs when a
# controller is unavailable.
#
# Examples
#
# template = OfflineTemplate.new(:users)
# template.render("users/index", :layout => false, :locals => { :users => users })
#
# template = OfflineTemplate.new(ProjectsHelper, PermissionsHelper)
# template.render("projects/recent", :projects => recent_projects)
#
class OfflineTemplate
include ActionController::UrlWriter
include ActionController::Helpers::ClassMethods
# Public: Returns the ActionView::Base internal view.
attr_reader :view
# Public: Convenience method to
delegate :render, :to => :view
# Public: Initialize an offline template for the current Rails environment.
#
# helpers - The Rails helpers to include (listed as symbols or modules).
def initialize(*helpers)
helper(helpers + [ApplicationHelper])
#view = ActionView::Base.new(Rails.configuration.view_path, {}, self)
#view.class.send(:include, master_helper_module)
end
private
# Internal: Required to use ActionConroller::Helpers.
#
# Returns a Module to collect helper methods.
def master_helper_module
#master_helper_module ||= Module.new
end
end
This is available as a gist: https://gist.github.com/1386052.
Then you can use the class above to create an OfflineTemplate to render your views in a rake task:
task :recent_projects => :environment do
template = OfflineTemplate.new(ProjectsHelper, PermissionsHelper)
puts template.render("projects/recent", :projects => recent_projects)
end

Related

Rake task to access models

I'm trying to access a model called Book from a rake task like so
task :create_epubs => :environment do
include Rails.application.routes.url_helpers # brings ActionDispatch::Routing::UrlFor
include ActionView::Helpers::TagHelper
av = ActionView::Base.new(Rails.root.join('app', 'views'))
books = Book.all
av.render("books/", :books => books)
end
but i get the following warning
rake aborted!
undefined method `to_sym' for nil:NilClass
Tasks: TOP => create_epubs
(See full trace by running task with --trace)
I'm trying to load environment like the following accessing rails models from rake task but maybe it's slightly off for rails 3.1
*edit Book.all returns something when I do puts Book.all.to_yaml so the to_sym error is probably something else in av.render
I've figured out what the problem is. I was referring to instance variable from my view.
Can anyone tell me how to keep using instance variables by setting that variable?
This is the working version when I change the instance variables to the :params variables
task :create_epubs => [:environment] do
av = ActionView::Base.new(Rails.root.join('app', 'views'), :assigns => self)
av.view_paths = ActionController::Base.view_paths
av.extend ApplicationHelper #or any other helpers your template may need
book = Book.first
puts av.render(:template => "books/epub-show", :locals => {:book => book}, :layout => false) # this isn't passing #book to the view correctly, i get undefined method for nil:nilClass
end
you should probably use instance variables.
#book = Book.first
and in your render
:locals => { :book => #book }
also, i think you want
:layout => nil

URLs in mailer view files using namespaces and custom member actions

I am running Ruby on Rails 3 and I would like to use URLs in mailer view files with namespaces and custom member actions. I read this but I have still problems on implementing that.
In routes.rb I have:
namespace "users" do
resources :accounts do
member do
get 'my_action_name'
post 'my_action_name'
end
end
end
In users_mailer.rb I have:
class Users::Accounts < ActionMailer::Base
default_url_options[:host] = "test_name_site.com"
def account_delete(account)
#account = account
#action_url = users_account_url(:controller => "users/accounts", :action => "my_action_name")
...
end
end
When in the controller I try to do this:
#account = Users::Account.find(params[:id])
Users::Accounts.account_delete(Users::Account.find_by_id(#account.id)).deliver
I get this error:
ActionController::RoutingError (No route matches {:action=>"my_action_name", :controller=>"users/accounts"})
How I can solve that?
In users_mailer.rb I tryed also
# the difference is: from 'users_account_url' to 'users_accounts_url'
#action_url = users_accounts_url(:controller => "users/account_authentications", :action => "confirm_account_authentication_delete")
# the difference is: from 'users_account_url' to 'url_for'
#action_url = url_for(:controller => "users/account_authentications", :action => "confirm_account_authentication_delete")
but it doesn't work.
You are not specify an ID of a record in users_account_url
try this
#action_url = my_action_name_users_account_url(account)
http://0.0.0.0:3000/users/accounts/1/my_action_name
Routing gude

Multiple robots.txt for subdomains in rails

I have a site with multiple subdomains and I want the named subdomains robots.txt to be different from the www one.
I tried to use .htaccess, but the FastCGI doesn't look at it.
So, I was trying to set up routes, but it doesn't seem that you can't do a direct rewrite since every routes needs a controller:
map.connect '/robots.txt', :controller => ?, :path => '/robots.www.txt', :conditions => { :subdomain => 'www' }
map.connect '/robots.txt', :controller => ?, :path => '/robots.club.txt'
What would be the best way to approach this problem?
(I am using the request_routing plugin for subdomains)
Actually, you probably want to set a mime type in mime_types.rb and do it in a respond_to block so it doesn't return it as 'text/html':
Mime::Type.register "text/plain", :txt
Then, your routes would look like this:
map.robots '/robots.txt', :controller => 'robots', :action => 'robots'
For rails3:
match '/robots.txt' => 'robots#robots'
and the controller something like this (put the file(s) where ever you like):
class RobotsController < ApplicationController
def robots
subdomain = # get subdomain, escape
robots = File.read(RAILS_ROOT + "/config/robots.#{subdomain}.txt")
respond_to do |format|
format.txt { render :text => robots, :layout => false }
end
end
end
at the risk of overengineering it, I might even be tempted to cache the file read operation...
Oh, yeah, you'll almost certainly have to remove/move the existing 'public/robots.txt' file.
Astute readers will notice that you can easily substitute RAILS_ENV for subdomain...
Why not to use rails built in views?
In your controller add this method:
class StaticPagesController < ApplicationController
def robots
render :layout => false, :content_type => "text/plain", :formats => :txt
end
end
In the view create a file: app/views/static_pages/robots.txt.erb with robots.txt content
In routes.rb place:
get '/robots.txt' => 'static_pages#robots'
Delete the file /public/robots.txt
You can add a specific business logic as needed, but this way we don't read any custom files.
As of Rails 6.0 this has been greatly simplified.
By default, if you use the :plain option, the text is rendered without
using the current layout. If you want Rails to put the text into the
current layout, you need to add the layout: true option and use the
.text.erb extension for the layout file. Source
class RobotsController < ApplicationController
def robots
subdomain = request.subdomain # Whatever logic you need
robots = File.read( "#{Rails.root}/config/robots.#{subdomain}.txt")
render plain: robots
end
end
In routes.rb
get '/robots.txt', to: 'robots#robots'
For Rails 3:
Create a controller RobotsController:
class RobotsController < ApplicationController
#This controller will render the correct 'robots' view depending on your subdomain.
def robots
subdomain = request.subdomain # you should also check for emptyness
render "robots.#{request.subdomain}"
end
end
Create robots views (1 per subdomain):
views/robots/robots.subdomain1.txt
views/robots/robots.subdomain2.txt
etc...
Add a new route in config/routes.rb: (note the :txt format option)
match '/robots.txt' => 'robots#robots', :format => :txt
And of course, you should declare the :txt format in config/initializers/Mime_types.rb:
Mime::Type.register "text/plain", :txt
Hope it helps.
If you can't configure your http server to do this before the request is sent to rails, I would just setup a 'robots' controller that renders a template like:
def show_robot
subdomain = # get subdomain, escape
render :text => open('robots.#{subdomain}.txt').read, :layout => false
end
Depending on what you're trying to accomplish you could also use a single template instead of a bunch of different files.
I liked TA Tyree's solution but it is very Rails 2.x centric so here is what I came up with for Rail 3.1.x
mime_types.rb
Mime::Type.register "text/plain", :txt
By adding the format in the routes you don't have to worry about using a respond_to block in the controller.
routes.rb
match '/robots.txt' => 'robots#robots', :format => "text"
I added a little something extra on this one. The SEO people were complaining about duplicated content both in subdomains and in SSL pages so I created a two robot files one for production and one for not production which is also going to be served with any SSL/HTTPS requests in production.
robots_controller.rb
class RobotsController < ApplicationController
def robots
site = request.host
protocol = request.protocol
(site.eql?("mysite.com") || site.eql?("www.mysite.com")) && protocol.eql?("http://") ? domain = "production" : domain = "nonproduction"
robots = File.read( "#{Rails.root}/config/robots-#{domain}.txt")
render :text => robots, :layout => false
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

switching rails controller

I have to separate models: nested sections and articles, section has_many articles.
Both have path attribute like aaa/bbb/ccc, for example:
movies # section
movies/popular # section
movies/popular/matrix # article
movies/popular/matrix-reloaded # article
...
movies/ratings # article
about # article
...
In routes I have:
map.path '*path', :controller => 'path', :action => 'show'
How to create show action like
def show
if section = Section.find_by_path!(params[:path])
# run SectionsController, :show
elsif article = Article.find_by_path!(params[:path])
# run ArticlesController, :show
else
raise ActiveRecord::RecordNotFound.new(:)
end
end
You should use Rack middleware to intercept the request and then rewrite the url for your proper Rails application. This way, your routes files remains very simple.
map.resources :section
map.resources :articles
In the middleware you look up the entity associated with the path and remap the url to the simple internal url, allowing Rails routing to dispatch to the correct controller and invoking the filter chain normally.
Update
Here's a simple walkthrough of adding this kind of functionality using a Rails Metal component and the code you provided. I suggest you look at simplifying how path segments are looked up since you're duplicating a lot of database-work with the current code.
$ script/generate metal path_rewriter
create app/metal
create app/metal/path_rewriter.rb
path_rewriter.rb
# Allow the metal piece to run in isolation
require(File.dirname(__FILE__) + "/../../config/environment") unless defined?(Rails)
class PathRewriter
def self.call(env)
path = env["PATH_INFO"]
new_path = path
if article = Article.find_by_path(path)
new_path = "/articles/#{article.id}"
elsif section = Section.find_by_path(path)
new_path = "/sections/#{section.id}"
end
env["REQUEST_PATH"] =
env["REQUEST_URI"] =
env["PATH_INFO"] = new_path
[404, {"Content-Type" => "text/html"}, [ ]]
end
end
For a good intro to using Metal and Rack in general, check out Ryan Bates' Railscast episode on Metal, and episode on Rack.
Rather than instantiating the other controllers I would just render a different template from PathController's show action depending on if the path matches a section or an article. i.e.
def show
if #section = Section.find_by_path!(params[:path])
render :template => 'section/show'
elsif #article = Article.find_by_path!(params[:path])
render :template => 'article/show'
else
# raise exception
end
end
The reason being that, whilst you could create instances of one controller within another, it wouldn't work the way you'd want. i.e. the second controller wouldn't have access to your params, session etc and then the calling controller wouldn't have access to instance variables and render requests made in the second controller.

Resources