Rails Active Record Polymorphic Nested Resource Navigation - ruby-on-rails

I've run into a bit of an issue and not sure how to get round it.
We have a number of polymorphic nested resources in our datamodel, eg:
Destination > Accommodation > Address
Destination > Attraction > Address
So it is possible to arrive at the Address controller from multiple parents. I need to be able to associate these correctly and also navigate back up the tree of parents.
Address is the same model in these cases, so my first solution for
this was to created nested resources in the routes file.
We then also started to use this nesting to provide a breadcrumb
navigation thing, so when our URLS get like this:
localhost:3000/destinations/1/accommodations/3/address/new
We can split it up and use it to navigate back down the path to any level.
I also, to make the controller generic, I use the nested resources to
work out what the parent resource for map is, so the controller looks
like this:
def new
#parent = find_parent_model
if !#parent.nil?
#destination = #parent.destinations.new
[...]
def find_parent
params.each do |name, value|
if name =~ /(.+)_id$/
return $1.classify.constantize.find(value)
end
end
nil
end
This works. But the problem is that we have 1800 lines of nested resources in the routes.rb file and now it takes the rails app about 5 minutes to start, and it sits
there using 500MB of ram. :S
Does anyone know of a less crazy way of doing this?

You might want to give up on using the nested resources syntax for the routing.
A single route like
get 'destinations/:destination_id/:parent_type/:parent_id/address/new' => 'address#new'
would match all of resources, and in AddressController#new you could have
#parent = params[:parent_type].constantize.find(params[:parent_id])
You might also want to check that #parent is of one of the expected types afterwards.

Related

Rails: help me avoid having to use such deeply nested routes

I've got really long nested routes like so:
# barracudas have many elephants
resources :barracudas do
# elephants have any barracudas, and have many gloworms too
resources :elephants do
# gloworms have any elephants, and have many chimpanzees too
resources :gloworm do
# chimpanzees have many gloworms
resources :chimpanzees do
end
end
end
end
As you can see there are a lot of has_many, :through relationships here, so it's hard to find a chimpanzees parent gloworm wihout a little help from the URL:
# chimpanzees_controller.rb
def show
#barracuda = Barracuda.find(params[:barracuda_id])
#elephant = #barracuda.elephants.find(params[:elephant_id])
#gloworm = #elephant.gloworms.find(params[:gloworm_id])
#chimpanzee = #gloworm.chimpanzees.find(params[:id])
end
I would like to hide the fact that my system has barracudas, I want to change the urls from:
http://lvh.me:3000/barracudas/1/elephants/32/gloworms/5/chimpanzees/3
to:
http://lvh.me:3000/elephants/32/gloworms/5/chimpanzees/3
Am I right in thinking that I just need to remove the nesting from the routes, and add a :barracuda_id to the session so that I can always find it in the controller? Something like this?
#barracuda = Barracuda.find(session[:barracuda_id])
What if I wanted to remove all mention of elephants and gloworms from the URLs too? I guess it would be the same idea right? Expiring session data will probably be the biggest job.
Can a chimpanzee belong to more than one gloworm (and so on up the chain)? If not, you can collapse your whole structure into unnested routes and use something like this in your Controller:
# chimpanzees_controller.rb
def show
#chimpanzee = Chimpanzee.find(params[:id])
#gloworm = #chimpanzee.gloworm
#elephant = #gloworm.elephant
#barracuda = #elephant.barracuda
# Maybe do some checking on the current user to make sure the right Barracuda is loaded?
end
As long as you have belongs_to associations all the way up, you should be golden.
If you've got has_and_belongs_to_many associations anywhere in there, then you have to have the IDs from both sides of the association. Your session approach seems reasonable, although it can fall down if your users ever open multiple tabs to browse separate Barracudas.
Another approach could be composite IDs in your routes. Something like:
get "/chimpanzees/:barracuda_id-:elephant_id-:gloworm_id-:id" => "chimpanzee#show"
The URLs will look a little non-standard, but it disguises the objects you're dealing with.

Controller Best Practice

I have a Cards Controller where i need to set up categories. Because the views for this Controller would get pretty heavy to oversee i divided everything in folders.
routes.rb
resources :cards do
collection do
get 'druid'
get 'hunter'
get 'mage'
get 'paladin'
get 'priest'
get 'rogue'
get 'shaman'
get 'warlock'
get 'warrior'
get 'free'
get 'common'
get 'rare'
get 'epic'
get 'legendary'
get 'spell'
get 'minion'
get 'weapon'
get 'beast'
get 'deamon'
get 'dragon'
get 'murloc'
get 'pirate'
get 'totem'
end
end
View Folders:
Views ->
cards ->
class ->
druid.html.erb
hunter.html.erb
mage.html.erb
paladin.html.erb
priest.html.erb
rogue.html.erb
shaman.html.erb
warlock.html.erb
warrior.html.erb
rarity ->
free.html.erb
common.html.erb
rare.html.erb
epic.html.erb
legendary.html.erb
type ->
spell.html.erb
minion.html.erb
weapon.html.erb
race ->
beast.html.erb
deamon.html.erb
dragon.html.erb
murloc.html.erb
priate.html.erb
totem.html.erb
Now i don't think this is such a good Idea, but as for now i don't know any better way of doing it..
My messy controller will look like this:
def druid
render 'cards/class/druid'
end
def hunter
render 'cards/class/hunter'
end
def mage
render 'cards/class/mage'
end
def paladin
render 'cards/class/paladin'
end
etc...
Now... This list will get pretty long...
Is there a better way of dealing with this ???
A remark first: in your example (which I suppose is a simplified version of your application), your controller is just firing up the view. If this is correct, the pages could as well be totally static (pure HTML) and served statically.
Now, I think you should have more resources there: class, rarity, type and race could be resources by themselves, with the different "values" being the pages. After all (for what I can infer with my RPG knowledge), those are the elements of your domain model, so they should work great as resources.
The folders are already like that, so this could give something like: (warning, pseudo code out of my head)
resources :classes do # ClassesController
collection do
get 'druid'
get 'mage'
end
end
resources :rarity do # RarityController
collection do
get 'rare'
get 'common'
end
Finally, never forget that controllers & the routing file are just ruby code. You can make loops there:
cards_list = ['rogue', 'druid', ...]
resources :cards do
collection do
cards_list.each do |card_name|
get card_name
end
end
end
end
This would work for the version by resource above too.
Some metaprogramming could achieve the same on your controller (if you have nothing different between the various action methods).
UPDATE: 04/24/2014
Based on your absolute need to maintain separate views I would define the routes like this:
resources :cards do
collection do
get :cards, path: '/cards/:arg1'
end
end
OR
resources :cards do
collection do
get :cards_by_class, path: '/cards/class/:class'
get :cards_by_race, path: '/cards/race/:race'
#etc...
end
end
In your controller:
def index
template, #cards = get_cards
# you need to determine the path to the views based on params[:arg1]
render template
end
private
# These constants could be defined in the Card model itself
CLASS_TYPES = %w(druid hunter mage paladin) # etc.
RARITIES = %w(free common rare epic legendary)
WEAPON_TYPES = %w(spell minion weapon)
RACE_TYPES = %w(beast demon dragon) # etc.
def get_cards
path = ""
cards = nil
case params[:arg1]
when CLASS_TYPES
# Substitute 'classtype' with the proper column name
cards = Card.where("classtype = '#{params[:arg1]}'")
path = "/cards/class/#{params[:arg1]}"
when RARITIES
# Substitute 'rarity' with the proper column name
cards = Card.where("rarity = '#{params[:arg1]}'")
path = "/cards/rarity/#{params[:arg1]}"
when WEAPON_TYPES
# Substitute 'weapon_type' with the proper column name
cards = Card.where("weapon_type = '#{params[:arg1]}'")
path = "/cards/type/#{params[:arg1]}"
when RACE_TYPES
... # you get the idea
end
return path, cards
end
To be more accurate, I would need to see your schema for the Card model.
Although Martin's solution would surely work just fine, I would probably go in this direction, and avoid hard-coding the different types, classes, etc:
# This sets up routes for :new, :create, :show, :update, :delete, :edit, :index
resources :cards
If you want to get a list of 'druid' cards in your spec/test:
get :index, {kind: 'druid'}
My controller method would look similar to this:
def index
#cards = Card.where("kind = #{params[:kind]}")
render "cards/class/#{params[:kind]}"
end
You could even make the view a single ERB or HAML template that may be generic enough to handle the different types of attributes. In that case you would get rid of, or change, the 'render' line above and let it default to 'index' or 'cards/class/index.html.erb' or whatever.
This is just off the top of my head, but I would tend to avoid concrete-coding the attributes, and instead treat them like properties of a 'card'. After all, if the 'card' is the resource, then all of the races, rarity, class, etc. are just attributes of a card.
Hopefully this all makes sense. Let me know if I can clarify further. The 'kind' attribute maybe something different in your DB schema model of the 'Card'.

Add new view to a controller in Rails

I have a controller, clients_controller, with corresponding index, show, edit, delete, new & form views. Is there a way to create a new view like clients/prospects.html.erb that acts the same way as clients/index.html.erb, except is routed at clients/prospects/?
I've tried this:
match '/clients/prospects' => 'clients#prospects'
And some other things in routes.rb, but of course get the error "Couldn't find Client with id=prospects".
The goal here is basically to have a prospects view and a clients view, and by simply switching the hidden field to a 1, it (in the user's mind) turns a prospect into a client (it's a CRM-like app).
There's a couple of things you need to do. First you need to put the your custom route before any generic route. Otherwise Rails assumes the word "prospects" is an id for the show action. Example:
get '/clients/prospects' => 'clients#prospects' # or match for older Rails versions
resources :clients
Also you need to copy / paste the index method in your ClientsController and name it prospects. Example:
class ClientsController < ApplicationController
def index
#clients = Client.where(prospect: false)
end
def prospects
#prospects = Client.where(prospect: true)
end
end
Lastly, you need to copy the index.html.erb view and name the copy prospects.html.erb. In the example above you would have to work with the #prospects instance variable.
Create a new action in clients controller named prospects. And then define a collection route in routes.rb for it as either resource full way. Or u directly use match as you were doing.
What you're doing is not wrong (although I'd change match to get, otherwise POST and DELETE requests to that url will also render your prospects view). Presumably you have
resources :clients
in your routes file? If so, what you have will probably work if you just move the line you quoted above the resources declaration -- the problem is that /clients/prospects matches the show route for the clients resource, so if it's defined first then that's the route that gets matched.
However, there's a more idiomatic way to define this route
resources :clients do
collection do
get :prospects
end
end
See Rails Routing documentation for more
Also see migu's answer for what else needs to be done once the url is being routed correctly (though there are other things you can do -- if you the two views are similar enough, you can reuse the view template, for example).

Organizing site navigation actions in Rails

I'm new to Rails (I've worked in MVC but not that much) and I'm trying to do things the "right" way but I'm a little confused here.
I have a site navigation with filters Items by different criteria, meaning:
Items.popular
Items.recommended
User.items
Brand.items # by the parent brand
Category.items # by a category
The problem is that I don't know how to deal with this in the controller, where each action does a similar logic for each collection of items (for example, store in session and respond to js)
Either I have an action in ItemsController for every filter (big controller) or I put it in ItemsController BrandsController, CategoriesController (repeated logic), but neither provides a "clean" controller.
But I don't know witch one is better or if I should do something else.
Thanks in advance!
You're asking two separate questions. Items.popular and Items.recommended are best achieved in your Item model as a named scope This abstracts what Xavier recommended into the model. Then in your ItemsController, you'd have something like
def popular
#items = Item.popular
end
def recommended
#items = Item.recommended
end
This isn't functionally different than what Xavier recommended, but to me, it is more understandable. (I always try to write my code for the version of me that will come to it in six months to not wonder what the guy clacking on the keyboard was thinking.)
The second thing you're asking is about nested resources. Assuming your code reads something like:
class User
has_many :items
end
then you can route through a user to that user's items by including
resources :users do
resources :items
end
in your routes.rb file. Repeat for the other nested resources.
The last thing you said is
The problem is that I don't know how to deal with this in the controller, where each action does a similar logic for each collection of items (for example, store in session and respond to js)
If what I've said above doesn't solve this for you (I think it would unless there's a piece you've left out.) this sounds like a case for subclassing. Put the common code in the superclass, do the specific stuff in the subclass and call super.
There's a pretty convenient way to handle this, actually - you just have to be careful and sanitize things, as it involves getting input from the browser pretty close to your database. Basically, in ItemsController, you have a function that looks a lot like this:
def search
#items = Item.where(params[:item_criteria])
end
Scary, no? But effective! For security, I recommend something like:
def search
searchable_attrs = [...] #Possibly load this straight from the model
conditions = params[:item_criteria].keep_if do |k, v|
searchable_attrs.contains? k
end
conditions[:must_be_false] = false
#items = Item.where(conditions)
end
Those first four lines used to be doable with ActiveSupport's Hash#slice method, but that's been deprecated. I assume there's a new version somewhere, since it's so useful, but I'm not sure what it is.
Hope that helps!
I think both answers(#Xaviers and #jxpx777's) is good but should be used in different situations. If your view is exactly the same for popular and recommended items then i think you should use the same action for them both. Especially if this is only a way to filter your index page, and you want a way to filter for both recommended and popular items at the same time. Or maybe popular items belonging to a specific users? However if the views are different then you should use different actions too.
The same applies to the nested resource (user's, brand's and category's items). So a complete index action could look something like this:
# Items controller
before_filter :parent_resource
def index
if #parent
#items = #parent.items
else
#items = Item.scoped
end
if params[:item_criteria]
#items = #items.where(params[:item_criteria])
end
end
private
def parent_resource
#parent = if params[:user_id]
User.find(params[:user_id])
elsif params[:brand_id]
Brand.find(params[:brand_id])
elsif params[:category_id]
Category.find(params[:category_id])
end
end

Trying to make urls of the form <base_url>/boards/<name> in ruby on rails and getting errors

I am using ruby on rails to make a simple social networking site that includes different message boards for each committee of a student group. I want the url structure for each board to look like https://<base_url>/boards/<committee_name> and this will bring the user to the message board for that committee.
My routes.rb file looks like:
resources :committees, only: [:index]
match '/boards/:name', to: 'committees#index(name)'
My index function of committees_controller.rb file looks like:
def index(name)
#posts = Committee.where(name: name)
end
And then I'll use the #posts variable on the page to display all of the posts, but right now when I navigate to https://<base_url>/boards/<committee_name> I get an Unknown Action error, and it says The action 'index(name)' could not be found for CommitteesController.
Could someone guide me through what I have done wrong?
Once I get this working, how would I make a view that reflects this url structure?
Set up your routes like this:
resources :committees, only: [:index]
match '/boards/:name', to: 'committees#show'
and the controller like this:
def index
#committees = Committee.all
end
def show
#committee = Committee.find_by_name!(params[:name])
end
You can't really pass arguments to controller actions the way you were trying to with index(name). Instead, you use the params hash that Rails provides you. The :name part of the route declaration tells Rails to put whatever matches there into params[:name].
You also should be using separate actions for the listing of committees and displaying single committees. Going by Rails conventions, these should be the index and show actions, respectively.
When routing, you only specify the method name, not the arguments:
match '/boards/:name', to: 'committees#show'
Generally you will declare something with resources or match but not both. To stay REST-ful, this should be the show method. Index is a collection method, usually not taking any sort of record identifier.
Arguments always come in via the params structure:
def show
#posts = Committee.where(name: params[:name])
end
Controller methods that are exposed via routes do not take arguments. You may construct private methods that do take arguments for other purposes.

Resources