switching rails controller - ruby-on-rails

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.

Related

Rails Routing: How to have controllers in series to respond to same matching path

In my Rails routes.rb file I'm wanting to do something like the following.
get '/:id' => 'pages#show'
get '/:id' => 'articles#show'
So that if a visitor types in
http://www.example.com/about-this-site
The pages controller in the above example would get first shot at handling it. Then if not, the next controller in line would get a shot.
REASONs for wanting to do this:
1) I'm trying to port my Wordpress site over without establishing new urls for all my pages and blog posts. As it stands, all of my blog post files and pages are accessed directly off the root uri '/' folder.
2) Because I'm not able to, it's a learning thing for me. But, I want to do it without a hack.
How about redirecting to the second controller from your first controller?
in PagesController
def show
unless Page.find_by(id: params[:id])
redirect_to controller: :articles, action: :show, id: params[:id]
end
end
in ArticlesController
def show
# Handle whatever logic here...
end
Edit
If you really don't want to redirect then you can consolidate the logic into a single action:
def show
if Page.find_by(id: params[:id])
render :show
elsif Article.find_by(id: params[:id])
render controller: :articles, action: :show
else
# Handle missing case, perhaps a 404?
end
end
However, I'd recommend using a redirect if possible. It's a cleaner solution and keeps your controller code isolated.

(Rails) How to get 'id' out of edit url

I have a model called studies.
After action redirect redirect_to edit_study_path(#new_study),
URL: http://localhost:3000/studies/2/edit.
Is there anyway to customize an url after passing id ?
For example, http://localhost:3000/study
(still going to the edit path, and still with the :id in the params)
I guess what you want is to edit the current study?
In this case, it's possible, using ressource instead of ressources in the routes.
Let's have an example:
#in routes.rb
resources :studies
resource :study
Both of them will by default link to the StudiesController and call the same actions (eg. edit in your case) but in two different routes
get "/studies/:id/edit" => "studies#edit"
get "/study/edit" => "studies#edit"
in your edit action, you should then setup to handle correctly the parameters:
def edit
#study = params[:id].nil? ? current_study : Study.find(params[:id])
end
Note you need a current_study method somewhere, and store the current_study in cookies/sessions to make it works.
Example:
# In application_controller.rb
def current_study
#current_study ||= Study.find_by(id: session[:current_study_id]) #using find_by doesn't raise exception if doesn't exists
end
def current_study= x
#current_study = x
session[:current_study_id] = x.id
end
#... And back to study controller
def create
#...
#Eg. setup current_study and go to edit after creation
if study.save
self.current_study = study
redirect_to study_edit_path #easy peesy
end
end
Happy coding,
Yacine.

Accessing specific pages in a controller/view on Rails App

I am trying to set individual Meta Descriptions and Titles to individual pages in a Ruby on Rails App. It was a previous developers App, that I have been given the task to edit. Also, I am new to Rails and Ruby.
The app has a controllers/pages_controller.rb where I was am able to set unique variables for #descriptionX and #title on some pages (mission and disclaimer), but not for others, such as pet_planning.
class PagesController < ApplicationController
def index
#title = params[:page].humanize
render params[:page]
end
def pet_planning
#descriptionX = 'pet planning'
#title = 'pet planning title'
render :pet_planning
end
def mission
#title = 'Our Mission Statement'
#descriptionX = 'Mission Description'
render :mission
end
def disclaimer
#title = 'Our Disclaimer'
render :disclaimer
end
end
I think that the render params[:page] is where I am getting lost. I'm not 100% sure of what this is doing, or how to use it.
I don't understand why I would be able to control the #title and #description of mission but not pet_planning when their views are both in the same /views/pages/ directory. And I can't seem to find any distinction between the two anywhere else in the app.
Also, tried to add = #title = 'Pet Planning' in the /views/pages/pet_planning.html.haml file. It does change the title, however it also displays at the top of the page content unexpectedly.
Any help would be appreciate. Thanks.
I'd recommend having a read of the ActionController guide, which explains how Rails turns a request from the user into a page to render.
Basically, when you send a request, for example
GET http://www.example.com/pages/?page=pet_planning
then Rails works out what to do with it using the router (the routing guide explains this in more detail). I would imagine that your app is set up so that the /pages route matches to the PagesController#index action. You can have a look in your config/routes.rb file and/or type rake routes at the terminal to see how your routes are set up.
The bit after the question mark in the request is the "query string", and Rails turns this into a params hash which, for my example above, would look like {:page => "pet_planning"}. Your index action looks at that to get the name of the page to render - that's what render params[:page] is doing.
I guess that the reason you can modify the variables for some of your pages and not others is that some of them have their own routes - /pages/mission uses the PagesController#mission action, for example - while certain pages are accessed via the index action using the page param - /pages/?page=pet_planning or possibly /pages/index.html?page=pet_planning.
Update: Your existing route
match 'practice_areas/:page' => 'pages#index', :as => :pages
could be broken up into
match 'practice_areas/pet_planning' => 'pages#pet_planning' :as => :pet_planning
# etc ...
which would correspond to a controller that looks like this
class PagesController < ApplicationController
def pet_planning
#title = "Pet planning!"
#description = "Whatever..."
end
end
Your suggestion is close, but because the route format is "controller_name#action_name", you would require multiple controllers that looked like this
class PetPlanningController < ApplicationController
def index
#title = "Pet planning!"
#description = "..."
end
end
and you would have to move your views from app/views/pages/pet_planning.html.haml to app/views/pet_planning/index.html.haml. So it's probably not quite what you want.
Note that there might be a better way to tackle the problem than splitting everything up into separate actions, if all you are doing differently in each one is customising the title and description. For example, you could use a hash that maps your page name to its corresponding information:
class PagesController < ApplicationController
PAGE_INFO = {
"pet_planning" => ["Pet planning!", "Whatever..."],
"other_page" => ["Title", "Description"],
# ...
}
def index
page_name = params[:page]
#title, #description = PAGE_INFO[page_name]
render page_name
end
end
The render calls in pet_planning, mission, and disclaimer do the same as default behavior, so those calls can be removed. They are telling rails to use the pages with the given file names. For the index method, this is rendering a page based on a parameter.
The title and description are likely set in the layout. Look in /views/layouts/application.html.haml or /views/layouts/pages.html.haml.

How to get url of object in controller

I've got a Product model.
How do I get the url of a product in Rails 3, in a controller.
For example (pseudo code):
def foobar
#product = Product.first
puts #product.url
end
Is such a thing possible?
Assuming the Product model is mapped as :resources in your routes.rb file:
def foobar
#product = Product.first
url = product_url(#product)
end
In addition to named route product_url(#product) you can use the general url_for(#product) (docs). This has a side effect that if you have nested or namespaced routes, it is shorter:
customer_product_url(#customer, #product)
url_for([#customer, #product])
Also note, that by default url_for produces relative URLs, just as product_path would, which depending on your needs might be good or bad. To get full URL, pass :only_path => false.

Ruby on Rails Scaffold - Modify Show Method

I am a beginner when it comes to Ruby on Rails, so I need a little bit of help. I started reading a basic tutorial recently, which was taught using Scaffolding. I made a "Clients" model: script/generate scaffold clients name:string ip_address:string speed:integer ... Inside the clients_controller.rb file, there is a method called show:
# GET /clients/1
# GET /clients/1.xml
def show
#client = Client.find(params[:id])
respond_to do |format|
format.html # show.html.erb
format.xml { render :xml => #client }
end
end
For queries, I'd go to localhost:3000/clients/{Enter the ID here}. Instead of searching with the ID are the argument, I'd like to search with another value, like ip_address or speed, so I thought all I would have to do is change :id to :ip_address in "#client = Client.find(params[:id])". However, that does not work, so would someone please tell me how I can achieve a search with another parameter. Thanks!
This doesn't work because of the way things are routed
When you do something like
map.resources :client (See config/routes.rb)
This happens automatically when you use scaffold.
It sets up routes based on the assumption you're using an id.
One of these routes is something like
map.connect 'clients/:id', :controller => 'client', :action => 'show'
So :id is passed as a parameter as part of the URL.
You shouldn't have the IP be the primary identifier unless they're distinct - and even then it kind of messes with the RESTful routing.
If you want to have the ability to search by IP, modify your index action for the clients
def index
if params[:ip].present?
#clients = Client.find_by_ip_address(params[:ip]);
else
#clients = Client.all
end
end
Then you can search by ip by going to clients?ip=###.###.###
This line in your routes.rb file
map.connect 'clients/:id', :controller => 'client', :action => 'show'
implies that when the dispatcher receives an URI in the format "clients/abcdxyz' with GET Method, it will redirect it to show method with the value "abcdxyz" available in params hash with key :id.
EDIT
Since you have used scaffold, the clients resource will be RESTful. That means that when you send a GET request to "/clients/:id' URI, you will be redirected to show page of that particular client.
In your controller code you can access it as
params[:id] # which will be "abcdxyz"
The find method that is generated by scaffold searches on the primary key i.e 'id' column. You need to change that statement to either
#client = Client.find_by_ip_address(params[:id]) #find_by_column_name
OR
#client = Client.find(:first, :conditions => [":ip_address = ?", params[:id]])
:-)

Resources