DRY-ing up ActiveAdmin - ruby-on-rails

I've got multiple resources in my ActiveAdmin installation that share quite a lot of the same traits, like:
The same or similar scopes
Equal or similar controller methods (action_methods, for example)
Similar attributes (with code blocks) in the show action
Similar attributes (with code blocks) in the edit action
What is the best way to avoid duplicating this functionality across the different resources?
I have set up decorators to avoid duplicating functionality in the index view, but I'm not sure if (and how?) this could be used in the other cases.

You can also extend your module. For example:
module AccountManageable
def has_manageable_account
permit_params :name, :email, :role, :avatar
filter :name, as: :string
filter :email, as: :string
# ... other DSL methods
end
end
and then in your admin
ActiveAdmin.register Admin do
extend AccountManageable
has_manageable_account
end

You need to extend the DSL with monkey patch:
module ActiveAdmin
# This is the class where all the register blocks are evaluated.
class ResourceDSL < DSL
def your_custom_method attr
#common code
end
end
end
Now you can use your_custom_method in your registered resource file.
https://github.com/activeadmin/activeadmin/blob/master/lib/active_admin/resource_dsl.rb

Related

How can I store a regex expression string in a helper method to use to validate several different fields?

I have several different fields that store different phone numbers in my rails app as a string. I can use the built-in rails format validator with a regex expression. Is there a way to place this expression in a helper method for all my phone number validations instead of having to open each model file and paste the expression string.
You can require any file from application.rb:
# application.rb
require Rails.root.join "lib", "regexes.rb"
# lib/regexes.rb
PHONE_NUMBER_REGEX = /your regex/
Then you simply use the constant wherever needed
You can alternatively make use of the built in autoload functionality of Rails, for example with the concern approach the other commenter laid out - the concern file is autoloaded by default, as are models, controllers, etc
Loading custom files instead of using the Rails' defaults might not seem idiomatic or the "rails way". However, I do think it's important to understand that you can load any files you want. Some people autoload the entire lib/ folder and subfolders (see Auto-loading lib files in Rails 4)
Another alternative is to place your code somewhere in the config/initializers folder, these files are automatically loaded at startup and you can define shared classes/modules/constants there
Add a custom validator app/validators/phone_validator.rb
class PhoneValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value.to_s =~ /YOUR_REGEX_HERE/
record.errors.add attribute, 'must be a valid phone number'
end
end
end
Then in your models
class MyModel < ApplicationRecord
#phone: true tells it to use the PhoneValidator defined above
validates :phone_number, presence: true, phone: true
end
One way to do this is to create a concern that will be included in each model that has a phone number. In models/concerns, create a new file called something like phonable.rb.
# models/concerns/phonable.rb
module Phonable
extend ActiveSupport::Concern
VALID_PHONE_REGEX = /\A\+?\d+\z/ # Use your regex here
end
Now include the concern like this:
# models/my_model.rb
class MyModel < ApplicationRecord
include Phonable
validates :phone, format: {with: VALID_PHONE_REGEX}
...
end
Now that you have a Phonable concern, you can put any other phone-number-related logic here as well, such as parsing and the like. The advantage of this approach is that all your logic related to phone numbers will be available for use in the models that need it, and none of that logic will be available in models that don't need it.
You can put it in ApplicationRecord (application_record.rb)
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
VALID_PHONE_NUMBER_REGEX = /\d{10}/ # your regex here
end
And then you can use it in any model that inherits fro ApplicationRecord

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.

Differences between 2 ways of Rails module included callbacks?

I'm new to Rails. I have a model called AdvItem, basically what I want to do is to move all its validation statements to a module named AdvItemValidation. After some searches here's what I get:
module AdvItemValidation
extend ActiveSupport::Concern
included do
# validations
validates :link, presence: true
validate :check_valid_link
end
def check_valid_link
...
end
end
But I've just seen another way to do this:
module AdvItemValidation
extend ActiveSupport::Concern
def self.included(base)
# validations
base.validates :link, presence: true
base.validate :check_valid_link
end
def check_valid_link
...
end
end
So what is the difference between these 2 ways of implementation? And which way is better providing that I have a lot of default and custom validation statements?
PS: For the 1st way CodeClimate reports this message "Very complex code in AdvItemValidation definition outside of methods", but imho I see it much shorter.
Thanks for the explanations.
The form below
def self.included(base)
is the default callback called when a module is included in another module and class.
The other form
included do
is provided by ActiveSupport::Concern
If you are using active support and Concerns you should preference this form. If not you will be limited to the first form anyway.
The include from Concern is largely syntactic sugar, although it does handle dependencies more gracefully.
Some further explanation here http://api.rubyonrails.org/classes/ActiveSupport/Concern.html
Well the second code snippet is ruby ruby code.
def self.included(base)
# validations
base.validates :link, presence: true
base.validate :check_valid_link
end
It has no dependency on any library.
The first piece of code, has dependency on ActiveSupport so won't work without including that library.
included do
# validations
validates :link, presence: true
validate :check_valid_link
end

Rails + Active Admin: Display name

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

How to create an object of a STI subclass using ActiveAdmin

Given the following setup(which is not working currently)
class Employee < ActiveRecord::Base
end
class Manager < Employee
end
ActiveAdmin.register Employee do
form do |f|
f.input :name
f.input :joining_date
f.input :salary
f.input :type, as: select, collection: Employee.descendants.map(&:name)
end
end
I would like to have a single "new" form for all employees and be able to select the STI type of the employee in the form.
I am able to see the select box for "type" as intended but when I hit the "Create" button, I get the following error:
ActiveModel::MassAssignmentSecurity::Error in Admin::EmployeesController#create
Can't mass-assign protected attributes: type
Now, I am aware of the way protected attributes work in Rails and I have a couple of workarounds such as defining Employee.attributes_protected_by_default but that is lowering the security and too hack-y.
I want to be able to do this using some feature in ActiveAdmin but I can't find one. I do not want to have to create a custom controller action as the example I showed is highly simplified and contrived.
I wish that somehow the controller generated by ActiveAdmin would identify type and do Manager.create instead of Employee.create
Does anyone know a workaround?
You can customize the controller yourself. Read ActiveAdmin Doc on Customizing Controllers. Here is a quick example:
controller do
alias_method :create_user, :create
def create
# do what you need to here
# then call create_user alias
# which calls the original create
create_user
# or do the create yourself and don't
# call create_user
end
end
Newer versions of the inherited_resources gem have a BaseHelpers module. You can override its methods to change how the model is altered, while still maintaining all of the surrounding controller code. It's a little cleaner than alias_method, and they have hooks for all the standard REST actions:
controller do
# overrides InheritedResources::BaseHelpers#create_resource
def create_resource(object)
object.do_some_cool_stuff_and_save
end
# overrides InheritedResources::BaseHelpers#destroy_resource
def destroy_resource(object)
object.soft_delete
end
end

Resources