ActiveRecord::ReadOnlyRecord when using ActiveAdmin and Friendly_id - ruby-on-rails

I started using ActiveAdmin recently in a project and almost everything works great but I'm having a problem when using it in combination with the friendly_id gem. I'm getting ActiveRecord::ReadOnlyRecord thrown for my forms [i believe] because of the friendly_id attribute whose ID is readonly:
{"utf8"=>"✓",
"_method"=>"put",
"authenticity_token"=>"Rc5PmUYZt3BiLvfPQr8iCPPXlbfgjoe/n+NhCwXazNs=",
"space"=>{"name"=>"The Kosmonaut",
"address"=>"8 Sichovykh Striltsiv 24",
"email"=>"info#somedomain.com"},
"commit"=>"Update Space",
"id"=>"the-kosmonaut"} <--- culprit
I'm guessing the last line is the culprit as it's a readonly attribute, it's not in my form but rather in the PATH
http://localhost:5000/manage/spaces/the-kosmonaut/edit
How can I fix this from trying to update the ID?
Form from in ActiveAdmin looks like this:
form do |f|
f.inputs "Details" do
f.input :name
f.input :address
f.input :email
f.input :phone
f.input :website
end
f.inputs "Content" do
f.input :description
f.input :blurb
end
f.buttons
end
UPDATE: This doesn't work either so it's not the friendly_id?
I tried using #watson's suggestion which should have worked but still got the same error ;-(
{"utf8"=>"✓",
"_method"=>"put",
"authenticity_token"=>"Rc5PmUYZt3BiLvfPQr8iCPPXlbfgjoe/n+NhCwXazNs=",
"space"=>{"name"=>"The Kosmonaut 23"},
"commit"=>"Update Space",
"id"=>"6933"}
http://localhost:5000/manage/spaces/6933/edit
When I check the record in the console with record.readonly? it returns false
UPDATE UPDATE: removing the scope_to fixes the problem.
scope_to :current_user, :unless => proc{ current_user.admin? }
Only problem is I need the scope_to to prevent users from seeing records they do not own. My guess is (as I'm assuming scope_to normally works with has_many) that my HABTM association causes some weirdness? Ie Users <-- HABTM --> Spaces?

If you only want friendly ID's in the front end and don't care about them inside Active Admin, you can revert the effects of the friendly_id gem for your Active Admin controllers.
I don't know exactly how friendly_id overrides the to_param method, but if it's doing it the normal way, re-overriding it inside all of your Active Admin controllers should fix it, e.g.:
ActiveAdmin.register Foobar do
before_filter do
Foobar.class_eval do
def to_param
id.to_s
end
end
end
end
Even better you could create a before filter in the base Active Admin controller ActiveAdmin::ResourceController so that it is automatically inherited into all your Active Admin controllers.
First add the filter to the config/initializers/active_admin.rb setup:
ActiveAdmin.setup do |config|
# ...
config.before_filter :revert_friendly_id
end
The open up ActiveAdmin::ResourceController and add a revert_friendly_id method, E.g. by adding the following to the end of config/initializers/active_admin.rb:
ActiveAdmin::ResourceController.class_eval do
protected
def revert_friendly_id
model_name = self.class.name.match(/::(.*)Controller$/)[1].singularize
# Will throw a NameError if the class does not exist
Module.const_get model_name
eval(model_name).class_eval do
def to_param
id.to_s
end
end
rescue NameError
end
end
Update: I just updated the last code example to handle controllers with no related model (e.g. the Active Admin Dashboard controller)
Update 2: I just tried using the above hack together with the friendly_id gem and it seems to work just fine :)
Update 3: Cleaned up the code to use the standard way of adding Active Admin before filters to the base controller

You can customize the resource retrieval according to http://activeadmin.info/docs/2-resource-customization.html#customizing_resource_retrieval. Note that you want to use the find_resource method instead of resource, or you won't be able to create new records.
(Check https://github.com/gregbell/active_admin/blob/master/lib/active_admin/resource_controller/data_access.rb for more details)
In your ActiveAdmin resource class write:
controller do
def find_resource
scoped_collection.where(slug: params[:id]).first!
end
end
You can also overwrite the behavior for all resources by modyfing the ResourceController in the active_admin.rb initializer.
ActiveAdmin::ResourceController.class_eval do
def find_resource
if scoped_collection.is_a? FriendlyId
scoped_collection.where(slug: params[:id]).first!
else
scoped_collection.where(id: params[:id]).first!
end
end
end
Hope that helps!
Side note: When creating new records through the admin interface make sure you don't include the slug field in the form, or FriendlyId will not generate the slugs. (I believe that's for version 5+ only)

This method works for me. add this code in app/admin/model_name.rb
ActiveAdmin.register model_name do
controller do
defaults finder: :find_by_slug
end
end

To add to Denny's solution, a more "friendly" solution would be to use friendly_id's finders.
controller do
def find_resource
scoped_collection.friendly.find_by_friendly_id(params[:id])
end
end
Source

Here is my solution based on #Thomas solution
ActiveAdmin.setup do |config|
# ...
config.before_filter :revert_friendly_id, :if => -> { !devise_controller? && resource_controller? }
end
# override #to_param method defined in model in order to make AA generate
# routes like /admin/page/:id/edit
ActiveAdmin::BaseController.class_eval do
protected
def resource_controller?
self.class.superclass.name == "ActiveAdmin::ResourceController"
end
def revert_friendly_id
model_name = self.class.name.match(/::(.*)Controller$/)[1].singularize
# Will throw a NameError if the class does not exist
Module.const_get model_name
eval(model_name).class_eval do
def to_param
id.to_s
end
end
rescue NameError
end
end

Related

ActiveAdmin with friendly id

I am using friendly_id in my rails 4 application with slug. Now I am using active_admin gem.
Problem:
When I click on show link from active admin for Group resource, It is throwing the following exception:
ActiveRecord::RecordNotFound at /admin/groups/username20-s-group-1
I guess, I need to override some of the active_admin default functions?
There are cases, when application has quit a few resources, hence in order to keep it DRY there is a nice solution requiring few lines of code for whole application - simply override activeadmin's resource controller.
Create config/initializers/active_admin_monkey_patching.rb file with the following content:
ActiveAdmin::ResourceController.class_eval do
def find_resource
finder = resource_class.is_a?(FriendlyId) ? :slug : :id
scoped_collection.find_by(finder => params[:id])
end
end
Do not forget to restart the server.
A better approach to #AndreyDeineko's is to override ActiveAdmin::ResourceController's find_resource method in config/initialisers/active_admin.rb and leverage the methods provided by FriendlyId (5.x at this point):
In config/initialisers/active_admin.rb:
ActiveAdmin.setup do |config|
# == Friendly Id addon
ActiveAdmin::ResourceController.class_eval do
def find_resource
if resource_class.is_a?(FriendlyId)
scoped_collection.friendly.find(params[:id])
else
scoped_collection.find(params[:id])
end
end
end
# initial config
end
This looks much cleaner to me, than putting it in the application controller, as it is related to the configuration of Active Admin.
Found solution for the problem:
In your app/admin/[ResourceName.rb] add:
# app/admin/group.rb
# find record with slug(friendly_id)
controller do
def find_resource
begin
scoped_collection.where(slug: params[:id]).first!
rescue ActiveRecord::RecordNotFound
scoped_collection.find(params[:id])
end
end
end
This solved my problem.
class User < ActiveRecord::Base
extend FriendlyId
friendly_id :username, :use => [:slugged, :finders]
IMHO, it's suboptimal to completely override the find_resource as most of the answers suggest. Better to prepend a module and call super preserving normal behaviour when FriendlyId is not in use. For reference you can check how this method is currently (as of writing) implemented, it is not simply scoped_collection.find(params[:id]) as one might think:
https://github.com/activeadmin/activeadmin/blob/b45b1fb05af9a7f6c5e2be94f61cf4a5f60ff3bb/lib/active_admin/resource_controller/data_access.rb#L104
module ActiveAdminFriendlyIdScoping
def find_resource
if resource_class.is_a? FriendlyId
scoped_collection.friendly.find params[:id]
# Or potentially even
# scoped_collection.friendly.send method_for_find, params[:id]
# Or you could do something like this
# raise "Using FriendlyId, find method configuration ignored" if method_for_find != :find
else
super
end
end
end
ActiveAdmin.setup do |config|
#...
Rails.application.config.to_prepare do
ActiveAdmin::ResourceController.prepend(ActiveAdminFriendlyIdScoping)
end
end
If you've tried some of the other answers here and gotten
uninitialized constant InheritedResources::Base (NameError)
Then you might consider monkey patching FriendlyId rather than ActiveAdmin. Create a new initializer file config/initializers/friendly_id_monkey_patch.rb containing this:
module FriendlyIdModelMonkeyPatch
def to_param
if caller.to_s.include? 'active_admin'
id&.to_s
else
super
end
end
end
module FriendlyId::Model
prepend FriendlyIdModelMonkeyPatch
end
Now all of your FriendlyId models will revert to using their ID in ActiveAdmin and their slug everywhere else.
See also this answer, which does the same thing but for only one model (rather than monkey patching for all FriendlyId models)

Friendly_id and Active Admin conflict - possibly because of revert_freindly_id

first time asking a question on stack overflow :)
I'm having a conflict between friendly_id and active admin (it's an assumption), as discussed in many threads here. I've looked at all those threads, but I'm not entirely sure they solve my problem. Sorry for the really long post!
I'm trying to create friendly links to products on my website. I've added the friendly_id gem and everything works fine in my dev and staging environments, but friendly links fail on production. Here is all my code:
Model:
class Product < ActiveRecord::Base
extend FriendlyId
friendly_id :name, use: :slugged
...
end
Controller:
class ProductsController < ApplicationController
before_filter :get_product, only: [:show]
...
private
def get_product
#product = Product.friendly.find(params[:id])
end
end
All my product records have a completed slug field at this point. I don't want to use slugs in my admin interface, so when I came across a solution here, I went ahead and modified it a bit to get active admin to work together with friendly_id.
config/initializers/active_admin.rb:
ActiveAdmin.setup do |config|
...
config.before_filter :revert_friendly_id
end
I've defined revert_friendly_id in the application controller:
class ApplicationController < ActionController::Base
...
protected
def revert_friendly_id
model_name = self.class.name.match(/::(.*)Controller$/)[1].singularize
# Will throw a NameError if the class does not exist
Module.const_get model_name
eval(model_name).class_eval do
def to_param
id.to_s
end
end
rescue NameError
end
end
I've noticed that when I first deploy to production via capistrano, the friendly links work as expected. So my product links are accessible with: http://website.com/products/my-product-slug. But the minute I access the admin interface on production, the links immediately switch back to product ids instead: http://website.com/products/12345. I'm not entirely sure how to resolve this problem, though I understand why it might be happening, can someone help me please?
Here is how I solved the problem. Based on armstrjare's fix at this link.
I removed the revert_friendly_id function from my application controller and the before_filter from my config. Then just added the following to app/admin/product.rb:
ActiveAdmin.register Product do
around_filter do |controller, action|
Product.class_eval do
alias :__active_admin_to_param :to_param
def to_param() id.to_s end
end
begin
action.call
ensure
Product.class_eval do
alias :to_param :__active_admin_to_param
end
end
end
...
end
And everything worked as expected. Hope this helps someone else!
I found a very simple solution: Just overwrite the to_param in your model and check if it is called from active_admin.
app/models/product.rb:
class Product < ActiveRecord::Base
def to_param
if caller.to_s.include?"active_admin"
id && id.to_s
else
slug
end
end
end
When you set the to_param method, it will be set on the entire application. So you have to check if the requested controller is in the Admin namespace or not. Based on that you have to switch back the return of the to_param method.
You can redefine find_resource method in controller:
controller do
def find_resource
scoped_collection.friendly.find(params[:id])
end
end
For the active_admin belongs_to association you can to use the finder: option (from https://github.com/activeadmin/inherited_resources/blob/master/lib/inherited_resources/belongs_to_helpers.rb#L17)
For example:
belongs_to :content, finder: :find_by_slug!

Rails routes with :name instead of :id url parameters

I have a controller named 'companies' and rather than the urls for each company being denoted with an :id I'd like to have the url use their :name such as: url/company/microsoft instead of url/company/3.
In my controller I assumed I would have
def show
#company = Company.find(params[:name])
end
Since there won't be any other parameter in the url I was hoping rails would understand that :name referenced the :name column in my Company model. I assume the magic here would be in the route but am stuck at this point.
Good answer with Rails 4.0+ :
resources :companies, param: :name
optionally you can use only: or except: list to specify routes
and if you want to construct a URL, you can override ActiveRecord::Base#to_param of a related model:
class Video < ApplicationRecord
def to_param
identifier
end
# or
alias_method :to_param, :identifier
end
video = Video.find_by(identifier: "Roman-Holiday")
edit_videos_path(video) # => "/videos/Roman-Holiday"
params
The bottom line is you're looking at the wrong solution - the params hash keys are rather irrelevant, you need to be able to use the data contained inside them more effectively.
Your routes will be constructed as:
#config/routes.rb
resources :controller #-> domain.com/controller/:id
This means if you request this route: domain.com/controller/your_resource, the params[:id] hash value will be your_resource (doesn't matter if it's called params[:name] or params[:id])
--
friendly_id
The reason you have several answers recommending friendly_id is because this overrides the find method of ActiveRecord, allowing you to use a slug in your query:
#app/models/model.rb
Class Model < ActiveRecord::Base
extend FriendlyId
friendly_id :name, use: [:slugged, :finders]
end
This allows you to do this:
#app/controllers/your_controller.rb
def show
#model = Model.find params[:id] #-> this can be the "name" of your record, or "id"
end
Honestly, I would just overwrite the to_param in the Model. This will allow company_path helpers to work correctly.
Note: I would create a separate slug column for complex name, but that's just me. This is the simple case.
class Company < ActiveRecord::Base
def to_param
name
end
end
Then change my routes param for readability.
# The param option may only be in Rails 4+,
# if so just use params[:id] in the controller
resources :companies, param: :name
Finally in my Controller I need to look it up the right way.
class CompaniesController < ApplicationController
def show
# Rails 4.0+
#company = Company.find_by(name: params[:name])
# Rails < 4.0
#company = Company.find_by_name(params[:name])
end
end
I recommend using the friendly_id for this purpose.
Please be noted that there are differences between friendly_id 4 and 5. In friendly_id 4, you can use like this
#company = Company.find(params[:id])
However, you won't be able to do that in friendly_id 5, you have to use:
#company = Company.friendly.find(params[:id])
In case that you don't want to use the params[:id] but params[:name], you have to override the route in routes.rb. For example
get '/companies/:name', to: "companies#show"
Hope these info would be helpful to you
There's actually no magic to implement this, you have to either build it yourself by correctly implementing to_param at your model (not recommended) or using one of the gems available for this like:
friendly_id
has_permalink
I use friendly_id and it does the job nicely.
Model.find(primary_key)
The default parameter here is primary_key id.
If you want to use other columns, you should use Model.find_by_xxx
so here it could be
def show
#company = Company.find_by_name(params[:name])
end
The :id parameter is whatever comes after the slash when the URL is requested, so a name attribute needs to be extracted from this by checking the :id parameter for non-numerical values with regular expressions and the match? method in the controller. If a non-numerical value is present, the instance can be assigned by the name attribute using the find_by_name() method that rails generated for the model (assuming that the model has an attribute called name)
That's how I figured out how to do it in my app with my Users resource. My users have a username attribute, and all I had to do was modify the UsersController to define my #user variable differently depending on the :id parameter:
private
# allow routing by name in addition to id
def get_user
if params[:id].match?(/\A\d+\Z/)
# if passed a number, use :id
#user = User.find(params[:id])
else
# if passed a name, use :username
#user = User.find_by_username(params[:id])
end
end
This gives me the option to use either id or username when I create a link to a particular user, or type it into the browser's address bar.
Then the only other (optional) thing to do is to change all the links in the views so that they point to the URL with the name instead of the URL with the id.
For example, within the link_to() method call in my navigation bar I changed
... user_path(current_user) ...
to
... user_path(current_user.username) ...
In your example, you might have a company view with the following link:
<%= link_to #company.name, company_path(#company.name) %>
Which, if the current company is Microsoft, would display "Microsoft" and link to "companies/Microsoft", even though the URL "companies/1" would still be valid and display the same thing as "companies/Microsoft"

Forbidden Attributes Error in Rails 4 when encountering a situation where one would have used attr_accessible in earlier versions of Rails

With the recent upgrade to Rails 4, updating attributes using code resembling the below does not work, I get a ActiveModel::ForbiddenAttributes error:
#user.update_attributes(params[:user], :as => :admin)
Where User has the following attr_accessible line in the model:
attr_accessible :role_ids, :as =>admin
# or any attribute other than :role_ids contained within :user
How do you accomplish the same task in Rails 4?
Rails 4 now has features from the strong_parameters gem built in by default.
One no longer has to make calls :as => :admin, nor do you need the attr_accessible :user_attribute, :as => admin in your model. The reason for this is that, by default, rails apps now have 'security' for every attribute on models. You have to permit the attribute you want to access / modify.
All you need to do now is call permit during update_attributes:
#user.update_attributes(params[:user], permit[:user_attribute])
or, to be more precise:
#user.update_attributes(params[:user].permit(:role_ids))
This single line, however, allows any user to modify the permitted role. You have to remember to only allow access to this action by an administrator or any other desired role through another filter such as the following:
authorize! :update, #user, :message => 'Not authorized as an administrator.'
. . . which would work if you're using Devise and CanCan for authentication and authorization.
If you create a new Rails 4 site you'll notice that generated controllers now include a private method which you use to receive your sanitised params. This is a nice idiom, and looks something like this:
private
def user_params
params.require(:user).permit(:username, :email, :password)
end
The old way of allowing mass assignment was to use something like:
attr_accessible :username, :email, :password
on your model to mark certain parameters as accessible.
Upgrading
To upgrade you have several options. Your best solution would be to refactor your controllers with a params method. This might be more work than you have time for right now though.
Protected_attributes gem
The alternative would be to use the protected_attributes gem which reinstates the attr_accessible method. This makes for a slightly smoother upgrade path with one major caveat.
Major Caveat
In Rails 3 any model without an attr_accessible call allowed all attributes though.
In Rails 4 with the protected_attributes gem this behaviour is reversed. Any model without an attr_accessible call has all attributes restricted. You must now declare attr_accessible on all your models. This means, if you haven't been using attr_accessible, you'll need to add this to all your models, which may be as much work as just creating a params method.
https://github.com/rails/protected_attributes
This problem might also be caused by the Cancan gem
Just add to application_controller.rb
before_filter do
resource = controller_name.singularize.to_sym
method = "#{resource}_params"
params[resource] &&= send(method) if respond_to?(method, true)
end
Works without any further modifications of code
got it from here: https://github.com/ryanb/cancan/issues/835#issuecomment-18663815
Don't forget to add your new user_params method to the controller action:
def create
#user = User.new(user_params)
#user.save
redirect_to 'wherever'
end
def create
#user = User.create(user_params)
....
end
def update
#user = User.find(params[:id])
if #user.update_attributes(blog_params)
redirect_to home_path, notice: "Your profile has been successfully updated."
else
render action: "edit"
end
end
private
def user_params
params.require(:user).permit(:name, :age, :others)
end

How to get ActiveAdmin to work with Strong Parameters?

Update: this question was asked before there was a solution for it already in ActiveAdmin. As Joseph states, the ActiveAdmin documentation now contains this information, but the answers here are provided for those working with older versions of ActiveAdmin.
When the strong_parameters 0.1.4 is used with ActiveAdmin 0.5.0 in Rails 3.2.8, if the model you are using is using StrongParameters by including:
include ::ActiveModel::ForbiddenAttributesProtection
then you get the following error in the log if you try to create/edit a record:
ActiveModel::ForbiddenAttributes (ActiveModel::ForbiddenAttributes)
Update to the latest inherited_resources gem and do this in your controller block:
ActiveAdmin.register Blog do
#...
controller do
#...
def permitted_params
params.permit(:blog => [:name, :description])
# params.permit! # allow all parameters
end
end
end
The documentation now clearly states how to go about Setting up Strong Parameters in Rails 4. See:
https://github.com/gregbell/active_admin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters
The accepted answer did not work for me with resources defined in an engine, so I tracked down the original resource_params in inherited_resources/lib/inherited_resources/base_helpers.rb and came up with this solution which closer mimics that code, and which works with engines:
In config/initializers/active_admin.rb:
ActiveAdmin::ResourceController.class_eval do
# Allow ActiveAdmin admins to freely mass-assign when using strong_parameters
def resource_params
[(params[resource_request_name] || params[resource_instance_name]).try(:permit!) || {}]
end
end
in your config/initializers/active_admin.rb
config.before_filter do
params.permit!
end
Update: See #Brendon-Muir's answer for latest way to do this. The following information was correct previously, so I'll leave it here in case it helps others with an older version of ActiveAdmin.
A patch had been proposed in a google group thread:
https://groups.google.com/forum/?fromgroups=#!topic/activeadmin/XD3W9QNbB8I
Then was being put together here:
https://github.com/gregbell/active_admin/issues/1731
But for now, the least invasive way to add strong parameters support to ActiveAdmin in your app is to redefine resource_params in your controller block, either via the "permit all params" method, which is less secure:
controller do
def resource_params
return [] if request.get?
[ params[active_admin_config.resource_class.name.underscore.to_sym].permit! ]
end
end
or the more secure explicit way:
controller do
def resource_params
return [] if request.get?
[ params.require(:name_of_model).permit(:each,:param,:goes,:here,:if,:you,:want) ]
end
end
See Active Admin docs on modifying controllers:
http://activeadmin.info/docs/8-custom-actions.html#modify_the_controller
You can also use permit_params as follows:
ActiveAdmin.register Resource do
permit_params do
%i(first_name last_name)
end
index pagination_total: false do
column :id
column :first_name
column :last_name
actions
end
end

Resources