Refactor some model methods in Ruby on Rails - ruby-on-rails

I have some methods in my model which I use to access stored text hashes in my views:
class CarSpec < ActiveRecord::Base
##fuel_type_select_data = Hash[(1..5).to_a.zip(['Petrol', 'Diesel', 'Gas/petrol', 'Hybrid', 'Electric'])]
##mileage_type_select_data = Hash[(1..2).to_a.zip(['km', 'miles'])]
##transmission_select_data = Hash[(1..3).to_a.zip(['Manual', 'Automatic', 'Tiptronic'])]
##wheel_drive_data = Hash[(1..3).to_a.zip(['Front', 'Rear', 'All'])]
##color_data = Hash[(1..8).to_a.zip(['black', 'white', 'beige',
'blue', 'yellow', 'green', 'red', 'silver'])]
def text_for_fuel_type
##fuel_type_select_data[fuel_type]
end
def text_for_mileage_type
##mileage_type_select_data[mileage_type]
end
def text_for_transmission
##transmission_select_data[transmission]
end
def text_for_wheel_drive
##wheel_drive_data[wheel_drive]
end
def text_for_color
##color_data[color]
end
def text_for_interior_color
##color_data[interior_color]
end
Currently, I need to write a new method for every field. How can I refactor these methods, so that I do not need to write a new method for each field? Please include how the new method/s would be called in the view.

Using constants instead of class variables would be much better in your case.
An example:
class CarSpec < ActiveRecord::Base
WHEEL_DRIVE_DATA = {'Front' => 1}
# remaining class code
end
Sample code for view:
<%= CarSpec::WHEEL_DRIVE_DATA['Front'] %>

Agree with #Humza: constants are better than class variables here.
The current set of methods can be defined dynamically - with some metaprogramming magic - as follows:
fields = %w{ fuel_type mileage_type transmission wheel_drive interior_color }
fields.each do |field|
define_method("text_for_#{field}") do
CarSpec.class_variable_get("###{field}_select_data").send(:[], eval(field))
end
end
Note:
For the above to work, the class variables for all fields will have to be consistent with the ###{field}_select_data name; so ##wheel_drive_data should be changed to ##wheel_drive_select_data, etc.

Related

What is the purpose of defining methods in a model?

What is the purpose of defining methods inside a model like the example here? What does this get me? I was under the impression that only the fields of a model are defined in the model.
class Bean
include Mongoid::Document
field :name, type: String
field :roast, type: String
field :origin, type: String
field :quantity, type: Float
has_many :pairings
# has_many :pastries
def pastries
Pastry.find pastry_ids
end
#accepts_nested_attributes_for :pastries
def pastry_ids
pastry_ids_array = []
self.pairings.each do |one_pairing|
if one_pairing.pastry_id
pastry_ids_array.push one_pairing.pastry_id
end
end
pastry_ids_array
end
def pastry_ids=(list)
self.pairings.destroy
list.each do |pastry_id|
self.pairings.create(pastry_id: pastry_id)
end
end
# some way of showing a list
def pastry_list
pastries_string = ""
pastries.each do |one_pastry|
pastries_string += ", " + one_pastry.name
end
pastries_string.slice(2,pastries_string.length - 1)
pastries_string
end
end
I don't know if you know enough ruby but let's say you don't. This is a basic Class question? Defining methods on a model it's like having an helper. Let's say that you have
class CanadianPopulation
attr_accessor :population, :number_of_french_speaker, :number_of_english_speaker
def initialize(a,b,c)
#population = a
#number_of_french_speaker = b
#number_of_english_speaker = c
end
def total_people_that_have_a_different_mother_tongue
#Canadian who speak english or french but have a different mother tongue
self.population - (self.number_of_french_speaker + self.number_of_english_speaker)
end
end
census_2014 = CanadianPopulation.new(34_000_000, 4_000_000, 12_000_000)
let's say that you didn't have the method total_people_that_have_a_different_mother_tonguehow will you do to retrieve the total number of Canadians that have a different mother tongue? you will do the caculation yourself like for a view
<p>Canadian who speak english or french but have a different mother tongue
<br>
<%= #census = #census.population - (#census.number_of_english_speaker + #census.number_of_french_speaker) %>
</p>
Your view or your controller shouldn't do much logic (calculations) so that's one of the reason why you have a method inside the model (or class) it should be like this
<p>Canadian who speak english or french but have a different mother tongue
<br>
<%= #census.total_people_that_have_a_different_mother_tongue %>
</p>
For the second part of your question what does those methods do. rails c -s on your terminal than call or create a new instance model Bean and check to see what it does (the output/results)
Bean.first
b = _
b.pastries
b.pastry_ids
b.pastry_list
edit: #paul-richer recommends to maintain a thin controller

How to create a enum type and default to a specific value for new objects

I have a model
class Transaction < ActiveRecord::Base
end
I have a transaction_type column which is an integer.
How can I create an enumeration that I could map values to names like:
one_time = 1
monthly = 2
annually = 3
So in the db column, the values would be 1, 2 or 3.
Also, whenever I create a new instance, or save a model and the field wasn't set like:
#transaction = Transaction.new(params)
It should default to 1 (on_time).
I'm not sure how I can do this?
basically the same answer as Amit, slight variation
class TransactionType
TYPES = {
:one_time => 1,
:monthly => 2,
:annually => 3
}
# use to bind to select helpers in UI as needed
def self.options
TYPES.map { |item| [item[0], item[1].to_s.titleize] }
end
def self.default
TYPES[:one_time]
end
end
one way to control the default value
class Transaction < ActiveRecord::Base
before_create :set_default_for_type
def set_default_for_type
type = TransactionType.default unless type.present?
end
end
but - best way is to just apply the defaults on your database column and let ActiveRecord get it from there automatically
NOTE: it might also make sense to just have a TransactionType ActiveRecord object instead of above, depends on your situation, i.e.
# on Transaction with type_id:integer
belongs_to :type, class_name: "TransactionType"
You can map the values by creating a constant either in the same Transaction model or by creating a new module and place it inside that as explained by #KepaniHaole
In Transaction model, you can do it like :
class Transaction < ActiveRecord::Base
TRANSACTION_TYPES = { 'one_time' => 1, 'monthly' => 2, 'monthly' => 3 }
end
You can access these values by accessing the constant as
Transaction::TRANSACTION_TYPES['one_time'] # => 1
Transaction::TRANSACTION_TYPES['monthly'] # => 2
Transaction::TRANSACTION_TYPES['monthly'] # => 3
To add a default value to transaction_type column just create a new migration with :
def up
change_column :transactions, :transaction_type, :default => Transaction::TRANSACTION_TYPES['one_time']
end
With this, every time you create a Transaction object without passing transaction_type, the default value 1 with be stored in it.
Maybe you could try something like this? Ruby doesn't really support c-style enums..
module TransactionType
ONCE = 1
MONTHLY = 2
ANUALLY = 3
end
then you could access their values like so:
#transaction = Transaction.new(TransactionType::ONCE)

Rails meta_search greater and less conditions in one combobox

I have a model Phone with checked_by field; if this field is equal to 1, then we know this phone is unchecked, else(>1) - checked. On admin side I can review a list of Phones and I need to create a filter using meta_search to review:
All Phones
Checked
Unchecked
I can see checked_by_greater_than, or checked_by_less_than methods in meta_search, but how to combine those methods in a single select box?
Thanks in any advise
With a scope and a made-up field.
The scope:
class Phone < ActiveRecord::Base
scope :checked, lambda { |value|
!value.zero? ? checked_by_greater_than(1) : where(:checked_by => 1)
}
end
Then add a select-box with three values, returning [nil, 0, 1] as values, and in your controller use that parameter to apply the new scope.
class PhonesController < ApplicationController
def index
# ...
#phones ||= Phone.scoped
checked_select_value = params.delete("checked_select") # here use the name of your form field
if checked_select_value.present?
#phones = #phones.checked(checked_select_value.to_i)
end
# now apply the rest of your meta-search things to the #phones
#
end
end

Ransack: How to use existing scope?

Converting a Rails 2 application to Rails 3, I have to replace the gem searchlogic. Now, using Rails 3.2.8 with the gem Ransack I want to build a search form which uses an existing scope. Example:
class Post < ActiveRecord::Base
scope :year, lambda { |year|
where("posts.date BETWEEN '#{year}-01-01' AND '#{year}-12-31'")
}
end
So far as I know, this can be achieved by defining a custom ransacker. Sadly, I don't find any documentation about this. I tried this in the Postclass:
ransacker :year,
:formatter => proc {|v|
year(v)
}
But this does not work:
Post.ransack(:year_eq => 2012).result.to_sql
=> TypeError: Cannot visit ActiveRecord::Relation
I tried some variations of the ransacker declaration, but none of them work. I Need some help...
UPDATE: The scope above is just on example. I'm looking for a way to use every single existing scope within Ransack. In MetaSearch, the predecessor of Ransack, there is a feature called search_methods for using scopes. Ransack has no support for this out of the box yet.
ransack supports it out of the box after merging https://github.com/activerecord-hackery/ransack/pull/390 . you should declare ransakable_scopes method to add scopes visible for ransack.
From manual
Continuing on from the preceding section, searching by scopes requires defining a whitelist of ransackable_scopes on the model class. The whitelist should be an array of symbols. By default, all class methods (e.g. scopes) are ignored. Scopes will be applied for matching true values, or for given values if the scope accepts a value:
class Employee < ActiveRecord::Base
scope :activated, ->(boolean = true) { where(active: boolean) }
scope :salary_gt, ->(amount) { where('salary > ?', amount) }
# Scopes are just syntactical sugar for class methods, which may also be used:
def self.hired_since(date)
where('start_date >= ?', date)
end
private
def self.ransackable_scopes(auth_object = nil)
if auth_object.try(:admin?)
# allow admin users access to all three methods
%i(activated hired_since salary_gt)
else
# allow other users to search on `activated` and `hired_since` only
%i(activated hired_since)
end
end
end
Employee.ransack({ activated: true, hired_since: '2013-01-01' })
Employee.ransack({ salary_gt: 100_000 }, { auth_object: current_user })
Ransack let's you create custom predicates for this, unfortunately the documentation leaves room for improvement however checkout: https://github.com/ernie/ransack/wiki/Custom-Predicates
Also I believe the problem you're trying to tackle is up on their issue tracker. There's a good discussion going on there: https://github.com/ernie/ransack/issues/34
I wrote a gem called siphon which helps you translate parameters into activerelation scopes. Combining it with ransack can achieves this.
You can read full explanation here. Meanwhile here's the gist of it
The View
= form_for #product_search, url: "/admin/products", method: 'GET' do |f|
= f.label "has_orders"
= f.select :has_orders, [true, false], include_blank: true
-#
-# And the ransack part is right here...
-#
= f.fields_for #product_search.q, as: :q do |ransack|
= ransack.select :category_id_eq, Category.grouped_options
```
ok so now params[:product_search] holds the scopes and params[:product_search][:q] has the ransack goodness. We need to find a way, now, to distribute that data to the form object. So first let ProductSearch swallow it up in the controller:
The Controller
# products_controller.rb
def index
#product_search = ProductSearch.new(params[:product_search])
#products ||= #product_formobject.result.page(params[:page])
end
The Form Object
# product_search.rb
class ProductSearch
include Virtus.model
include ActiveModel::Model
# These are Product.scopes for the siphon part
attribute :has_orders, Boolean
attribute :sort_by, String
# The q attribute is holding the ransack object
attr_accessor :q
def initialize(params = {})
#params = params || {}
super
#q = Product.search( #params.fetch("q") { Hash.new } )
end
# siphon takes self since its the formobject
def siphoned
Siphon::Base.new(Product.scoped).scope( self )
end
# and here we merge everything
def result
Product.scoped.merge(q.result).merge(siphoned)
end
end

Custom method in model to return an object

In the database I have a field named 'body' that has an XML in it. The
method I created in the model looks like this:
def self.get_personal_data_module(person_id)
person_module = find_by_person_id(person_id)
item_module = Hpricot(person_module.body)
personal_info = Array.new
personal_info = {:studies => (item_module/"studies").inner_html,
:birth_place => (item_module/"birth_place").inner_html,
:marrital_status => (item_module/"marrital_status").inner_html}
return personal_info
end
I want the function to return an object instead of an array. So I can
use Module.studies instead of Model[:studies].
This is relatively simple; you're getting an Array because the code is building one. If you wanted to return an object, you'd do something like this:
class PersonalData
attr_accessor :studies
attr_accessor :birth_place
attr_accessor :marital_status
def initialize(studies,birth_place,marital_status)
#studies = studies
#birth_place = birth_place
#marital_status = marital_status
end
end
And your translation code would look like:
def self.get_personal_data_module(person_id)
person_module = find_by_person_id(person_id)
item_module = Hpricot(person_module.body)
personal_info = PersonalData.new((item_module/"studies").inner_html,
(item_module/"birth_place").inner_html,
(item_module/"marital_status").innner_html)
return personal_info
end
Or, if you want to avoid a model class, you could do something weird:
class Hash
def to_obj
self.inject(Object.new) do |obj, ary| # ary is [:key, "value"]
obj.instance_variable_set("##{ary[0]}", ary[1])
class << obj; self; end.instance_eval do # do this on obj's metaclass
attr_reader ary[0].to_sym # add getter method for this ivar
end
obj # return obj for next iteration
end
end
end
Then:
h = {:foo => "bar", :baz => "wibble"}
o = h.to_obj # => #<Object:0x30bf38 #foo="bar", #baz="wibble">
o.foo # => "bar"
o.baz # => "wibble"
It's like magic!
on a slightly different tack.
The idea of using a class method to do this feels wrong from an OO point of view.
You should really refactor this so that it works from an instance method.
def personal_data_module
item_module = Hpricot(body)
{
:studies => (item_module/"studies").inner_html,
:birth_place => (item_module/"birth_place").inner_html,
:marrital_status => (item_module/"marrital_status").inner_html
}
end
Then, where you need to use it, instead of doing....
Foobar.get_personal_data_module(the_id)
you would do
Foobar.find_by_person_id(the_id).personal_data_module
This looks worse, but in fact, thats a bit artificial, normally, you would be
referencing this from some other object, where in fact you would have a 'handle' on the person object, so would not have to construct it yourself.
For instance, if you have another class, where you reference person_id as a foreign key, you would have
class Organisation
belongs_to :person
end
then, where you have an organisation, you could go
organisation.person.personal_information_module
Yes, I know, that breaks demeter, so it would be better to wrap it in a delegate
class Organisation
belongs_to :person
def personal_info_module
person.personal_info_module
end
end
And then from controller code, you could just say
organisation.personal_info_module
without worrying about where it comes from at all.
This is because a 'personal_data_module' is really an attribute of that class, not something to be accessed through a class method.
But this also brings up some questions, for instance, is person_id the primary key of this table? is this a legacy situation where the primary key of the table is not called 'id'?
If this is the case, have you told ActiveRecord about this or do you have to use 'find_by_person_id' all over where you would really want to write 'find'?

Resources