ActiveAdmin with friendly id - ruby-on-rails

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)

Related

Allowing additional parameters for admin accounts

I have a number of models with attributes that ordinary users should not be able to change, but admins should. For example (though this is not my problem domain), normal users should not be able to change a Post's user_id, but administrators should be allowed to do so.
Handling this at the view level is simple enough—I can show or not show fields depending on whether the user is an administrator—but I'm not sure how to handle it in the controller's strong parameter handling. The only solution I can come up with (and the solution offered previously) is to Repeat Yourself, something you try to Don't in Rails:
def post_params
if admin?
params.require(:post).permit(:title, :text, :date, :user_id)
else
params.require(:post).permit(:title, :text, :date)
end
end
Is there a better way to handle this?
I don't see anything wrong with your current implementation. That being said, if you wanted to re-use these attribute permissions in a different controller (e.g. an Api::PostsConrtoller), one way to DRY it up would be to extract the code into it's own class. This is the approach Ryan Bates used in the Railscast about Strong Parameters (note: requires Pro account).
# app/models/permitted_params.rb
class PermittedParams < Struct.new(:params, :user)
def post
if user && user.admin?
params.require(:post).permit(:title, :text, :date, :user_id)
else
params.require(:post).permit(:title, :text, :date)
end
end
end
You can then instantiate this class from within the ApplicationController
# app/controllers/application_controller.rb
def permitted_params
#permitted_params ||= PermittedParams.new(params, current_user)
end
and then use it in any controller where you need that permission logic without duplicating the logic.
# app/controllers/posts_controller.rb
def update
#post = Post.find(params[:id])
if #post.update_attributes(permitted_params.post)
...
else
...
end
end
What's really nice about this solution is that you can also use it to DRY-up your views by slightly modifying the PermittedParams class.
# app/models/permitted_params.rb
class PermittedParams < Struct.new(:params, :user)
def post
params.require(:post).permit(*post_attributes)
end
def post_attributes
if user && user.admin?
[:title, :text, :date, :user_id]
else
[:title, :text, :date]
end
end
end
and exposing the permitted_params method as a view helper.
# app/controllers/application_controller.rb
def permitted_params
#permitted_params ||= PermittedParams.new(params, current_user)
end
helper_method :permitted_params
Finally, use it within your view to show/hide the form fields.
# app/views/posts/edit.html.erb
<% if permitted_params.post_attributes.include? :user_id %>
# show the user_id field
<% end %>
You don't need to go for two different params list just to prevent bad users.
Rather you should consider using cancan Gem.
Keep using the single params list uniformly(irrespective of User role).
Upon that to restrict/protect your actions (based on user role) , try using cancan gem.
Define your individual roles authorizations in the ability.rb file.
A sample ability file will look like :
#ability.rb
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new
if user.is?(:Administrator) # Access for Admin user
can :access, :all
elsif user.is?(:"Finance Manager") #Access for Finance Manager
can :access, [:subjects, :researchsubjects, :visits, :sessions]
elsif user.is?(:"Research Director") # Access for Research Director
else
#do something
end
end
end
Please check, cancan gem for more info.
I wasn't really happy with any of the previous answers. What I really wanted to do was this:
params.require(:post).permit(:title, :text, :date, user_id: admin?)
So I decided to make that possible. First I tried to patch the Rails core, but they weren't interested in accepting the patch:
This API is delicate, there are several directions in which it could evolve and it can get easily out of hand, inconsistent, or create expectations for extending the extensions... We prefer by now to keep it as it is.
In general, we prefer use cases not directly supported by the API to be addressed by regular programming. In this case, it would be
pkeys = [:title, :body]
pkeys << :author_id if admin?
params.require(:post).permit(*pkeys)
or something in that line (you already knew that was possible of course, but just to illustrate the point with an example).
So I turned it into a gem instead, and I'll have it for as long as I want to keep it. It's a shame this feature won't be part of every future Rails app I write, but at least I'll be able to get it from my Gemfile.
gem 'rails_conditional_params'

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!

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

ActiveRecord::ReadOnlyRecord when using ActiveAdmin and Friendly_id

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

Virtual attributes in plugin

I need some help with virtual attributes. This code works fine but how do I use it inside a plugin. The goal is to add this methods to all classes that uses the plugin.
class Article < ActiveRecord::Base
attr_accessor :title, :permalink
def title
if #title
#title
elsif self.page
self.page.title
else
""
end
end
def permalink
if #permalink
#permalink
elsif self.page
self.page.permalink
else
""
end
end
end
Thanks
You can run the plugin generator to get started.
script/generate plugin acts_as_page
You can then add a module which defines acts_as_page and extends it into all models.
# in plugins/acts_as_page/lib/acts_as_page.rb
module ActsAsPage
def acts_as_page
# ...
end
end
# in plugins/acts_as_page/init.rb
class ActiveRecord::Base
extend ActsAsPage
end
This way the acts_as_page method is available as a class method to all models and you can define any behavior into there. You could do something like this...
module ActsAsPage
def acts_as_page
attr_writer :title, :permalink
include Behavior
end
module Behavior
def title
# ...
end
def permalink
# ...
end
end
end
And then when you call acts_as_page in the model...
class Article < ActiveRecord::Base
acts_as_page
end
It will define the attributes and add the methods. If you need things to be a bit more dynamic (such as if you want the acts_as_page method to take arguments which changes the behavior) try out the solution I present in this Railscasts episode.
It appears that you want a Module for this
# my_methods.rb
module MyMethods
def my_method_a
"Hello"
end
end
The you want to include it into the classes you want to use it for.
class MyClass < ActiveRecord::Base
include MyMethods
end
> m = MyClass.new
> m.my_method_a
=> "Hello!"
Take a look here for more information on mixing in modules. You can put the module wherever in a plugin if you like, just ensure its named correctly so Rails can find it.
Create a module structure like YourPlugin::InstanceMethods and include it this module like this:
module YourPlugin
module InstanceMethods
# your methods
end
end
ActiveRecord::Base.__send__(:include, YourPlugin::InstanceMethods)
You have to use __send__ to make your code Ruby 1.9 compatible. The __send__ line is usually placed at the init.rb file on your plugin root directory.

Resources