Rails + Active Admin: Display name - ruby-on-rails

I am using Active Admin with Ruby on Rails and I am having an issue with the way that some models are shown in the panel.
Taking the class User as an example, if I do not define any method to display it friendly, I see #<User:00000006b47868>. So Active Admin suggests implementing a method to specify, for each class, how to show it.
According to the documentation (http://activeadmin.info/docs/3-index-pages/index-as-table.html), Active Admin looks for one of these methods to guess what to display, in the following order:
:display_name, :full_name, :name, :username, :login, :title, :email, :to_s
So having this method within the User class would solve the problem:
def display_name
return self.id.to_s + '-' + self.full_name
end
However, before using Active Admin, I was already using the method display_name with other purposes (for example, in views) in order to show the user name in a friendly way, and I do not want to show the same in Active Admin panel.
I cannot change the name of the method because I use display_name in a lot of files along the project, and changing it would probably introduce bugs in the application.
An ideal solution for this case would be to have something like an active_admin_name method that is used by Active Admin to show models in its panel. So the question is:
Is there any way to have a method that is called by Active Admin instead of display_name? For example, to result in the following order:
:active_admin_name, :display_name, :full_name, :name, :username, :login, :title, :email, :to_s
I have searched in Active Admin documentation and in config/initializers/active_admin.rb, but I could not find a way to do it.
Thanks!

Try
ActiveAdmin.setup do |config|
config.display_name_methods = [:active_admin_name, :display_name ...]
end
You can find this setting in lib/active_admin/application.rb

You could also use :title key to define it for each page, like this:
show(title: 'Something') do |record|
...
end
# or with a Proc
show(title: ->(record) { record.another_method_to_display }) do |record|
...
end

Related

Rails ActiveAdmin modify resource object

I've currently got a user object but to avoid redundancy, I'd like to wrap it into a presenter object called MerchantUser/ProviderUser. However, with ActiveAdmin, I'm a little confused on how to do this. I've tried using before_create to change the user into the corresponding presenters but in index...do, I'm still seeing that user.class is equal to User and not the wrapper classes that I've defined.
I've looked into scoping_collection but unfortunately that only works on collections and not individual objects?
ActiveAdmin.register User, as: "Companies" do # rubocop:disable Metrics/BlockLength
before_create do |user|
if user.merchant?
user = MerchantUser.new(user)
else
user = ProviderUser.new(user)
end
end
actions :all, except: [:destroy]
permit_params :name, :email, contract_attributes: [:id, :flat_rate, :percentage]
filter :role, as: :select
index do # rubocop:disable Metrics/BlockLength
column :name do |user|
user.name <---I want it so I can just do this without the if/else blocks like below.
end
column :role
column :contact_phone
column :email
column :website do |user|
if user.merchant?
user.company.website
else
user.provider.website
end
end
column :flat_rate do |user|
money_without_cents_and_with_symbol(user.contract.flat_rate)
end
column :percentage do |user|
number_to_percentage(user.contract.percentage, precision: 0)
end
actions
end
Have you looked into Active Admin's support for decorators? This page is quite comprehensive. The best way to implement them depends on how your decorator/presenter object is implemented.
Link summary: use decorate_with or look into using this gem for PORO support
Are you sure you want/need a presenter here? You can register the same Rails model multiple times as ActiveAdmin resources with different names and customizations (filters, index page, forms, etc). You can also use Rails STI or just subclass Rails models, perhaps with different Rails default_scope and then register the subclasses.

FriendlyID for child

I have a Project model that belongs to a User. A User has_many Projects. I have setup FriendlyID according to the gem instructions for User model, however it is not working in this scenario.
<%= link_to gravatar_for(project.user, size: 50), project.user %>
The above creates a link with the numerical ID of the user (project.user) instead of a friendly URL (i.e. http://localhost:3000/users/102 instead of what I want, http://localhost:3000/users/*random-gen-string*).
User.rb file:
class User < ApplicationRecord
extend FriendlyId
friendly_id :generated_slug, use: :slugged
def generated_slug
require 'securerandom'
#random_slug ||= persisted? ? friendly_id : SecureRandom.hex(15)
end
I think the problem is that project.user is set in the projects_controller.rb to the user ID (via current_user.projects.build...). If I could somehow access the Friendly ID for User in the projects_controller.rb I may be able to save it in the database and access it like project.user_friendly_id. Not sure..
Relevant code in projects_controller:
def create
#project = current_user.projects.build(project_params)
# code
end
What is the best way to go about making the above link link to the Friendly ID and not the user (i.e. http://localhost:3000/users/*random-gen-string* is what I want instead of http://localhost:3000/users/102)?
Update:
As discussed in the chatroom, User.find_each(&:save!) reveals the errors when saving the User model. In the above case, the lack of password input was preventing the User records from being saved. Removing the validation temporarily allowed saving the User and thus regenerating slugs.
(Original answer left for history)
You can override the to_param method in your User model like this
class User < ApplicationRecord
# some code
def to_param
slug
end
And then that is used to generate the link. More on that in the guides.
When you build your link_to you can call the user.slug to ensure that you get the proper info
<%= link_to gravatar_for(project.user, size: 50), project.user.slug %>
that will generate the link that you are expecting http://localhost:3000/users/random-gen-string
Here is an example of cities with increasing order of specificity.
def slug_candidates
[
:name,
[:name, :city],
[:name, :street, :city],
[:name, :street_number, :street, :city]
]
end
In case of conflicting slugs it is better to use user-friendly names then UUID (e.g. 2bc2-d3dd-4f29-b2ad)

How do I validate certain fields with rails devise on registration only

I have a set of custom fields attached to a devise model called Entrant.
I have two forms, one for registration form (three fields) and one which sits in the account area (12 fields). Most of the custom fields area required but only within the form the sits in the account area.
How do I achieve this?
I am using rails 4.2 and ruby 2.1
You can simply specify validations on actions, that is:
validates :name, presence: true, on: :create # which won't validate presence of name on update action
If you ask where to put your custom fields, then generate devise's views and update corresponding ones with these fields.
There are several ways! You could do conditional validations, for instance
class Entrant < ActiveRecord::Base
validate :foo, if: :account_area?
def account_area?
!new_record? # Assumes that Entrant that has already been saved
# is in the account area
end
end
However, it sounds like your needs are advanced enough that you should consider making a Form Object
A form object is an object that accepts parameters, performs validations on that data, then saves a model instance.
class AccountForm
include ActiveModel::Model
include Virtus # Provides AR like attribute functionality and mass assignment
def initialize(entrant)
#entrant = entrant
end
attribute :foo, String
validates :foo, presence: true # This is only used on the account page, so no need to mess with conditional logic
def save
if valid?
persist!
true
else
false
end
end
def persist!
#entrant.update_attributes(foo: self.foo)
end
end
This is just a great example of how non-rails-specific object oriented programming can make your life easier and your app more maintainable. Make a class like above, stick it in app/forms and restart your server. Then in your controller, you'll just pass it the model
class EntrantController < ApplicationController
def update
#form = Form.new(Entrant.find(params[:id]))
#form.attributes = params[:entrant]
if #form.save
redirect_to some_path
else
render "edit"
end
end
end
By default devise only asks for a combination of email/password, you can add other fields by adding a sanitizer (see there -> Devise how to add a addtional field to the create User form?).
If you want to add other fileds to validate, you should create a secondary Entrant controller and add a specific callback to your model.
Typically:
after_update :validate_entrant_form, if: :property_changed?
I hope this will help you.
validates :name, presence: true, if: :condition_holds?
def condition_holds?
# some code here that evaluates to a boolean
end
Maybe this way help you.
Add attribute in devise model : say attr_accessor :validate_certain. In your controller action, devise model instance say #user have to update like this #user.validate_certain = true. and change your appropriate validation conditions in devise model
validates :name, presence: true, if: :validate_certain_changed?
def validate_certain_changed?
validate_certain.present?
end
When I have to do something like this I like to think of it as it validates if something in in the field but you can also take a nil value
Entrant.validates_presence_of(:foo, :allow_nil => true)
I also have this concern when using devise on customer with forms on separate pages updating different set of customer fields
I believe most of the solution works but I was looking for the simplest, easiest and foolproof way to implement the solution
Thus came this.
validates :phone, :country, :postal_code, :street_address, presence: true, allow_nil: true
The allow_nil: true instruct the model to validate the fields ONLY if it exists on the submitted form. If you want more protection, you can use extra para like :on => :update

Can't mass-assign protected attributes for creating a has_many nested model with Devise

I've watched the RailsCast, another nested attributes video, lots of SO posts, and fought with this for a while, but I still can't figure it out. I hope it's something tiny.
I have two models, User (created by Devise), and Locker (aka, a product wishlist), and I'm trying to create a Locker for a User when they sign up. My login form has a field for the name of their new Locker (aptly called :name) that I'm trying to assign to the locker that gets created upon new user registration. All I'm ever greeted with is:
WARNING: Can't mass-assign protected attributes: locker
I've tried every combination of accepts_nested_attributes and attr_accesible in both of my models, yet still nothing works. I can see from the logs that it's being processed by the Devise#create method, and I know Devise isn't smart enough to create my models how I want :)
Here's the relevant bits of my two models:
# user.rb
class User < ActiveRecord::Base
attr_accessible :username, :email, :password, :password_confirmation, :remember_me, :locker_attributes
# Associations
has_many :lockers
has_many :lockups, :through => :lockers
# Model nesting access
accepts_nested_attributes_for :lockers
end
and
# locker.rb
class Locker < ActiveRecord::Base
belongs_to :user
has_many :lockups
has_many :products, :through => :lockups
attr_accessible :name, :description
end
# lockers_controller.rb (create)
#locker = current_user.lockers.build(params[:locker])
#locker.save
I'm assuming I need to override Devise's create method to somehow get this to work, but I'm quite new to rails and am getting used to the black box "magic" nature of it all.
If anyone can help me out, I'd be incredibly thankful. Already spent too much time on this as it is :)
EDIT: I realized I omitted something in my problem. My Locker model has three attributes - name, description (not mandatory), and user_id to link it back to the User. My signup form only requires the name, so I'm not looping through all the attributes in my nested form. Could that have something to do with my issue too?
EDIT 2: I also figured out how to override Devise's RegistrationsController#create method, I just don't know what to put there. Devise's whole resource thing doesn't make sense to me, and browsing their source code for the RegistrationsController didn't help me much either.
And for bonus points: When a user submits the login form with invalid data, the Locker field always comes back blank, while the regular Devise fields, username & email, are filled in. Could this also be fixed easily? If so, how?
first, you have a typo :
attr_accessible :locker_attributes
should be plural :
attr_accessible :lockers_attributes
then, the standard way to use nested_attributes is :
<%= form_for #user do |f| %>
<%# fields_for will iterate over all user.lockers and
build fields for each one of them using the block below,
with html name attributes like user[lockers_attributes][0][name].
it will also generate a hidden field user[lockers_attributes][0][id]
if the locker is already persisted, which allows nested_attributes
to know if the locker already exists of if it must create a new one
%>
<% f.fields_for :lockers do |locker_fields| %>
<%= locker_fields.label :name %>
<%= locker_fields.text_field :name %>
<% end %>
<% end %>
and in you controller :
def new
#user = User.new
#user.lockers.build
end
def create
# no need to use build here : params[:user] contains a
# :lockers_attributes key, which has an array of lockers attributes as value ;
# it gets assigned to the user using user.lockers_attributes=,
# a method created by nested_attributes
#user = User.new( params[:user] )
end
as a side note, you can avoid building a new locker for new users in controller in different ways:
create a factory method on User, or override new, or use an after_initialize callback to ensure every new user instantiated gets a locker builded automatically
pass a specific object to fields_for :
<% f.fields_for :lockers, f.object.lockers.new do |new_locker_fields| %>
Someone helped me figure out the solution in a more "Rails 4'y" way with strong attributes & how to override Devise's sign_up_params (to catch all the data coming from my signup form).
def sign_up_params
params.require(:user).permit(:username, :email, :password, :lockers_attributes)
end
Gemfile addition: gem 'strong_parameters'
Commenting out the attr_accessible statement in my user.rb file, since apparently strong parameters eliminate the need for attr_accessible declarations.
# attr_accessible :username, :email, :password, :password_confirmation, :lockers
And the/a correct way of building a Locker before submitting the form: at the beginning of the nested form:
<%= l.input :name, :required => true, label: "Locker name", :placeholder => "Name your first locker" %>
Thanks again for all your help. I know a question like this is difficult to answer without seeing the whole codebase.

Mass-assignment issue

Please, explain, how can I make records in database with several methods, changing attributes I DON'T want to be attr_accessible.
For example, in User Model:
attr_accessible :email, :password, :password_confirmation, :guest
I don't want 'admin' true or false row be placed here because of security issue
You can assign the attribute manually, for example if your model is named User you can do the following :
user = User.first
user.update_attributes(attributes_hash)
user.admin = true
user.save
attr_accessible is used only for mass assignment via update_attributes for example, but you can always assign a specific property by calling it directly like in my example above.
I think you may be asking how to change the admin attribute in your test or development environment without adding it to attr_accessable.
One way would be the toggle method. For example,
user = User.first
user.admin?
=> false
user.toggle!(:admin)
user.admin?
=> true
A couple of things about toggle to consider. The attribute must be passed as a symbol and all callbacks and validations are skipped. In other words use caution for anything outside of testing and development. So this is how you can mass-assign the admin attribute without adding it to attr_accessable.

Resources