How should I route search modes? Different resources? One resource? Multiple actions? - ruby-on-rails

I have a resource named Cimgs
You can see them in the #index
Them can be searched by content, via Sphinx
Also can be searched by tags
And can be searched by another related Cimg, checking for tags collisions
And here is my question: which is the better way to route this?
Should I pass an extra parameter to the #index and search accordingly to the parameter? After all they all use the same view.
Or should I create an action for each search method?
Or maybe a whole resource?
I'm currently doing the following:
resources :cimgs, path: 'pics' do
collection do
get 'search(/:q)', action: :index, search_by: :content, as: :search_by_content
get 'tags/:tag', action: :index, search_by: :tags, as: :search_by_tag
get 'page/:page', action: :index
end
member do
get 'related', action: :index, search_by: :related, as: :search_by_related
end
end
root :to => 'cimgs#index'
The problem is that I'm using Kaminari for paging, and it doesn't detect the page/:page for another route than the root. (I don't actually know the mechanism that it uses to detect the page/:page)
And here is my controller action:
def index
#cimgs = case params[:search_by]
when :content; Cimg.content_search params[:q], params[:page], request.remote_ip
return redirect_to #cimgs[0] if #cimgs.length == 1
when :tag; Cimg.search_by_tag params[:tag], params[:page]
when :related; Cimg.search_related_to params[:id], params[:page]
else; Cimg.get_for_front_page params[:page]
end
render :index
end

I would keep them all in a single action. My assumption is that eventually, you'll decide it's useful to combine your search methods ("I want an image that's related to this other image, but has the tag 'ketchup'."). That'll be easy to do if you have a single action that handles all your searches already, but much more difficult - or at least involving lots of duplicate code - if you have separate actions.
I do something similar in a site I'm working on. I take a master search hash as params[:search], and then have each individual search term in that hash. And then, since I've made a named_scope (might be a different name in Rails 3) for each search option, I can do something like:
scope = MyModel.scoped
params[:search].each_pair do |key, value|
scope = scope.send(key, value)
end
#models = scope.all
Plus some basic error checking, but that's the idea.
Hope that helps!

Related

If Model A has_many Instances of B, through another model C, how might I make a route to destroy an instance of B on a particular instance of A?

So forgive me if my title was a little unclear; I tried to make it as generic as possible.
I have a model User, a ChatRoom model and a ChatRoomUser model (which has no controller). User has a has_many relationship with ChatRoom, through the ChatRoomUser, and vice versa.
I want to make a route to allow the user to leave one of the chat rooms; I have the code to handle the actual leaving of the room, in the UsersController:
def leave
#chosen = ChatRoom.find(params[:chat_room_id])
if #chosen.nil?
redirect_back(fallback_location: chat_room_path(#chosen))
end
current_user.chat_rooms.remove(#chosen)
# If there are no users left in the room, destroy it.
if(!#chosen.users.any?)
#chosen.destroy
end
redirect_to chat_rooms_path
end
and I think that's fine. But I'm unclear as to how to properly set up a route for this. I have in my routes file:
delete '/users/chat_room/:chat_room_id', to: 'users#leave'
but I have no idea how to refer to this in an html.erb file. I've tried <%= link_to users_chat_room_path(#chatroom), method: :delete %> but no dice.
Should I perhaps make a controller for the ChatRoomUsers and make a destroy method on that to handle this? I don't know if that's good practice (making a controller just for a single method).
Thanks in advance for any help.
Routes can be given custom names like this:
delete '/users/chat_room/:chat_room_id',
to: 'users#leave',
as: :leave_room
Now it's available as leave_room_path.
Should I perhaps make a controller for the ChatRoomUsers and make a destroy method on that to handle this? I don't know if that's good practice (making a controller just for a single method).
That is what REST advocates would tell you to do. A controller for just one method is not a bad practice. Bloating your controllers - that's bad practice. Controllers should be as skinny as possible.
Adding to how you might do this RESTfully, and given the object your deleting is a chat_room_user and the only param is :id on a chat_room:
In routes, nest a 'resource' under chat_room for chat_room_user.
resources :chat_room do
resource :chat_room_user, :only => [:create, :delete]
end
Then in the chat_room_users controller
def create
#chat_room = ChatRoom.find_by_id(params[:chat_room_id])
# current_user join a chat room
end
def delete
#chat_room = ChatRoom.find_by_id(params[:chat_room_id])
# current_user leave a chat room
end
In the view, you'd refer to this route as chat_room_chat_room_user_path(#chat_room) and add the method: :method => :delete or :method => :post.

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

RESTFUL routing with the same page

I am looking to find the best way to setup the routes for my app.
The application allows users to register, post jobs and apply for jobs. The issue I am having is that two routes that should really show different things are linking through to the same pages.
My routes are as follows:
resources :users do
resources :apps
end
resources :jobs do
resources :apps
end
As a result of this I end up with two paths:
users/id/apps
jobs/id/apps
What I would like to do is use the first path to show job applications that a user has completed. I would then like to use the second path to show the owner of the job the applications they have received for that job.
The issue I am having is that both paths end up at apps#index
Any advice people can offer on how to best route this would be much appreciated! :)
Both those paths are potentially fine for what you want to do. When a request comes into
users/:user_id/apps
then you'll be able to do something like this in your controller to populate the list of apps:
#apps = User.find_by_id(params[:user_id]).apps
And in the case of the other path, you'll do the same but with params[:jobs_id], e.g.
#apps = Job.find_by_id(params[:job_id]).apps
In your controller you would have some conditional code that builds #apps depending on which path the request came in via (by looking to see if :user_id or :job_id is in params)... something like this:
if params[:user_id]
#apps = User.find_by_id(params[:user_id]).apps
elsif params[:job_id]
#apps = Job.find_by_id(params[:job_id]).apps
end
or maybe refactored to...
if params[:user_id]
#user = User.find(params[:user_id])
#apps = #user.apps
elsif params[:job_id]
#job = Job.find(params[:job_id])
#apps = #job.apps
end
If you use the "resources" keyword, rails will just map to the default routes.
If you would like specific routes to map to something else, you should consider using "match."
For example:
match "users/id/apps" => "users/id/apps"
match "jobs/id/owners" => "jobs/id/owners"
This page shows a more detailed usage of it: http://guides.rubyonrails.org/routing.html
You also have the options to change the route to something else or the code itself.
Hope that helps.

How to implement "short" nested vanity urls in rails?

I understand how to create a vanity URL in Rails in order to translate
http://mysite.com/forum/1 into http://mysite.com/some-forum-name
But I'd like to take it a step further and get the following working (if it is possible at all):
Instead of:
http://mysite.com/forum/1/board/99/thread/321
I'd like in the first step to get to something like this: http://mysite.com/1/99/321
and ultimately have it like http://mysite.com/some-forum-name/some-board-name/this-is-the-thread-subject.
Is this possible?
To have this work "nicely" with the Rails URL helpers you have to override to_param in your model:
def to_param
permalink
end
Where permalink is generated by perhaps a before_save
before_save :set_permalink
def set_permalink
self.permalink = title.parameterize
end
The reason you create a permalink is because, eventually, maybe, potentially, you'll have a title that is not URL friendly. That is where parameterize comes in.
Now, as for finding those posts based on what permalink is you can either go the easy route or the hard route.
Easy route
Define to_param slightly differently:
def to_param
id.to_s + permalink
end
Continue using Forum.find(params[:id]) where params[:id] would be something such as 1-my-awesome-forum. Why does this still work? Well, Rails will call to_i on the argument passed to find, and calling to_i on that string will return simply 1.
Hard route
Leave to_param the same. Resort to using find_by_permalink in your controllers, using params[:id] which is passed in form the routes:
Model.find_by_permalink(params[:id])
Now for the fun part
Now you want to take the resource out of the URL. Well, it's a Sisyphean approach. Sure you could stop using the routing helpers Ruby on Rails provides such as map.resources and define them using map.connect but is it really worth that much gain? What "special super powers" does it grant you? None, I'm afraid.
But still if you wanted to do that, here's a great place to start from:
get ':forum_id/:board_id/:topic_id', :to => "topics#show", :as => "forum_board_topic"
Take a look at the Rails Routing from the Outside In guide.
maybe try something like
map.my_thread ':forum_id/:board_od/:thread_id.:format', :controller => 'threads', :action => 'show'
And then in your controller have
#forum = Forum.find(params[:forum_id])
#board = #forum.find(params[:board_id])
#thread = #board.find(params[:thread_id])
Notice that you can have that model_id be anything (the name in this case)
In your view, you can use
<%= link_to my_thread_path(#forum, #board, #thread) %>
I hope this helps

Accessing a resource in routes.rb by using attributes other than Id

I have the following in my routes.rb
map.resources :novels do |novel|
novel.resources :chapters
end
With the above defined route, I can access the chapters by using xxxxx.com/novels/:id/chapters/:id.
But this is not what I want, the Chapter model has another field called number (which corresponds to chapter number). I want to access each chapter through an URL which is something like
xxxx.com/novels/:novel_id/chapters/:chapter_number. How can I accomplish this without explicitly defining a named route?
Right now I'm doing this by using the following named route defined ABOVE map.resources :novels
map.chapter_no 'novels/:novel_id/chapters/:chapter_no', :controller => 'chapters', :action => 'show'
Thanks.
:id can be almost anything you want. So, leave the routing config untouched and change your action from
class ChaptersControllers
def show
#chapter = Chapter.find(params[:id])
end
end
to (assuming the field you want to search for is called :chapter_no)
class ChaptersControllers
def show
#chapter = Chapter.find_by_chapter_no!(params[:id])
end
end
Also note:
I'm using the bang! finder version (find_by_chapter_no! instead of find_by_chapter_no) to simulate the default find behavior
The field you are searching should have a database index for better performances

Resources