I'm trying to use a named route with {{id}} as one of the params, allowing the rendered content to be consumed by Handlebars. url_for is escaping the param so the resulting url contains %7B%7Bid%7D%7D. I've tried adding :escape => false to the call, but it has no effect.
routes.rb
resources :rants, :except => [:show] do
post '/votes/:vote', :controller => 'votes', :action => 'create', :as => 'vote'
end
index.haml
%script{:id => 'vote_template', :type => 'text/x-handlebars-template'}
.votes
= link_to 'up', rant_vote_path(:rant_id => '{{id}}', :vote => 'up')
%span {{votes}}
= link_to 'down', rant_vote_path(:rant_id => '{{id}}', :vote => 'down')
application.js
var vote_template = Handlebars.compile($('#vote_template').html());
output
<script id="vote_template" type="text/x-handlebars-template">
<div class='votes'>
up
<span>{{votes}}</span>
down
</div>
</script>
I've simplified the example for the sake of readability but question remains the same; is there any way to use a named route with {{ }} as a param? I understand I can just do link_to 'up', '/rants/{{id}}/votes/up' so please don't supply that as an answer.
The trouble is the mustache characters are not valid in URLs and are being escaped. I'd recommend creating a wrapper.
def handlebar_path(helper, arguments={})
send("#{helper}_path", arguments).gsub(/%7B%7B(.+)%7D%7D/) do
"{{#{$1}}}"
end
end
handlebar_path :rant_vote, :rant_id => '{{id}}', :vote => 'up'
I ended up just overriding url_for to handle a custom param:
module TemplateHelper
def url_for(*args)
options = args.extract_options!
return super unless options.present?
handle = options.delete(:handlebars)
url = super(*(args.push(options)))
handle ? url.gsub(/%7B%7B(.+)%7D%7D/){|m| "{{#{$1}}}"} : url
end
end
So now, calling named_url(:id => '{{id}}', :handlebars => true) works as you'd expect.
Related
I'm a front end guy getting more and more into using sinatra. I'm building an app currently and am trying to find an elegant way to DRY up the routes in my myapp.rb file.
Currently I have these routes:
get '/' do
haml :content
end
get '/content' do
haml :content, :layout => :empty
end
get '/show_layout' do
haml :show_layout
end
get '/conversion' do
haml :conversion, :layout => :empty
end
get '/optout' do
haml :optout, :layout => false
end
get '/terms' do
haml :terms, :layout => :empty
end
With regards to the templates, I know I can do something like this to combine them:
get '/:name' do
haml params[:name].to_sym
end
But what about the layouts? There are only 2 layouts here, layout.haml and empty.haml (:layout, and :empty), 3 if you count :layout => false
Is this something that is best done using a hash? Something like:
layout_map = {
"" => "",
:content => "",
:show_layout => "",
:conversion => :empty,
:optout => false,
:terms => :empty
}
get '/:name' do
haml params[:name].to_sym, :layout => layout_map[:name]
end
Seems like its on the right track but I can't get it to work properly.
Thanks for all your help.
You can use your:
get '/:name' do
haml params[:name].to_sym
end
plus a before route that will set your layout:
before '/:name' do
layout_map = {
:content => "",
:show_layout => "",
:conversion => :empty,
:optout => false,
:terms => :empty
}
set :layout => layout_map[params[:name]]
end
This will set your layout according to params[:name] with every call. But be careful with .simming every route. If someone calls many 404s you create lots and lots of dead symbols which are not garbage collected and eventually will crash your app. Better do this:
get '/:name' do
halt 404 unless File.exist?("views/#{params[:name]}.haml")
time = File.stat("views/#{params[:name]}.haml").ctime
last_modified(time)
haml params[:name].intern
end
This will only create a symbol for params[:name] if there is a file with that name. So you're on the safe side because the symbol already exists.
Thanks for all your help everyone. Ended up going with this as my solution due to some other requirements of the app.
get "/:base_route/" do
haml :"#{params[:base_route]}/content", :layout => :"#{params[:base_route]}/layout"
end
get "/:base_route/:name" do
layout_map = {
:landing => :layout,
:content => :empty,
:show_layout => :layout,
:conversion => :empty,
:terms => :empty
}
haml :"#{params[:base_route]}/#{params[:name]}", :layout => :"#{params[:base_route]}/#{layout_map[params[:name].to_sym]}"
end
I'm converting an application from rails 2 to rails 3 and could somebody please help me regarding this small bit of code. The link_to is not working , could some one point me how to use link_to instead of the link_to_remote in rails 3.1 properly?
Rails 2 code
<%= link_to_remote package_item.getId(),
:url => { :controller => 'cmn/popup',
:action => "show_popup",
:frame_url => admin_url(
:ctrl => controller,
:app_action => 'package.item.edit',
:id => package_item.getId().to_s,
:remote => true
),
:frame_width => '570px',
:frame_height => '355px'
}
%>
Rails 3.1 code
<%= link_to package_item.getId(),
:url => { :controller => 'cmn/popup',
:action => "show_popup",
:frame_url => admin_url(
:ctrl => controller,
:app_action => 'package.item.edit',
:id => package_item.getId().to_s
),
:frame_width => '570px',
:frame_height => '355px',
:remote => true
}
%>
I replace all .rjs file with .js.erb. This is the URL I'm getting in Rails 3:
3
This is in Rails 2:
2
my controller
def show_popup
#content_data = {}
#content_data.merge!(params)
render(:template => 'cmn/popup/show_popup', :nolayout => 1)
end
Please look at the syntax of link_to in Rails 3:
http://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to
You have all your parameters in a :url hash, but you don't need to name it :url, just pass in the options in a hash, like this:
<%= link_to package_item.getId(),
{
:controller => 'cmn/popup',
:action => "show_popup",
:frame_url => admin_url(
:ctrl => controller,
:app_action => 'package.item.edit',
:id => package_item.getId().to_s
),
:frame_width => '570px',
:frame_height => '355px'
},
:remote => true
%>
Remember to get :remote out of the url hash.
Let me know if this works.
It is the first time I see a string defining a path as a :controller param.
I mean de :controller => 'cmn/popup'. This is new to me, and it feels strange.
Are you sure this works and the request is being received by the correct controller and action?
Another thing i think it can be tricky is the render call on the controller. Just call
render :layout => false
or maybe dont call anything at all.
If the template has the same name than the action and it is placed in a directory named like the controller, rails knows what template needs to be rendered and the extension (js/html/xml) based on the request type. Maybe render :template => .... forces to render an html template.
The :nolayout option i think it is invalid. Anyway, if the request is for a javascript file, it never renders layout.
I have this in my controller:
#statics = [{'home' => 'about'},
{'home' => 'termsandconditions'},
{'home' => 'information'},
{'news' => 'archives'}]
and in my view:
#statics.each do |controller, action|
xml.loc url_for(:only_path => false, :controller => controller, :action => action)
xml.lastmod w3c_date(Time.now)
xml.changefreq "weekly"
xml.priority 0.8
end
the URLs aren't what I would expect, e.g.
http://localhost:3000/homeinformation
instead of
http://localhost:3000/information
However, I just did this manually, I get the right URL:
url_for(:only_path => false, :controller => 'brownies', :action => 'index') #works!
What am I missing here?
#statics is an array with one hash entry, so you'll need your each block to take just a single parameter and split it up inside the block.
I'd like my website to have URLs looking like this:
example.com/2010/02/my-first-post
I have my Post model with slug field ('my-first-post') and published_on field (from which we will deduct the year and month parts in the url).
I want my Post model to be RESTful, so things like url_for(#post) work like they should, ie: it should generate the aforementioned url.
Is there a way to do this? I know you need to override to_param and have map.resources :posts with :requirements option set, but I cannot get it all to work.
I have it almost done, I'm 90% there. Using resource_hacks plugin I can achieve this:
map.resources :posts, :member_path => '/:year/:month/:slug',
:member_path_requirements => {:year => /[\d]{4}/, :month => /[\d]{2}/, :slug => /[a-z0-9\-]+/}
rake routes
(...)
post GET /:year/:month/:slug(.:format) {:controller=>"posts", :action=>"show"}
and in the view:
<%= link_to 'post', post_path(:slug => #post.slug, :year => '2010', :month => '02') %>
generates proper example.com/2010/02/my-first-post link.
I would like this to work too:
<%= link_to 'post', post_path(#post) %>
But it needs overriding the to_param method in the model. Should be fairly easy, except for the fact, that to_param must return String, not Hash as I'd like it.
class Post < ActiveRecord::Base
def to_param
{:slug => 'my-first-post', :year => '2010', :month => '02'}
end
end
Results in can't convert Hash into String error.
This seems to be ignored:
def to_param
'2010/02/my-first-post'
end
as it results in error: post_url failed to generate from {:action=>"show", :year=>#<Post id: 1, title: (...) (it wrongly assigns #post object to the :year key). I'm kind of clueless at how to hack it.
Pretty URLs for Rails 3.x and Rails 2.x without the need for any external plugin, but with a little hack, unfortunately.
routes.rb
map.resources :posts, :except => [:show]
map.post '/:year/:month/:slug', :controller => :posts, :action => :show, :year => /\d{4}/, :month => /\d{2}/, :slug => /[a-z0-9\-]+/
application_controller.rb
def default_url_options(options = {})
# resource hack so that url_for(#post) works like it should
if options[:controller] == 'posts' && options[:action] == 'show'
options[:year] = #post.year
options[:month] = #post.month
end
options
end
post.rb
def to_param # optional
slug
end
def year
published_on.year
end
def month
published_on.strftime('%m')
end
view
<%= link_to 'post', #post %>
Note, for Rails 3.x you might want to use this route definition:
resources :posts
match '/:year/:month/:slug', :to => "posts#show", :as => :post, :year => /\d{4}/, :month => /\d{2}/, :slug => /[a-z0-9\-]+/
Is there any badge for answering your own question? ;)
Btw: the routing_test file is a good place to see what you can do with Rails routing.
Update: Using default_url_options is a dead end. The posted solution works only when there is #post variable defined in the controller. If there is, for example, #posts variable with Array of posts, we are out of luck (becase default_url_options doesn't have access to view variables, like p in #posts.each do |p|.
So this is still an open problem. Somebody help?
It's still a hack, but the following works:
In application_controller.rb:
def url_for(options = {})
if options[:year].class.to_s == 'Post'
post = options[:year]
options[:year] = post.year
options[:month] = post.month
options[:slug] = post.slug
end
super(options)
end
And the following will work (both in Rails 2.3.x and 3.0.0):
url_for(#post)
post_path(#post)
link_to #post.title, #post
etc.
This is the answer from some nice soul for a similar question of mine, url_for of a custom RESTful resource (composite key; not just id).
Ryan Bates talked about it in his screen cast "how to add custom routes, make some parameters optional, and add requirements for other parameters."
http://railscasts.com/episodes/70-custom-routes
This might be helpful. You can define a default_url_options method in your ApplicationController that receives a Hash of options that were passed to the url helper and returns a Hash of additional options that you want to use for those urls.
If a post is given as a parameter to post_path, it will be assigned to the first (unnassigned) parameter of the route. Haven't tested it, but it might work:
def default_url_options(options = {})
if options[:controller] == "posts" && options[:year].is_a?Post
post = options[:year]
{
:year => post.created_at.year,
:month => post.created_at.month,
:slug => post.slug
}
else
{}
end
end
I'm in the similar situation, where a post has a language parameter and slug parameter. Writing post_path(#post) sends this hash to the default_url_options method:
{:language=>#<Post id: 1, ...>, :controller=>"posts", :action=>"show"}
UPDATE: There's a problem that you can't override url parameters from that method. The parameters passed to the url helper take precedence. So you could do something like:
post_path(:slug => #post)
and:
def default_url_options(options = {})
if options[:controller] == "posts" && options[:slug].is_a?Post
{
:year => options[:slug].created_at.year,
:month => options[:slug].created_at.month
}
else
{}
end
end
This would work if Post.to_param returned the slug. You would only need to add the year and month to the hash.
You could just save yourself the stress and use friendly_id. Its awesome, does the job and you could look at a screencast by Ryan Bates to get started.
I'm trying to use the Rails Atom Feed Helper to generate a feed for a nested resource. My view template (index.atom.builder) is:
atom_feed(:schema_date => #favourites.first.created_at) do |feed|
feed.title("Favourites for #{#user.login}")
feed.updated(#favourites.first.created_at)
#favourites.each do |favourite|
feed.entry(favourite, :url => favourite.asset.external_ref) do |entry|
entry.title(favourite.asset.external_ref)
entry.content(image_tag(favourite.asset.location), :type => 'html')
entry.author do |author|
author.name(#user.login)
end
end
end
end
And I have the following routes:
map.namespace :public do |pub|
pub.resources :users, :has_many => [ :favourites ]
pub.resources :favourites
pub.resources :assets, :only => [ :show ]
end
Unfortunately the url is failing to generate for the feed.entry line:
feed.entry(favourite, :url => favourite.asset.external_ref) do |entry|
The error is "undefined method `favourite_url' for ActionView::Base".
I've tried changing the feed.entry line to:
feed.entry([:public, favourite], :url => favourite.asset.external_ref) do |entry|
But this then returns the entry for an Array rather than a favourite! Someone had a similar problem here also.
I know that adding the line:
map.resource :favourites
to my routes.rb would 'fix' this problem but this resource is only available nested beneath the /public namespace.
Has anyone had this problem before?
Cheers
Arfon
Just to follow up. Based upon Michael's suggestion I'm passing the full url param and this seems to generate the correct url for the feed.entry line.
#favourites.each do |favourite|
feed.entry(favourite, :url => public_user_favourite_url(:id => favourite, :user_id => #user)) do |entry|
entry.title(favourite.asset.external_ref)
entry.content(image_tag(favourite.asset.location), :type => 'html')
entry.author do |author|
author.name(#user.zooniverse_user_id)
end
end
end
You are using favourite.asset.external_ref as the title of the entry, which leaves me to believe the URL for that entry should probably be defined as:
public_user_favourite_url(:id => favourite, :user_id => #user)
Which, if favorite.id = 9 and #user.id = 1, would generate:
http://localhost:3000/public/users/1/favourites/9
Is this what you are looking for?