Rails: help me avoid having to use such deeply nested routes - ruby-on-rails

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.

Related

Ruby on Rails 7 Multistep form with multiple models logic

I am currently struggling with building up a multi step form where every step creates a model instance.
In this case I have 3 models:
UserPlan
Connection
GameDashboard
Since the association is like that:
An user has an user_plan
A connection belongs to an user_plan
A game_dashboard belongs to a connection
I would like to create a wizard to allow the current_user to create a game_dashboard going through a multi-step form where he is also creating connection and user_plan instance.
For this purpose I looked at Wicked gem and I started creating the logic from game_dashboard (which is the last). As soon as I had to face with form generating I felt like maybe starting from the bottom was not the better solution.
That’s why I am here to ask for help:
What would be the better way to implement this wizard? Starting from the bottom (game_dashboard) or starting
from the top (use_plan)?
Since I’m not asking help for code at the moment I didn’t write any controller’s or model’s logic, in case it would be helpful to someone I will put it!
Thanks a lot
EDIT
Since i need to allow only one process at a time but allowing multiple processes, to avoid the params values i decided to create a new model called like "onboarding" where i handle steps states there, checking each time the step
The simplest way would be to rely on the standard MVC pattern of Rails.
Just use the create and update controller methods to link to the next model's form (instead of to a show or index view)
E.g.
class UserPlansController < ApplicationController
...
def create
if #user_plan = UserPlan.create(user_plan_params)
# the next step in the form wizard process:
redirect_to new_connection_path(user_id: current_user, user_plan_id: #user_plan.reload.id)
else
#user_plan = UserPlan.new(user: current_user)
render :new
end
end
...
# something similar for #update action
end
For routes, you have two options:
You could nest everything:
# routes.rb
resources :user do
resources :user_plan do
resources :connection do
resources : game_dashboard
end
end
end
Pro:
This would make setting your associations in your controllers easier because all your routes would have what you need. E.g.:
/users/:user_id/user_plans/:user_plan_id/connections/:connection_id/game_dashboards/:game_dashboard_id
Con:
Your routes and link helpers would be very long and intense towards the "bottom". E.g.
game_dashboard_connection_user_plan_user_path(:user_id, :user_plan_id, :connection_id, :game_dashboard)
You could just manually link your wizard "steps" together
Pro:
The URLs and helpers aren't so crazy. E.g.
new_connection_path(user_plan_id: #user_plan.id)
With one meaningful URL variable: user_plan_id=1, you can look up everything upstream. e.g.:
#user_plan = UserPlan.find(params['user_plan_id'])
#user = #user_plan.user
Con:
(not much of a "con" because you probably wind up doing this anyway)
If you need to display information about "parent" records, you have to perform model lookups in your controllers first:
class GameDashboardController < ApplicationController
# e.g. URL: /game_dashboards/new?connection_id=1
def new
#connection = Connection.find(params['connection_id'])
#user_plan = #connection.user_plan
#user = #user_plan.user
#game_dashboard = GameDashboard.new(connection: #connection)
end
end

Map multiple paths to a controller

I have a controller called CardController. Currently I have routes like card_path that map to /cards/:id. I would like to make it so that I can use /trips/:id and /events/:id that map to the same /cards/:id. I know I'll have to override card_path eventually but is it possible to set up my routes file for this? Do I need to set up a Trip and Event controller that just redirect to the card actions?
Edit:
Trips should completely map to cards, meaning 'trips/1/edit' should end up at 'cards/1/edit', 'trips/1/images/12' should end up at 'cards/1/images/12'
I ended up adding some controller to the routes file.
routes.rb
def card_routes
member do
get 'test'
end
end
class TripsController < CardsController; end
resources :trips { card_routes }
resources :cards { card_routes }
Now /trips/1/test and /cards/1/test go to the same place.
You can easily do something like:
get 'trips/:id' => 'cards#show'
Try accessing different trips in your browser, trips/1 or trips/2 (if cards with those ids exist), and they should redirect to the appropriate card.
If you haven't already, I recommend taking a few minutes and reading the Routing Guide, as it's really comprehensive and shows different ways of accomplishing things:
http://guides.rubyonrails.org/routing.html

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

Rails Active Record Polymorphic Nested Resource Navigation

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.

Is it ok to use both nested and shallow resources in rails? How to write the controller/views?

I have resources for which it makes perfect sense to address them both as nested withing other resources and separately. I.e. i expect to use all urls like these:
/account/4/transfers # all transfers which belong to an account
/user/2/transfers # all transfers input by specific user
/project/1/transfers # all transfers relevant to a project
/transfers # all transfers
my concern is how do I write TransfersController actions (for example index) as it would double the logic found in parent models - is there a better way than doing something like
TransfersController
...
def index
if !params[account_id].nil?
#account = Account.find(params[account_id])
#transfers = #account.transfers
elsif !params[user_id].nil?
#user = User.find(params[user_id])
if #user.accesible_by?(current_user)
#transfers = #user.transfers
end
elsif !params[projects_id].nil?
.....
and the same holds for views - although they all will list transfers they will have very different headers, navigation etc for user, account, project, ...
I hope that you see the pattern from this example. I think there should be some non-ugly solution to this. Basically I would love to separate the logic which selects the transfers to be displayed and other things like context specific parts of view.
I've got an open question on this. In my question I outline the 2 methods I came up with. I'm using the second currently, and it's working pretty well.
Routing nested resources in Rails 3
The route I'm using is a bit different because I'm using usernames in place of the IDs, and I want them first. You would stick with something like:
namespace :projects, :path => 'projects/:project_id' do
resources :transfers #=> controllers/projects/transfers_controller.rb
end
# app/controllers/projects/transfers_controller.rb
module Projects
class TransfersController < ApplicationController
# actions that expect a :project_id param
end
end
# app/controllers/transfers_controller.rb
class TransfersController < ApplicationController
# your typical actions without any project handling
end
The reason I use the namespace instead of a call to resources is to have Rails let me use a separate controller with separate views to handle the same model, rather than pushing all the nasty conditional logic into my controller actions.

Resources