How to refactor standard methods calls? - ruby-on-rails

Need idea how to refactor the code:
attr_accessor :product
attr_reader :name, :mark
def name=(value)
super unless product.present?
end
def mark=(value)
super unless product.present?
end
... and a whole bunch of method of sorts.
The idea is simple - to deny assigning values if a product is already set. But the code above isn't DRY at all.
Any ideas?

attr_accessor :product
attr_reader :name, :mark
["name", "mark"].each do |method|
define_method("#{method}=") do |value|
super(value) unless product.present?
end
end

You can create a method such as attr_* to handle this. This is done by reopening the Class class and defining the following method.
def attr_validator(*args)
#We simply iterate through each passed in argument...
args.each do |arg|
# Create an attr_reader
self.class_eval("
def #{arg};
##{arg};
end
")
# Here we hardcode "product" since this every attribute
# checks itself against this object
self.class_eval("
def #{arg}=(val);
super unless product.present?;
end
")
end
end
This way, we get rid of the redundancy of appending attributes to the pre-processor method (in my case, attr_validator) and also a different array for metaprogramming purposes.
This can be used thus...
attr_accessor :product
attr_validator :name, :mark, :price, :stock # ...and so on

Related

How do a custom method applicable to various models

I have the following method called capitalizeEachWord. Inside this method there is an attribute called company
class BusCompany < ActiveRecord::Base
attr_accessible :company
before_save :capitalizeEachWord
validates :company,presence: true,
uniqueness: { case_sensitive: false },
format: /^([a-zA-z0-9]+\s?){1,}$/
def capitalizeEachWord
self.company=self.company.downcase.split.map(&:capitalize).join(' ')
end
end
I would like that this method not use the attribute company directly, but receives this attribute as a parameter for doesn't do it dependent of the model BusCompany. Something as the following. The problem is that this method I going to use in various models and don't want to write it in each model but use the inheritance
class BusCompany < ActiveRecord::Base
attr_accessible :company
before_save :capitalizeEachWord(self.company)
validates :company,presence: true,
uniqueness: { case_sensitive: false },
format: /^([a-zA-z0-9]+\s?){1,}$/
def capitalizeEachWord(attribute)
self.attribute=self.attribute.downcase.split.map(&:capitalize).join(' ')
end
end
Add the following code into config/initializers/capitalizer.rb
module Capitalizer
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def capitalize(*attributes)
#attributes_to_capitalize = attributes
before_save :capitalize_each_word
end
def attributes_to_capitalize
Array.new(#attributes_to_capitalize)
end
end
def capitalize_each_word
self.class.attributes_to_capitalize.each do |attr|
if value = send(attr)
self.send("#{attr}=", value.strip.titleize)
end
end
end
end
And then in your class:
class BusCompany < ActiveRecord::Base
include Capitalizer
capitalize :company
...
end
First, I'd recommend you override the setter for company instead of using error prone callbacks, like this:
class BusCompany < ActiveRecord::Base
# you can also use #titleize instead of capitalize each word
# also use try, in case `arg` is nil
def company=(arg)
super arg.try(:titleize)
end
end
Then you can use modules to wrap this functionality into a reusable unit. Throw this in a file in your concerns folder, or just in to the models folder:
module CapitalizedSetter
def capitalize_setter(*attr_names)
# for each attr name, redifine the setter so it supers the titleized argument instead
attr_names.each do |attr|
define_method(:"#{attr}=") { |arg| super arg.try(:titleize) }
end
end
end
Finally extend it into the desired models:
class BusCompany
extend CapitalizedSetter
capitalized_setter :company
end

Rails validation - confirming presence of form attribute that is not saved in model

My application has a form_for tag with element :foo that is not saved in the model for the object used in form_for.
I need to confirm that the user submitted a value for this element, using Rails Validation Helpers. However, the 'presence' validator makes a call to object.foo to confirm that it has a value. Since foo is not saved as part of my object, how can I do this validation?
Thanks!
You should probably check for the presence of it in the params in your controller action:
def create
#model = MyModel.find(params[:id])
unless params[:foo].present?
#model.errors.add(:foo, "You need more foo")
end
# ...
end
If :foo is an attribute of your object that isn't saved in the database and you really want to use ActiveRecord Validations, you can create an attr_accessor for it, and validate presence like this.
class MyModel < ActiveRecord::Base
attr_accessor :foo
validates :foo, presence: true
end
But that could result in invalid records being saved, so you probably don't want to do it this way.
Try this..
class SearchController < ApplicationController
include ActiveModel::ForbiddenAttributesProtection
def create
# Doesn't have to be an ActiveRecord model
#results = Search.create(search_params)
respond_with #results
end
private
def search_params
# This will ensure that you have :start_time and :end_time, but will allow :foo and :bar
params.require(:val1, :foo).permit(:foo, :bar , whatever else)
end
end
class Search < ActiveRecord::Base
validates :presence_of_foo
private
def presence_of_foo
errors.add(:foo, "should be foo") if (foo.empty?)
end
end
See more here

Single Table Inheritance in Rails 4

I'm trying to implement a somewhat simple STI in Rails 4, but there's something I can't yet manage to achieve.
I have the following classes:
class Person < ActiveRecord::Base
end
class NaturalPerson < Person
end
class LegalPerson < Person
end
class Employee < NaturalPerson
end
class Customer < NaturalPerson
end
The thing is, I have some attributes that I want to access only from the Employee class, some only from Customer, etc, but I can't find the way. If I were to be using Rails 3's way I would've solved it with attr_accesible. But this isn't posible now, since I'm neither using the attr_accesible gem, nor I'm willing to.
I woud use different person_params in my controller,
def person_params
params.require(:person).permit(:email, :last_name, :first_name)
end
def natural_person_params
params.require(:person).permit(:email, :job, :location)
end
and create a method where I would test the class name of object or the type attribute as it is a STI) to determine which params to use...
Hope this helps
Cheers
If you're trying to use a single controller for all of the models, then put ALL the attributes into the white listed params.
def person_params
params.require(:person).permit(:email, :last_name, :first_name, :job, :location)
end
If you want to separate them, then you'll want separate params for each type of person:
def person_params
params.require(:person).permit(:email, :last_name, :first_name)
end
def employed_person_params
params.require(:person).permit(:email, :job, :location)
end

Accepts Nested Attributes & Filters

Let's say I have two models; Post & Comment
class Post < ActiveRecord::Base
has_many :comments
accepts_nested_attributes_for :comments
end
class Comment < ActiveRecord::Base
belongs_to :post
before_save :do_something
def do_something
# Please, let me do something!
end
end
I have a form for Post, with fields for comments. Everything works as expected, except for the filter. With the above configuration, before_save filter on Comment isn't triggered.
Could you explain why, and how I can fix this?
Rails doesn't instantiate and save the comments individually in this case. You would be better off adding a callback in your Post model to handle this for nested comments:
class Post < AR::Base
before_save :do_something_on_comments
def do_something_on_comments
comments.map &:do_something
end
end
According to Bryan Helmkamp, it's better to use the form object pattern than it is to use accepts_nested_attributes_for. Take a look at 7 Patterns to Refactor Fat ActiveRecord Models
Maybe you could do something like this?
class NewPost
include Virtus
extend ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
attr_reader :post
attr_reader :comment
# Forms are never themselves persisted
def persisted?
false
end
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
#post = Post.create!
#comment = #post.comment.create!
end
end
do_something would get called when you create the comment.

Rails ActiveRecord: validate single attribute

Is there any way I can validate a single attribute in ActiveRecord?
Something like:
ac_object.valid?(attribute_name)
You can implement your own method in your model. Something like this
def valid_attribute?(attribute_name)
self.valid?
self.errors[attribute_name].blank?
end
Or add it to ActiveRecord::Base
Sometimes there are validations that are quite expensive (e.g. validations that need to perform database queries). In that case you need to avoid using valid? because it simply does a lot more than you need.
There is an alternative solution. You can use the validators_on method of ActiveModel::Validations.
validators_on(*attributes) public
List all validators that are being used to validate a specific
attribute.
according to which you can manually validate for the attributes you want
e.g. we only want to validate the title of Post:
class Post < ActiveRecord::Base
validates :body, caps_off: true
validates :body, no_swearing: true
validates :body, spell_check_ok: true
validates presence_of: :title
validates length_of: :title, minimum: 30
end
Where no_swearing and spell_check_ok are complex methods that are extremely expensive.
We can do the following:
def validate_title(a_title)
Post.validators_on(:title).each do |validator|
validator.validate_each(self, :title, a_title)
end
end
which will validate only the title attribute without invoking any other validations.
p = Post.new
p.validate_title("")
p.errors.messages
#=> {:title => ["title can not be empty"]
note
I am not completely confident that we are supposed to use validators_on safely so I would consider handling an exception in a sane way in validates_title.
I wound up building on #xlembouras's answer and added this method to my ApplicationRecord:
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
def valid_attributes?(*attributes)
attributes.each do |attribute|
self.class.validators_on(attribute).each do |validator|
validator.validate_each(self, attribute, send(attribute))
end
end
errors.none?
end
end
Then I can do stuff like this in a controller:
if #post.valid_attributes?(:title, :date)
render :post_preview
else
render :new
end
Building on #coreyward's answer, I also added a validate_attributes! method:
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
def valid_attributes?(*attributes)
attributes.each do |attribute|
self.class.validators_on(attribute).each do |validator|
validator.validate_each(self, attribute, send(attribute))
end
end
errors.none?
end
def validate_attributes!(*attributes)
valid_attributes?(*attributes) || raise(ActiveModel::ValidationError.new(self))
end
end

Resources