Dynamically determining Rails model attributes - ruby-on-rails

I have a model (simplified version below - the full one has a lot more fields) on which I want to do a search.
class Media < ActiveRecord::Base
attr_accessible :subject, :title, :ref_code
validates :title, :presence => true
validates :subject, :presence => true
def self.search(terms_hash)
return if terms_hash.blank?
composed_scope = self.scoped
terms_hash.each_pair do |key, value|
if self.respond_to? key and not value.is_blank?
value.split(' ').each do |term|
term = "%#{term}%"
composed_scope = composed_scope.where("#{key} LIKE ?", term)
end
end
composed_scope
end
end
Since the advanced search form is almost identical to the form used to create/update instances of the model, I want to create a method that dynamically looks through the params list of the request and matches form fields to model attributes via the name. So if the search request was,
/search?utf8=✓&ref_code=111&title=test&subject=code&commit=Search
then my controller could just call Media.search(params) and it would return a scope that would search the appropriate fields for the appropriate value(s). As you can see I tried using respond_to? but that is only defined for instances of the model rather than the actual model class and I need something that ignores any params not related to the model (eg. utf8 and commit in this case). If this all works well, I plan to refactor the code so that I can reuse the same method for multiple models.
Is there a class method similar to respond_to?? How would you go about the same task?
I know this is related to get model attribute dynamically in rails 3 but it doesn't really answer what I'm trying to do because I want to do it on the the model rather than an instance of the model.

There is a columns method on ActiveRecord model classes so you can easily get a list of attribute names:
names = Model.columns.map(&:name)
You could remove a few common ones easily enough:
names = Model.columns.map(&:name) - %w[id created_at updated_at]
The accessible_attributes class method might also be of interest, that will tell you what has been given to attr_accessible.

Related

Rails 5 re-use before_validation code in multiple models with different fields

I have a Rails 5 application where users can enter currency values in different fields and different models.
Since I only serve one locale, I want users to be able to enter decimals using both . and , decimal separators and ignore any thousands separator.
For example, my users might enter: 1023.45 or 1023,45 but never 1.023,45.
Simple solution that fits my use case: On specific decimal fields representing currency, replace , with . using gsub(',', '.').
What is the best place to put this code? There are multiple models with differently named fields that need to use this code.
Preferably, I would use something like a custom validator that I create once and simply reference with 1 line from all models with the appropriate field. Very much like this example of a custom validator that checks whether an entered value is positive:
class PositiveValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if value and BigDecimal.new(value).negative?
record.errors[attribute] << 'is negative'
end
end
end
And then in the model reference it with:
validates :total, positive: true, allow_blank: true
However, this is ofcourse for validations, which shouldn't modify data. I tried to do this with concerns, but from the examples I have seen I should already know which fields are being transformed.
Do I really need to write in each model something like this:
before_validation lambda {self.total.gsub!(',', '.') if self.total}
or is there a more elegant Rails-like solution?
edit: again, note that field name 'total' might be different in each model
You could use custom type-casting starting from Rails 4.2
class LocalFloat < ActiveRecord::Type::Float
def cast_value(value)
value = value.gsub(',', '.') if value.is_a? String
super(value)
end
end
class Order < ApplicationRecord
attribute :total, LocalFloat.new
end
o = Order.new total: '5,5'
o.total # => 5.5

How should I store a collection for an attribute in Rails?

I have MyModel which has attribute category. There are only two possible categories Category 1 and Category 2. The attributes are assigned using a form as follows:
<%= f.input :category, collection: category_options %>
What is considered "good practice" in Rails. Should I save the attributes as a string in the db, or should I create a new table / reference for the collection?
Storing the category as a string has the benefit that it keeps the db clean, but will have to store the collections seperately in the controller. Also, since I'm using i18n, I would expect that storing the category as a string will lead to translation issues.
If according to business logic you are planning to have some attributes, methods or other things for each of your categories, you should create another model for Category. It would be less hard to implement your future needs in terms of managing complexity. If you are 100% sure that there will be no more extras about categories, you should make it an attribute with addition of some validations in your code(inclusion in category1, category2, for example).
For simple cases, I usually use something like this. You can i18n the UI choices, using the internal string as the key.
# View Code:
# <%= form.select :role, MyModel.categories, prompt: '' -%>
class MyModel < ActiveRecord::Base
validates :category,
presence: true,
inclusion: {
in: :categories,
allow_blank: true }
class << self
def categories
%w[hot warm cold]
end
end
def categories
self.class.categories
end
end

Custom validation method in Rails that receives whole hash of fields

I need a custom validation method that receives all set fields. I need this because I dynamically add multiple fields for a has_many :through relation in a form and I need to make sure there aren't any duplicate values entered.
I know that regular Rails validation methods operate on a single field, but I couldn't find anything to handle all fields.
From the docs, you can do this:
class Person < ActiveRecord::Base
validates_each :name, :surname, :any_other_attributes, do |record, attr, value|
...
# e.g. record.errors.add(attr, 'must start with upper case') if value =~ /\A[a-z]/
end
end

When overriding an ActiveRecord Model, and defining a new attr_accessible, how do I add without duplicating?

When I am using a Rails Engine, and desiring to override and add to its behavior, I have faced the following problem:
Say the Engine has a ActiveRecord model named Course
module MyEngine
class Course < ActiveRecord::Base
attr_accessible :name, :description, :price
end
end
And I want to create a migration in my main Rails app, to add a column to it, and I need to add that new column to the attr_accessible (so it can be mass assigned)
MyEngine::Course.class_eval do
attr_accessible :expiration_date
end
But then Rails complains that the first 3 attrs are not Mass-Assignable, so instead of just "adding" the new attribute to the override, I have to re-declare all the attributes in the overridden class, like:
MyEngine::Course.class_eval do
attr_accessible :name, :description, :price, :expiration_date
end
Is there a better way to not re-declare these attributes, and just add the new attribute?
By looking at the source code:
# File activemodel/lib/active_model/mass_assignment_security.rb, line 174
def attr_accessible(*args)
options = args.extract_options!
role = options[:as] || :default
self._accessible_attributes = accessible_attributes_configs.dup
Array.wrap(role).each do |name|
self._accessible_attributes[name] = self.accessible_attributes(name) + args
end
self._active_authorizer = self._accessible_attributes
end
You could try to use one of the internal data structures to recover the attributes that have already been defined, so you don´t duplicate code, or you can hack your way and create a new method that let´s you "append" values to attr_accessible. But there is no code baked for that yet.

Difference between attr_accessor and attr_accessible

In Rails, what is the difference between attr_accessor and attr_accessible? From my understanding, using attr_accessor is used to create getter and setter methods for that variable, so that we can access the variable like Object.variable or Object.variable = some_value.
I read that attr_accessible makes that specific variable accessible to the outside world.
Can someone please tell me whats the difference
attr_accessor is a Ruby method that makes a getter and a setter. attr_accessible is a Rails method that allows you to pass in values to a mass assignment: new(attrs) or update_attributes(attrs).
Here's a mass assignment:
Order.new({ :type => 'Corn', :quantity => 6 })
You can imagine that the order might also have a discount code, say :price_off. If you don't tag :price_off as attr_accessible you stop malicious code from being able to do like so:
Order.new({ :type => 'Corn', :quantity => 6, :price_off => 30 })
Even if your form doesn't have a field for :price_off, if it's in your model it's available by default. This means a crafted POST could still set it. Using attr_accessible white lists those things that can be mass assigned.
Many people on this thread and on google explain very well that attr_accessible specifies a whitelist of attributes that are allowed to be updated in bulk (all the attributes of an object model together at the same time)
This is mainly (and only) to protect your application from "Mass assignment" pirate exploit.
This is explained here on the official Rails doc : Mass Assignment
attr_accessor is a ruby code to (quickly) create setter and getter methods in a Class. That's all.
Now, what is missing as an explanation is that when you create somehow a link between a (Rails) model with a database table, you NEVER, NEVER, NEVER need attr_accessor in your model to create setters and getters in order to be able to modify your table's records.
This is because your model inherits all methods from the ActiveRecord::Base Class, which already defines basic CRUD accessors (Create, Read, Update, Delete) for you.
This is explained on the offical doc here Rails Model and here Overwriting default accessor (scroll down to the chapter "Overwrite default accessor")
Say for instance that: we have a database table called "users" that contains three columns "firstname", "lastname" and "role" :
SQL instructions :
CREATE TABLE users (
firstname string,
lastname string
role string
);
I assumed that you set the option config.active_record.whitelist_attributes = true in your config/environment/production.rb to protect your application from Mass assignment exploit. This is explained here : Mass Assignment
Your Rails model will perfectly work with the Model here below :
class User < ActiveRecord::Base
end
However you will need to update each attribute of user separately in your controller for your form's View to work :
def update
#user = User.find_by_id(params[:id])
#user.firstname = params[:user][:firstname]
#user.lastname = params[:user][:lastname]
if #user.save
# Use of I18 internationalization t method for the flash message
flash[:success] = t('activerecord.successful.messages.updated', :model => User.model_name.human)
end
respond_with(#user)
end
Now to ease your life, you don't want to make a complicated controller for your User model.
So you will use the attr_accessible special method in your Class model :
class User < ActiveRecord::Base
attr_accessible :firstname, :lastname
end
So you can use the "highway" (mass assignment) to update :
def update
#user = User.find_by_id(params[:id])
if #user.update_attributes(params[:user])
# Use of I18 internationlization t method for the flash message
flash[:success] = t('activerecord.successful.messages.updated', :model => User.model_name.human)
end
respond_with(#user)
end
You didn't add the "role" attributes to the attr_accessible list because you don't let your users set their role by themselves (like admin). You do this yourself on another special admin View.
Though your user view doesn't show a "role" field, a pirate could easily send a HTTP POST request that include "role" in the params hash. The missing "role" attribute on the attr_accessible is to protect your application from that.
You can still modify your user.role attribute on its own like below, but not with all attributes together.
#user.role = DEFAULT_ROLE
Why the hell would you use the attr_accessor?
Well, this would be in the case that your user-form shows a field that doesn't exist in your users table as a column.
For instance, say your user view shows a "please-tell-the-admin-that-I'm-in-here" field.
You don't want to store this info in your table. You just want that Rails send you an e-mail warning you that one "crazy" ;-) user has subscribed.
To be able to make use of this info you need to store it temporarily somewhere.
What more easy than recover it in a user.peekaboo attribute ?
So you add this field to your model :
class User < ActiveRecord::Base
attr_accessible :firstname, :lastname
attr_accessor :peekaboo
end
So you will be able to make an educated use of the user.peekaboo attribute somewhere in your controller to send an e-mail or do whatever you want.
ActiveRecord will not save the "peekaboo" attribute in your table when you do a user.save because she don't see any column matching this name in her model.
attr_accessor is a Ruby method that gives you setter and getter methods to an instance variable of the same name. So it is equivalent to
class MyModel
def my_variable
#my_variable
end
def my_variable=(value)
#my_variable = value
end
end
attr_accessible is a Rails method that determines what variables can be set in a mass assignment.
When you submit a form, and you have something like MyModel.new params[:my_model] then you want to have a little bit more control, so that people can't submit things that you don't want them to.
You might do attr_accessible :email so that when someone updates their account, they can change their email address. But you wouldn't do attr_accessible :email, :salary because then a person could set their salary through a form submission. In other words, they could hack their way to a raise.
That kind of information needs to be explicitly handled. Just removing it from the form isn't enough. Someone could go in with firebug and add the element into the form to submit a salary field. They could use the built in curl to submit a new salary to the controller update method, they could create a script that submits a post with that information.
So attr_accessor is about creating methods to store variables, and attr_accessible is about the security of mass assignments.
attr_accessor is ruby code and is used when you do not have a column in your database, but still want to show a field in your forms. The only way to allow this is to attr_accessor :fieldname and you can use this field in your View, or model, if you wanted, but mostly in your View.
Let's consider the following example
class Address
attr_reader :street
attr_writer :street
def initialize
#street = ""
end
end
Here we have used attr_reader (readable attribute) and attr_writer (writable attribute) for accessing purpose. But we can achieve the same functionality using attr_accessor. In short, attr_accessor provides access to both getter and setter methods.
So modified code is as below
class Address
attr_accessor :street
def initialize
#street = ""
end
end
attr_accessible allows you to list all the columns you want to allow Mass Assignment. The opposite of this is attr_protected which means this field I do NOT want anyone to be allowed to Mass Assign to. More than likely it is going to be a field in your database that you don't want anyone monkeying around with. Like a status field, or the like.
In two words:
attr_accessor is getter, setter method.
whereas attr_accessible is to say that particular attribute is accessible or not. that's it.
I wish to add we should use Strong parameter instead of attr_accessible to protect from mass asignment.
Cheers!
A quick & concise difference overview :
attr_accessor is an easy way to create read and write accessors in
your class. It is used when you do not have a column in your database,
but still want to show a field in your forms. This field is a
“virtual attribute” in a Rails model.
virtual attribute – an attribute not corresponding to a column in the database.
attr_accessible is used to identify attributes that are accessible
by your controller methods makes a property available for
mass-assignment.. It will only allow access to the attributes that you
specify, denying the rest.

Resources