Rails - Multiple Models with root sluggable routes - ruby-on-rails

I'm looking for advice to find the ideal solution that fits within Rails' best practises.
My app has 5 models:
Category
Place
Division
Subdivision
Item (polymorphic)
A Place belongs to a Category. A Division belongs to a Place. A Subdivision belongs to a Division, and an Item can belong to a Place, Division OR Subdivision.
Here is my routes.rb
resources :categories, path: ''
get ':category/:place', to: 'places#show', as: :place
get ':category/:place/:item', to: 'items#show', as: :place_item
get ':category/:place/:division', to: 'divisions#show', as: :division
get ':category/:place/:division/:item', to: 'items#show', as: :division_item
get ':category/:place/:division/:subdivision', to: 'subdivisions#show', as: :subdivision
get ':category/:place/:division/:subdivision/:item', to: 'items#show', as: :subdivision_item
The issue is that if I type in the URL:
/category/place/divison
The error is "could not find ITEM".
I am currently using friendly_id for the slugs, is there a best method for introducing a new model "Slug" that would handle the slugs for all 5 of these models?
Potentially something like this? (Haven't tested)
get '/*category_id', :controller => 'slugs'
Here's item_controller:
def show
end
def show_place_item
render "show"
end
def show_division_item
render "show"
end
def show_subdivision_item
render "show"
end
private
def set_item
#item = Item.friendly.find(params[:id])
end
def item_params
params.require(:item).permit(:name, :slug, :category_id)
end

Related

What is the best way to allow several methods on the controller without adding on routes?

Hi everybody I'm from the old school using rails 2.
Actually I'm using rails 4 and I'm trying to find a way to create methods on the controller without writting
On RAILS 2 used: (only needed to write the name on the controller)
#controller
def report_a
end
def report_b
end
def report_c
end
...and whatever def
#ROUTES
map.connect ':controller/:action/:id'
map.connect ':controller/:action/:id.:format'
On RAILS 4
#controller
def report_a
end
def report_b
end
def report_c
end
#ROUTES
match ':controller(/:action(/:id(.:format)))', :via => [:get, :post]
The problem is when I create a view report like this: (views/reports/report_a.html.erb)
<%= form_tag :action=>"report_a" do %>
<% end %>
I get this message:
No route matches [GET] "/reports/report_a"
To resolve this issue and doing Rails instruccions works like this:
#controller
def report_a
#users= User.search(params[:name])
end
def result_report_a
#users= User.search(params[:name])
end
#view/reports/report_a.html.erb
<%= form_tag :action=>"result_report_a" do %>
<% end %>
#routes.rb
get "reports#report_a"
post "reports#result_report_a"
get "reports#report_b"
post "reports#result_report_b"
get "reports#report_c"
post "reports#result_report_c"
Also I found this better way:
#controller reports.rb
def search_report_a
report_a
render :report_a
end
def report_a
#users = User.where(:name=>params[:name])
end
def search_report_b
report_b
render :report_b
end
def report_b
#users = User.where(:address=>params[:address])
end
...
#Routes.rb
resources :users do
match 'search_report_a', :via => [:post,:get], :on => :collection
match 'search_report_b', :via => [:post,:get], :on => :collection
...
end
Is there any other way to create methods without adding all inside ROUTES.RB ?
Any suggestions or the only way is adding get and post?
Imagine a case where you have several methods.
Best approach in Rails is to use REST architecture. Your controller should be able to view, create, update and destroy some resource (of course all actions are not mandatory).
For example:
def ReportsController
def index
# Actions to show links to all possible reports
end
def show
# Show report based on params
end
end
Your #show method may show any of report (report_a, report_b, etc) just by checking param from GET request.
And you don't need to make all logics inside #show method. It would be better to place report-related logic in, maybe, some service objects.

rails - how to make nested route with ancestry

I have a single model:
class Page < ActiveRecord::Base
has_ancestry
validates :slug, :name, uniqueness: true, presence: true
before_validation :generate_slug
def to_param
slug
end
def generate_slug
self.slug = Russian.translit(name).parameterize
end
end
and I'm using ancestry gem to create tree of pages and subpages, i.e. page can have multiple sub-pages and sub-pages can also have multiple sub-pages, and so on to infinity.
But my problem is that I can't make something is /page-1/page-1-2/page-1-2-1. All sub-pages have a URL is: /page-1-2 or /page-1-3-1.
My routes.rb:
Rails.application.routes.draw do
get '/pages' => 'pages#index'
resources :pages, path: "", path_names: { new: 'add' }
root 'pages#index'
end
How to make nested URL?
Thanks!
As far as I know there's no neat way of capturing nested tree structured routes with dynamic permalinks, you can create a named route to capture pretty nested pages path:
get '/p/*id', :to => 'pages#show', :as => :nested_pages
Also, make sure you update slug of your page object to have nested urls, i.e.: append parent pages' slug to it. For example:
page1.slug = '/page-1'
page2.slug = '/page-1/page-2' # page2 is a child of page1
page3.slug = '/page-1/page-2/page-3' # page3 is a child of page2
So, to make this work, you can probably change generate_slug method in your Page model class:
def generate_slug
name_as_slug = Russian.translit(name).parameterize
if parent.present?
self.slug = [parent.slug, (slug.blank? ? name_as_slug : slug.split('/').last)].join('/')
else
self.slug = name_as_slug if slug.blank?
end
end

Publication status, Rails

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.

Rails 4 - routing actions for contact form

I have two actions in the controller:
def report
#user = User.find_by_slug(params[:slug])
end
def reportForm
#user = User.find_by_slug(params[:slug])
Thread.new do
mail = ...
end
#message = 'Thanks!'
end
and in routes:
# User report form
get "/user/:slug/report", to: "users#report"
# Catch report form and action
post "/user/:slug/report", to: 'users#reportForm'
And the view:
<form method="POST" action="/user/<%= #user.slug %>/reportForm">
...
But the problem is, that when I send the form, the action reportForm is not called and instead of that is only refresh the current page with the form.
What's wrong here?
Thank you guys.
Form Helpers
The first thing that's wrong is you're not using the form helpers that Rails provides - this is a problem because you'll end up with niggly little problems like the one you're receiving:
#config/routes.rb
resources :users do
get :report #-> domain.com/users/:id/report
post :reportForm #-> domain.com/users/:id/reportForm
end
#view
<%= form_tag user_reportForm_path(#user) do %>
...
<% end %>
Routes
The second issue you have is to do with your routes
You've set the following routes:
get "/user/:slug/report", to: "users#report"
post "/user/:slug/report", to: 'users#reportForm'
This means you've got to send the request to domain.com/user/user_slug/report. Your form sends the URL to reportForm...
You should see my routes above for the solution to this problem
But more importantly, you should read up on nested resources:
#config/routes.rb
resources :users do
match :report, action: "reportForm", via: [:get, :post] #-> domain.com/users/:id/report
end
Slug
Finally, you're trying to use params[:slug] in your controller
With the resourceful routes you should be using in Rails, you'll be passing params[:id] most of the time. This should not be an issue (what is contained in params[:id] can be anything).
I would highly recommend looking at a gem called friendly_id, which makes including slugs in your application a lot simpler:
#app/models/user.rb
Class User < ActiveRecord::Base
extend FriendlyId
friendly_id :name, use: [:slugged, :finders]
end
This will allow you to call:
#app/controllers/users_controller.rb
Class UsersController < ApplicationController
def reportForm
User.find params[:id] #-> will use either `id` or `slug`
end
end

Custom parameters in URL for show action

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)

Resources