What I want to do seems simple, but might not be "proper"
let's say I have an image resource, and I manipulate the image based on the url. In the url I want to specify it's size and whether it's grayed, colored, or dimmed or some other condition.
currently I have a number of named routes that look like this.
map.gray_product_image "images/:product/:image/gray/:size.:format", :controller => 'images', :action => 'gray_product_image'
for me the trick is that if I created this useing Rails resources, I don't know how I would specify the :size, :format, or it's "color type".
I guess I would like to add a member route and specify my params like the following.
map.resources :products do |products|
products.resources :images, :member => {:gray_product_image => {':image/:size.:format' => :get}}
end
There are other times where I have wanted to added extra info to a resource route but didn't know how.
Any help would be greatly appreciated,
Thanks.
There's no good way to remove the controller/id part of a resource. The closest you're going to get through tricking ActionController with something like this:
map.resources :gray, :path_prefix => "/images/:product/:image_id/",
:controller => 'images', :requirements => {:colour => "gray"}
Which will produce routes like www.site.com/images/product/4/gray/1234.html with the following params hash:
params => {
:image_id => 4,
:id => 1234,
:colour => "gray",
:product => "product"
}
The format won't be passed explicitly but it will be available in the controller through the usually respond_to means.
Next you'll have to work some magic in controller to trick rails into doing what you want.
class ImagesController < ApplicationController
def show
#size = params[:id]
#image = Image.find(params[:image_id])
...
end
end
This actually works better as a filter so:
class ImagesController < ApplicationController
def initialize_colour
unless params[:colour].nil?
#size = params[:id]
#colour = params[:colour]
#image = Image.find(params[:image_id])
end
end
before_filter :initialize_colour, :except => [:index, :new, :create]
...
end
However to make good use of these routes, you're going to have to pass all those extra parameters to your url for calls. Like this:
gray_url(size, :image_id => #image.id, :product => product)
But helpers make that easy.
module ApplicationHelper
def easy_gray_url(image, size, product)
gray_url(size, :image_id => image.id, :product => product)
end
end
Check out the documentation for Resources. You'll find this:
The resources method accepts the
following options to customize the
resulting routes:
:requirements - Set custom routing parameter requirements; this is a hash of either regular expressions (which must match for the route to match) or extra parameters. For example:
map.resource :profile,
:path_prefix => ':name',
:requirements => { :name => /[a-zA-Z]+/, :extra => 'value' }
will only match if the first part is alphabetic, and will pass the parameter :extra to the controller.
I have realized that the way I want to represent my resources simply falls outside of the normal Rails resources, and that's ok. The problem I was really having was that each time added anther action and named route to get to what I wanted it felt wrong, I was repeating myself, both in my routes and in my actions.
I went back to simply creating my named routes, and spent a little more time in the controller so that I could keep my routes simple. Below is what I have now, and I am ok with it.
#routes.rb
map.with_options :controller => 'sketched_images', :action => 'show', :path_prefix => '/sketches', :name_prefix => 'sketched_', :color => 'grey' do |m|
m.style "styles/:style/:color/:size.:format"
m.design "designs/:design/:color/:size.:format"
m.product "products/:product/:color/:size.:format"
m.color_combo "colored_products/:color_combo/:size.:format"
end
class SketchedImagesController < ApplicationController
caches_page :show
before_filter :load_data
def show
#size = params[:size] || 100
respond_to do |wants|
wants.png
wants.jpg
end
end
private
def load_data
case
when params[:design]
#image = ClothingDesign.from_param(params[:design]).sketched_image
greyed
when params[:style]
#image = ClothingStyle.from_param(params[:style]).sketched_image
greyed
when params[:product]
#image = Product.from_param(params[:product]).sketched_images.first
greyed
when params[:color_combo]
#color_combo = ColorCombo.find_by_id(params[:color_combo])
#object = #color_combo.colorable
if #object.active? && !#object.sketched_images.blank?
#image = #object.sketched_images.first
colored
else
#image = #product.style.sketched_image
dimmed
end
end
end
def greyed
#blank = "#FFF"
#print = "#000"
#highlight = "#666"
end
def colored
#blank = "##{#color_combo.blank_color.value}"
#print = "##{#color_combo.design_color.value}"
#highlight = "##{#color_combo.highlight_color.value}" unless #color_combo.highlight_color.blank?
end
def dimmed
#blank = "#BBB"
#print = "#000"
#highlight = "#444"
end
end
Related
I want to change the status of hotels in my site. When user create new hotel, he have status "pending". As an administrator, I can upgrade the hotel status from pending to approved or rejected. But I can not approved of in the rejected and vice versa.
I decided to do it with three buttons in admin panel in the place where showing all hotels but this code not working.
routes.rb
HotelAdvisor::Application.routes.draw do
devise_for :admins
devise_for :users
devise_scope :admin do
get '/admin', to: 'devise/sessions#new'
end
post '/rate' => 'rater#create', :as => 'rate'
root to: 'hotels#list'
resources :hotels do
resources :comments
get 'list', on: :collection
post 'comment'
end
resources :ratings, only: :update
namespace :admin do
resources :hotels, :users
end
base_controller
class Admin::BaseController < ApplicationController
before_filter :authenticate_admin!
layout 'admin'
end
hootels_controller(in admin folder)
class Admin::HotelsController < Admin::BaseController
def index
#hotels = Hotel.all
end
def new
#hotel = Hotel.new
end
def create
#hotel = Hotel.new(hotel_params)
#hotel.user_id = current_admin.id
if #hotel.save
render :index
else
render :new
end
end
def update
#hotel = Hotel.find(params[:id])
#hotel.update_attributes(params[:hotel])
end
end
index(in /admin/hotels)
- #hotels.each do |hotel|
.ui.segment
.ui.three.column.grid
.column
.ui.large.image
=image_tag hotel.avatar_url
=link_to hotel_path(hotel), class:'blue ui corner label' do
%i.fullscreen.icon
.column
.ui.message
.header
=hotel.title
.wraper=hotel.description.truncate(300)
.column
=simple_form_for Hotel.find([hotel.id]),:method => :put do |f|
=f.hidden_field :status, value: 'approved'
=f.button :submit, 'Approved', class: 'secondary button'
%br
%hr
I don't know why, but I see this error,
Missing template hotels/update, application/update with...
I think out that in updating rails do not use the controller in the folder admin. Perhaps this is causing the error
Given you didn't implement what to be done, e.g. render, redirect, etc. rails fallbacks to the default, which is to render views with the name of the action, in this case, update.
You might want to take some action, depending on the outcome of update_attributes, for instance:
if #hotel.update_attributes(params[:hotel])
redirect_to(#hotel)
else
render(:edit)
end
You might also want to take a look at Responders to DRY your actions.
I have URLs like this
arizona/AZ12
colorado/CO470
I added the AZ and CO because friendly id wanted unique ids. Arizona and Colorado could have a unit 12.
I'd like to have URLs like
arizona/unit12
colorado/unit470
Seems like you could write something that removes the first two characters and replaces them. Would that be in the routes or controller?
My routes
resources :states, :except => [:index ], :path => '/' do
resources :units, :except => [:index ], :path => '/'
end
My controller
def show
#units = Unit.all
#states = State.with_units.group('states.id')
#state = State.all
#unit = Unit.friendly.find(params[:id])
end
Implement to_param method on your model. Rails will call to_param to convert the object to a slug for the URL. If your model does not define this method then it will use the implementation in ActiveRecord::Base which just returns the id.
class SomeModel
def to_param
"unit#{id}"
end
end
You can refer https://gist.github.com/agnellvj/1209733 for example
I'm working on implementing a SEO-hiarchy, which means that I need to prepend parameters for a show action.
The use-case is a search site where the URL-structure is:
/cars/(:brand)/ => a list page
/cars/(:brand)/(:model_name)?s=query_params => a search action
/cars/:brand/:model_name/:variant/:id => a car show action
My problem is to make the show action URLs work without having to provide :brand, :model_name and :variant as individual arguments. They are always available from as values on the resource.
What I have:
/cars/19330-Audi-A4-3.0-TDI
What I want
/cars/Audi/A4/3.0-TDI/19330
Previously, this was how the routes.rb looked like:
# Before
resources :cars. only: [:show] do
member do
get 'favourize'
get 'unfavourize'
end
Following was my first attempt:
# First attempt
scope '/cars/:brand/:model_name/:variant' do
match ":id" => 'cars_controller#show'
match ":car_id/favourize" => 'cars_controller#favourize', as: :favourize_car
match ":car_id/unfavourize" => 'cars_controller#unfavourize', as: :unfavourize_car
end
This makes it possible to do:
cars_path(car, brand: car.brand, model_name: car.model_name, variant: car.variant)
But that is obviously not really ideal.
How is it possible to setup the routes (and perhaps the .to_param method?) in a way that doesn't make it a tedious task to change all link_to calls?
Thanks in advance!
-- UPDATE --
With #tharrisson's suggestion, this is what I tried:
# routes.rb
match '/:brand/:model_name/:variant/:id' => 'cars#show', as: :car
# car.rb
def to_param
# Replace all non-alphanumeric chars with - , then merge adjacent dashes into one
"#{brand}/#{model_name}/#{variant.downcase.gsub(/[^[:alnum:]]/,'-').gsub(/-{2,}/,'-')}/#{id}"
end
The route works fine, e.g. /cars/Audi/A4/3.0-TDI/19930 displays the correct page. Generating the link with to_param, however, doesn't work. Example:
link_to "car link", car_path(#car)
#=> ActionView::Template::Error (No route matches {:controller=>"cars", :action=>"show", :locale=>"da", :brand=>#<Car id: 487143, (...)>})
link_to "car link 2", car_path(#car, brand: "Audi")
#=> ActionView::Template::Error (No route matches {:controller=>"cars", :action=>"show", :locale=>"da", :brand=>"Audi", :model_name=>#<Car id: 487143, (...)>})
Rails doesn't seem to know how to translate the to_param into a valid link.
I do not see any way to do this with Rails without tweaking either the URL recognition or the URL generation.
With your first attempt, you got the URL recognition working but not the generation. The solution I can see to make the generation working would be to override the car_path helper method.
Another solution could be, like you did in the UPDATE, to override the to_param method of Car. Notice that your problem is not in the to_param method but in the route definition : you need to give :brand,:model_name and :variant parameters when you want to generate the route. To deal with that, you may want to use a Wildcard segment in your route.
Finally you can also use the routing-filter gem which make you able to add logic before and after the url recognition / generation.
For me, it looks like all theses solutions are a bit heavy and not as easy as it should be but I believe this came from your need as you want to add some levels in the URL without strictly following the rails behavior which will give you URL like /brands/audi/models/A3/variants/19930
OK, so here's what I've got. This works in my little test case. Obviously some fixups needed, and I am sure could be more concise and elegant, but my motto is: "make it work, make it pretty, make it fast" :-)
In routes.rb
controller :cars do
match 'cars', :to => "cars#index"
match 'cars/:brand', :to => "cars#list_brand", :as => :brand
match 'cars/:brand/:model', :to => "cars#list_model_name", :as => :model_name
match 'cars/:brand/:model/:variant', :to => "cars#list_variant", :as => :variant
end
In the Car model
def to_param
"#{brand}/#{model_name}/#{variant}"
end
And obviously fragile and non-DRY, in cars_controller.rb
def index
#cars = Car.all
respond_to do |format|
format.html # index.html.erb
format.json { render json: #cars }
end
end
def list_brand
#cars = Car.where("brand = ?", params[:brand])
respond_to do |format|
format.html { render :index }
end
end
def list_model_name
#cars = Car.where("brand = ? and model_name = ?", params[:brand], params[:model])
respond_to do |format|
format.html { render :index }
end
end
def list_variant
#cars = Car.where("brand = ? and model_name = ? and variant = ?", params[:brand], params[:model], params[:variant])
respond_to do |format|
format.html { render :index }
end
end
You just need to create two routes, one for recognition, one for generation.
Updated: use the routes in question.
# config/routes.rb
# this one is used for path generation
resources :cars, :only => [:index, :show] do
member do
get 'favourize'
get 'unfavourize'
end
end
# this one is used for path recognition
scope '/cars/:brand/:model_name/:variant' do
match ':id(/:action)' => 'cars#show', :via => :get
end
And customize to_param
# app/models/car.rb
require 'cgi'
class Car < ActiveRecord::Base
def to_param
parts = [brand,
model_name,
variant.downcase.gsub(/[^[:alnum:]]/,'-').gsub(/-{2,}/,'-'),
id]
parts.collect {|p| p.present? ? CGI.escape(p.to_s) : '-'}.join('/')
end
end
Sample of path helpers:
link_to 'Show', car_path(#car)
link_to 'Edit', edit_car_path(#car)
link_to 'Favourize', favourize_car_path(#car)
link_to 'Unfavourize', unfavourize_car_path(#car)
link_to 'Cars', cars_path
form_for(#car) # if resources :cars is not
# restricted to :index and :show
You want bounded parameters to be passed to url of which some parameters are optional and some of them strictly needs to be present.
Rails guides shows you can have strict as well as optional parameters and also you can give name to particular route in-order to simplify its usage.
Guide on rails routing
bound parameters
Example usage -
In below route,
brand is optional parameter as its surrounded by circular bracket
Also please note there can be optional parameters inside route but they needs to added at last /cars(/:brand)(/:make)(/:model)
match '/cars/(:brand)', :to => 'cars#index', :as => cars
here cars_url will map to index action of cars controller..
again cars_url("Totoya") will route index action of cars controller along-with params[:brand] as Toyota
Show url route can be as below where id is mandatory and others can be optional
match '/cars/:id(/:brand(/:model_name/)(/:variant)', :to => "cars#show", :as => car
In above case, id is mandatory field. Other parameters are optional.
so you can access it like car_url(car.id) or car_url(12, 'toyota') or car_url(12, 'toyota', 'fortuner') or car_url(12, 'toyota', 'fortuner', 'something else)
Orders can have many states. I would like to create named routes for those. I need the state to be passed in to the controller as a param. Here is what I was thinking, but it obviously does not work.
match "order/:state/:id" => "orders#%{state}", as: "%{state}"
So I would like order/address/17 to route to orders#address, with :state and :id being passed in as params. Likewise, order/shipping/17 would route to orders#shipping, again :state and :id would be passed in.
Here is the controller.
class OrdersController < ApplicationController
before_filter :load_order, only: [:address, :shipping, :confirmation, :receipt]
before_filter :validate_state, only: [:address, :shipping, :confirmation, :receipt]
def address
#order.build_billing_address unless #order.billing_address
#order.build_shipping_address unless #order.shipping_address
end
def shipping
#shipping_rates = #order.calculate_shipping_rates
end
def confirmation
end
def receipt
end
private
def load_order
#order = Order.find(params[:id])
end
# Check to see if the user is on the correct action
def validate_state
if params[:state]
unless params[:state] == #order.state
redirect_to eval("#{#order.state}_path(:#{#order.state},#{#order.id})")
return
end
end
end
end
Here is what we ended up going with:
routes.rb
%w(address shipping confirmation receipt).each do |state|
match "order/#{state}/:id", :to => "orders##{state}", :as => state, :state => state
end
orders_controller.rb
def validate_state
if params[:state]
unless params[:state] == #order.state
redirect_to(eval("#{#order.state}_path(#order)"))
return
end
end
end
You aren't going to be able to create dynamic named routes with that sort of syntax, but you're basically just using :state as the :action. If you replace :state with :action and specify the controller manually, it'll work. Obviously, you will have to change your code to look at params[:action] rather than params[:state] (or map that variable in a before_filter), but beyond that it should work fine.
match "order/:action/:id", :controller => "orders"
Be aware that if orders has RESTful resource mappings like create or delete, this route would allow GET requests to them, which would be bad; you may just want to add explicit routes for each action you want to complete. This will let you get params[:state], as well:
%w(address shipping).each do |state|
match "order/#{state}/:id", :to => "orders##{state}", :as => state, :state => state
end
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.