Controller Best Practice - ruby-on-rails

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'.

Related

Dynamic Custom Routing in Rails

I don't want to use Rails routing conventions, as I want my urls to look a certain way. My model structure hierarchy is dynamic.
If I have these three paths referencing different books:
/book/chapter/page
/book/chapter/page/sentence
/book/page/sentence
Different books I'm storing in the DB (mongodb) have different hierarchies, but they all START with Book and END with Sentence, it's just the middle part that varies.
My current logic for Routes is to handle it all in a RoutesController:
Routes
get '*path', to: "routes#route"
Routes#route
def route
path = params[:path].split("/")
## Look up the book
book = Book.find_by(name: path[0])
## Get the book specific hierarchy, like ["books", "pages", "sentences"]
## or ["books", "chapters", "pages", "sentences"]
hierarchy = book.hierarchy
if path.length == hierarchy.length
## Since end of hierarchy is always sentence
## Here is want to redirect_to Sentence#Show
else
## Here I want to look up based on model specific hierarchy
## LOOKUP CONTROLLER: hierarchy[path.length-1]}", ACTION: Show
## eg: Chapter#show, Subchapter#show, Page#show, etc.
end
end
I cant do a simple redirect_to because I'm not using the config/routes file so it throws an error:
No route matches {:action=>"show", :controller=>"chapters", :path=>"books/chapters"}
I know you don't want to use the default routes, but you may be able to make them work.
scope ':bookName' do
scope '(:chapter)', :chapter => /c\d+/ do #we need to know if it's a chapter
scope '(:page)', :page => /p\d+/ do #or a page, c1 = chapter, p1 = page
resources :sentence
end
resources :page
end
resources :chapter
end
The () make a part of the path optional.

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

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.

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