form_for nested resource with changed name - ruby-on-rails

I have two models:
class Project < ActiveRecord::Base
has_many :stories, class_name: 'ProjectStory'
end
class ProjectStory
belongs_to :project
end
with the following routes:
resources :projects do
resources :stories
end
All the controllers are set correctly. Now, I have a stories/_form partial (which is to be reused for both new and edit) with the following line:
= form_for [#story.project, #story] do
The problem is however, that #story.model_name.singular_route_name returns project_story and hence it is trying to execute project_project_name_path to calculate form url. This, obviously, raising no method error.
I could override it with url option, however I would need to use ternary operator to handle both edit and new cases:
= form_for #story, url: #story.new_record? ? project_stories_path(#story.project) : project_story_path(#story.project, #story)
Is there any way to let rails know that it should use different name for a single element of the nested route? I would prefer not to rename my models or controllers (to avoid those annoying url helpers like edit_project_project_story_path)

Related

Rails ActiveAdmin has_one and belongs_to causes 'Undefined Method for <PLURAL_RESOURCE>'

In Rails, I have a 'User' model and a 'Wallet' model. A 'User' has_one wallet and each 'Wallet' belongs_to a 'User'. I made a 'Show' page in ActiveAdmin to view a User's Wallet. However, going to that page returns this error:
undefined method `wallets' for #<User:0x007f...>
HOWEVER, when I update the User model to 'has_many :wallets' instead of ':has_one wallet', everything works. Here is the relevant code from my models and ActiveAdmin code:
Models:
class User < ActiveRecord::Base
has_one :wallet, dependent: :destroy
end
class Wallet < ActiveRecord::Base
belongs_to :user
end
ActiveAdmin:
ActiveAdmin.register Wallet do
belongs_to :user
actions :all, except: :destroy
show do
div do
'hello'
end
end
end
ActiveAdmin.register User do
actions :all, except: :destroy
permit_params
action_item :wallet, only: :show do
link_to('Wallet', admin_user_wallet_path(user, user.wallet.id))
end
index do...
end
Any ideas as to where I might have gone wrong?
Edit 1: updates to correct colon placement mistakes in description
Edit 2:
in response to:
Can you show your routes file? Also, can you give us the full traceback of the error message and give us the output of rake routes? I suspect that the reason it's complaining about wallets not being defined (even though you never call wallets in the above code) is that some routing is making assumptions about how the relationships look. – Glyoko 4 mins ago
My routes file contains no mention 'wallet' or 'wallets'.
My stack error more specifically looks like this:
activemodel (4.1.15) lib/active_model/attribute_methods.rb, line 435
Let me know if you need more than that.
Here's the related output from 'bin/rake routes':
admin_user_wallets GET /admin/users/:user_id/wallets(.:format) admin/wallets#index
POST /admin/users/:user_id/wallets(.:format) admin/wallets#create
new_admin_user_wallet GET /admin/users/:user_id/wallets/new(.:format) admin/wallets#new
edit_admin_user_wallet GET /admin/users/:user_id/wallets/:id/edit(.:format) admin/wallets#edit
admin_user_wallet GET
/admin/users/:user_id/wallets/:id(.:format) admin/wallets#show
admin_user_wallet PATCH /admin/users/:user_id/wallets/:id(.:format) admin/wallets#update
admin_user_wallet PUT /admin/users/:user_id/wallets/:id(.:format) admin/wallets#update
ActiveAdmin uses InheritedResources gem internally, the belongs_to method ends up inside InheritedResources.
Possible solution here
ActiveAdmin.register Wallet do
belongs_to :user, singleton: true
actions :all, except: :destroy
end
The option singleton: true makes the Wallet a singular resource for the User.
Probably, another option optional: true may be helpful if Wallet is not required for any User to present
Even though your routes may not explicitly reference wallet(s), there may be something there making assumptions about how records are related to each other.
Look at the output of rake routes, in particular:
admin_user_wallet GET
/admin/users/:user_id/wallets/:id(.:format) admin/wallets#show
When you call admin_user_wallet_path(user, user.wallet.id) it's matching the /admin/users/:user_id/wallets/:id(.:format) route. Notice how that expects both a user id and a wallet id in the path. This is a tip-off that something is off here, since if you have the user, there should be exactly one wallet associated with it. You shouldn't need to give both the user and wallet id.
Since the wallet resource is nested under users, the page where you view the user's wallet is actually more of an index page than a show. If the wallet were an independent resource, then you could have a path like /admin/wallets/:id and things would work out fine.
But since the wallet is a subresource of the user, you would ideally want a path like /admin/users/:user_id/wallet. There's no need to pass the wallet id, since you already have the user.
tl;dr: Try changing the shows to indexs and see where that gets you. e.g.
index do
div do
'hello'
end
end
# ...
action_item :wallet, only: :index do
link_to('Wallet', admin_user_wallets_path(user))
end
Okay.. So I had this same exact issue. I had a belongs_to a parent where the parent had only a has_one to the child model..... Nothing seemed to work so I decided to fake it. I am not sure if this is the best way to do this but it worked. In the parent model, add a method:
class User < ActiveRecord::Base
has_one :wallet
def wallets
Wallet.where(user_id: id)
end
end
The above code is a hotfix until I can find some other way to implement what I need.

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

Rails: Canned/RESTful way for accessing data returned by a method of a model

In my app I have a User model which defines a history method that returns a list of Activity objects, showing the last N actions the user has carried out. The UserController#history method wires this with a view.
The code looks as follows:
class UserController < ApplicationController
def history
user = User.find(params[:id])
#history = user.history(20)
end
end
class User < ActiveRecord::Base
has_many :activities
def history(limit)
...
end
end
Naturally, I also added this line to my routes.rb file:
match '/user/:id/:action', :controller => 'user'
so now when I go to localhost:3000/user/8/history I see the history of user 8. Everything works fine.
Being a Rails NOOB I was wondering whether there is some canned solution for this situation which can simplify the code. I mean, if /user/8 is the RESTful way for accessing the page of User 8, is it possible to tell Rails that /user/8/history should show the data returned by invoking history() on User 8?
First of all the convention to name controllers is in the plural form unless it is only for a single resource, for example a session.
About the routes I believe you used the resources "helper" in your routes, what you can do is specify that the resource routes to users also has a member action to get the history like this
resources :users do
member do
get :history
end
end
I think there is no cleaner way to do this
You can check it here http://guides.rubyonrails.org/routing.html#adding-more-restful-actions
As far as the rails standards are concerned, it is the correct way to show the history in your case. In rails controllers are suppose to be middle-ware of views and model, so defining an action history seems good to me.
And you can specify the routes in better way as:
resources :user do
get 'history', :on => :member #it will generate users/:id/history as url.
end

Rails has_one new action going to wrong route

When I click the 'New Schedule Status' button on the 'Project' show page, but the route that the error shows me is plural, when it should be singular. Here's my code:
# project.rb
class Project < ActiveRecord::Base
has_one :schedule_status
end
# schedule_status.rb
class ScheduleStatus < ActiveRecord::Base
belongs_to :project
end
# schedule_statuses_controller.rb
def new
#project = Project.find(params[:project_id])
#schedule_status = #project.build_schedule_status
end
# routes.rb
resources :projects do
resource :schedule_status
end
# _form.html.erb
<%= form_for [#project, #schedule_status] do |f| %>
...
The error informs me that my form_for line is incorrect. It seems like my instance variables are setup correctly, but the error is:
undefined method `project_schedule_statuses_path` for ...
Any idea why the route it's attempting to access is plural?
This is a bug. form_for looks for the plural version of the object. However since you've declared a singular resource :schedule_status, the path helper method is never created.
To get around this you should use :url parameter for form_for.
Look at this question/answer for more clarity.
It is not a bug it is a feature (ticket closed as won't fix):
rails issue:
https://github.com/rails/rails/issues/1769
summary quote:
the error has been around for a long while however a clean solution
doesn't readily represent itself. The polymorphic_url helper has no
'intelligence' in how it operates - it has no information about what
you've declared as resources in your routes.rb. All it has to go on it
the name of the model and how that maps to the named url helpers.
The problem is there is no easy way to discern whether a model maps
to a singular or a regular resource url. Checking for the presence of
a collection url doesn't work as the resource may have been specified
with :except => :index and trying to rescue route generation errors
doesn't work because passing an instance to a singular resource url
helper will generate a url with the format set to the id and no
exception.
rails issue closed in favour of the previous:
https://github.com/rails/rails/issues/4978
conclusion:
in such cases you're supposed to give the url. url_for cannot reflect
on routes to see if that's a resource or not.

Rails routing and how to keep category view DRY?

I have a model that has a belongs_to association to categories.
Example:
Thing
belongs_to :category
Category
has_one :thing
Should I make a view file for each category to find the given category, example:
views/things/category_name1.html.erb
views/things/category_name2.html.erb
views/things/category_name3.html.erb
views/things/category_name4.html.erb
Or is there a more convenient way to do this?
The views are the same for each category except for the category name in the find method. I have tried this:
<%= Thing.find(:all, :conditions => {:category => 'Name of category'}) %>
I want my url to be /category_name/name_of_thing.
The friendly_id gem will handle the URL generation that you're looking for. In your routes, you'll want to have something along the lines of (Rails 3). You do not want to have a view for each category -- this should be dynamic.
resources :categories do
resources :things
end
No! Don't create redundant views like that.
Put the following code in your Category model, and any other model you wish to have descriptive URLs:
def to_param
return "#{id} #{name}".parameterize
end
Assuming name is an attribute of Category, this will create unique URLs containing the category name. If you know that name will definitely be unique, you can replace the return line with just name.parameterize.

Resources