How to generate the proper `url_for` a nested resource? - ruby-on-rails

I am using Ruby on Rails 3.2.2 and I would like to generate a proper url_for URL for a nested resource. That is, I have:
# config/routes.rb
resources :articles do
resources :user_associations
end
# app/models/article.rb
class Article < ActiveRecord::Base
...
end
# app/models/articles/user_association.rb
class Articles::UserAssociation < ActiveRecord::Base
...
end
Note: generated named routes are like article_user_associations, article_user_association, edit_article_user_association, ...
When in my view I use:
url_for([#article, #article_association])
Then I get the following error:
NoMethodError
undefined method `article_articles_user_association_path' for #<#<Class:0x000...>
However, if I state routers this way
# config/routes.rb
resources :articles do
resources :user_associations, :as => :articles_user_associations
end
the url_for method works as expected and it generates, for instance, the URL /articles/1/user_associations/1.
Note: in this case, generated named routes are like article_articles_user_associations, article_articles_user_association, edit_article_articles_user_association, ...
However, I think it isn't "good" the way routers are builded / named in the latter / working case. So, is it possible in some way to make the url_for method to work by generating named routes like article_user_association (and not like article_articles_user_association)?
I read the Official Documentation related to the ActionDispatch::Routing::UrlFor method (in particular the "URL generation for named routes" section), but I cannot find out a solution. Maybe there is a way to "say" to Rails to use a specific named router as-like it makes when you want to change the primary key column of a table with the self.primary_key statement...
# app/models/articles/user_association.rb
class Articles::UserAssociation < ActiveRecord::Base
# self.primary_key = 'a_column_name'
self.named_router = 'user_association'
...
end

Your UserAssociation model is in the Articles namespace, which gets included in the named route:
# app/models/articles/user_association.rb
class Articles::UserAssociation < ActiveRecord::Base
...
end
# route => articles_user_association
# nested route => article_articles_user_association
If you remove the namespace, you will get the route helper you are looking for:
# app/models/articles/user_association.rb
class UserAssociation < ActiveRecord::Base
...
end
# route => user_association
# nested route => article_user_association
Unless you have a really good reason for keeping UserAssociation in a namespace, don't.

Related

Automatically use model attributes in URL generating a route

Hello Rails Stack community! <3
I want to generate a public sharing URL to a model that should always include some hash to prevent URL guessing.
This is what I came up with:
# routes.rb
resources :reports do
member do
get '/public/:public_hash', to: 'reports#public', as: 'public'
end
end
# In some view
public_report_path(#report, #report.public_hash)
# /reports/1234/public/xxxx-xxxxx-xxxxx-xxxx
This works okay, but I feel like there should be a more graceful way to do this from the routes definition. What I want to do is public_report_path(#report) which should include the public_hash automatically when generating the URL.
Something in the lines of:
# routes.rb
resources :reports do
member do
get :public, do |route_object|
route_object.path.push(route_object.params.first.public_hash)
end
end
end
# In some view
public_report_path(#report)
# /reports/1234/public/xxxx-xxxxx-xxxxx-xxxx
I saw some solutions where the definition of url_for was overwritten I'd rather not overwrite core functionality. Then I prefer giving 2 parameters to the url helper instead.
Save yourself a lot of trouble and use uuids in your rails migrations?
https://guides.rubyonrails.org/v5.0/active_record_postgresql.html#uuid
All you'd need to do is run this in your migration:
create_table : reports, id: :uuid do |t|
t.timestamps
end
Then standard rails routes, relationships etc will be respected.

Self nesting rails categories

I have an store application, where I need to make custom routing system where URL stores categories for products. For example, http://example.com/languages/ruby/rails will display category#show named 'rails', that has parent named 'ruby', that has parent named 'languages' and and URL of http://example.com/languages/ruby/rails/store will display product in this category.
Currently I have:
category.rb
belongs_to :parent, class_name: 'Category'
has_many :categories, foreign_key: :parent_id
has_many :products
routes.rb
resources :categories, :path => '', :only => [:index, :show] do
resources :products, :path => '', :only => [:show]
end
root :to => 'products#index'
but it still stacks up to 2, e.g. URL http://example.com and http://example.com/languages shows list of categories/subcategories, but http://example.com/languages/ruby have params: {"action"=>"show", "controller"=>"products", "category_id"=>"language", "id"=>"ruby"}
Removing products from routes does not help at all - then it just says that No route matches [GET] "/language/ruby", although I assume It might cause need for extra check if current URL point on category or product later on.
Also I tried get '*categories/:id', to: 'category#show' variations
+ I am using friendly_id gem so that path do not look like http://example.com/2/54/111/6
I just want to find out what is the best ruby on rails solution for this kind of situations, when you need search engine optimizations + endless (e.g. no way to define how deep such recursion can go) nested resources that nest themselves (including fact that category/language/category/ruby/category/rails just looks ugly).
Note: most information I used is taken from Stack Overflow and railscasts.com (including pro/revised episodes), so mentioning a good source with information like this will be great too.
I solved this myself recently with a CMS I built on Rails recently. I basically construct the routes dynamically at runtime from the database records. I wrote this blog post on the strategy:
http://codeconnoisseur.org/ramblings/creating-dynamic-routes-at-runtime-in-rails-4
The core of the solution (adapting the blog post above) is simply iterate over the database records and construct the routes needed for each category. This is the main class for doing that:
class DynamicRouter
def self.load
Website::Application.routes.draw do
Category.all.each do |cat|
get cat.route,
to: "categories#show",
defaults: { id: cat.id },
as: "#{cat.routeable_name}_#{cat.name}"
end
end
end
def self.reload
Website::Application.routes_reloader.reload!
end
end
For the above, the Category model should implement a "routeable_name" method which simply gives an underscored version of the category name that uniquely names that category's route (its not strictly necessary, but helps when doing "rake routes" to see what you have). and the #route method constructs the full route to the category. Notice the defaults which sets the ID param for the category. This makes the controller action a very simple lookup on the category's ID field like so:
class CategoryController < ApplicationController
def show
#category = Category.find(params[:id])
end
end

Strange Rails Routes Behavior

Following the Rails Guide on routing for nested resources, I have two models and my routes like so:
# media.rb
class Media < ActiveRecord::Base
has_many :captions, class_name: "Captions", dependent: :destroy
end
# captions.rb
class Captions < ActiveRecord::Base
belongs_to :media
end
# routes.rb
resources :medias do
resources :captions
end
When I run rake routes | grep captions I get the following, which seems incorrect. Some of my actions aren't nested the way I expect them to:
media_captions GET /medias/:media_id/captions(.:format) captions#index
POST /medias/:media_id/captions(.:format) captions#create
new_media_caption GET /medias/:media_id/captions/new(.:format) captions#new
edit_captions GET /captions/:id/edit(.:format) captions#edit
captions GET /captions/:id(.:format) captions#show
PUT /captions/:id(.:format) captions#update
DELETE /captions/:id(.:format) captions#destroy
As you can see, the index and create actions are properly nested but the other actions are not. Can anyone explain why this is happening?
Is it because your "Captions" class is plural? You're in for some pain if you're not following conventions. Your caption class file name should be caption.rb and look like so:
class Caption < ActiveRecord::Base
belongs_to :media
end
I believe it stems from the fact that the plural of "media" is "media". You can check that in the rails console:
2.0.0-p0 :001 > include ActionView::Helpers::TextHelper
2.0.0-p0 :002 > pluralize(2, "media")
=> "2 media"
So Rails thinks it's a singular resource.
I think routes are constructed this way to make it explicit that the caption is being edited, regardless as to which medias it belongs to.
Consider a has_and_belongs_to_many relationship, where a caption could belong to multiple medias. If you had the following routes:
/medias/1/captions/10/edit
/medias/5/captions/10/edit
You would still only be editing Caption 10. The routes then become redundant. We can write this simply as
/captions/10/edit
However, using an index action nested under a member route means "show me the captions for this media", so that needs to be explicitly stated:
/medias/1/captions
Or for actions such as new or create, we're saying "make a caption for this media". Again we need a specific route.
/medias/1/captions/new
And from the looks of things, you could be running into quite a bit of issues with pluralization. I would consider either adjusting your routes to fit convention, or devise a new standard for nomenclature.

Generated scaffold's view links are not corresponding correctly to the routes map

I have a problem with generated view paths. My routes.rb looks like following
Project::Application.routes.draw do
resources :project_templates do
resources :awards
end
...
project_template.rb like this
class ProjectTemplate < ActiveRecord::Base
belongs_to :user
has_many :awards #...
attr_accessible :user_id #...
...
award.rb like
class Award < ActiveRecord::Base
belongs_to :project_template
attr_accessible :tier #..
...
And generated view links are like this: awards_path
This way app does not work and I need to replace all with project_template_awards_path
I don't know why generator did this without project_template prefix but I ask for you to help me find a way to get around this. Maybe there is some generator command that will add up the missing suffixes to paths? I have to do the same with another class requirement.rb and there are views for that too so I hope there is some magic command for solving my issue.
rake routes | grep awards gives following output:
project_template_awards GET /project_templates/:project_template_id/awards(.:format) awards#index
POST /project_templates/:project_template_id/awards(.:format) awards#create
new_project_template_award GET /project_templates/:project_template_id/awards/new(.:format) awards#new
edit_project_template_award GET /project_templates/:project_template_id/awards/:id/edit(.:format) awards#edit
project_template_award GET /project_templates/:project_template_id/awards/:id(.:format) awards#show
PUT /project_templates/:project_template_id/awards/:id(.:format) awards#update
DELETE /project_templates/:project_template_id/awards/:id(.:format) awards#destroy
Could you try typing $ rake routes in project directory and post there output? Does it still show awards_path for Awards resources?
PS. Controllers have nothing to do here, as you can create route resources without corresponding controllers in app/.
The routes generated are correct. If you want to have this helpers, you can either add this to your route.rb
Project::Application.routes.draw do
resources :project_templates
resources :awards
end
Or implement helpers like
def awards_path(award)
project_template_awards(awards.project_template, awards)
end
More info on ruby guides

Rails Controller action: Customize key in params hash

I'm working on a Rails 3.1.x app and I have the following set of models:
class Widget
include Mongoid::Document
field :name
embeds_many :comments
end
class ShinyWidget < Widget; end
class DullWidget < Widget; end
class Comment
include Mongoid::Document
field :message
embedded_in :widget
end
So basically I need to allow comments to be associated with different types of widgets. Using the standard resources in my routes such as:
resources: widgets do
resources :comments
end
That exposes urls such as GET /widgets, GET /widgets/:widget_id/comments, etc. However, I'd like to expose an API for adding comments to different types of widgets. I'd like those API URL's to look something like:
GET /shinywidgets/:widget_id/comments
POST /shinywidgets/:widget_id/comments
However, I'm ok with having a ShinyWidgetsController and a DullWidgetsController, but I'd like to only create a single CommentsController. Since I haven't thought of a nice way of having a single CommentsController to handle comments for different types of widgets, I tried this:
resources :widgets do
get 'comments', to: 'widgets#comments_index'
post 'comments', to: 'widgets#comments_create'
end
When doing a POST to /widgets/:widget_id/comments the params hash stores the comment data that's being posted in a key named widget instead of what I was hoping for comment.
I know if used resources :comments Rails would change that key in the params hash to be comment, but can I tell Rails what to name that key given my current setup?
Currently I have to create a comment doing something like this:
def comments_create
widget = Widget.find(params.delete :widget_id)
comment = widget.comments.create(params[:widget])
end
I'd really like to have:
comment = widget.comments.create(params[:comment])
Any thoughts?
This is currently very wrong.
In order to make this work as it should, you should create a route like this
resources :widgets do
get 'comments' => 'comments#index'
post 'comments' => 'comments#create'
end
and when posting to this CommentsController, you pass in the comment info correctly in the prams[:comment].
Your controller would have actions like this
def create
widget = Widget.find(params.delete :widget_id)
comment = widget.comments.create(params[:comment])
end

Resources