Ruby on Rails: Execute Logic Based on Selected Menu - ruby-on-rails

I have a class that I use to contain select menu options for property types. It works fine. However, I need to be able to verify the selection and perform specific logic based on the selected option. This needs to happen in my Ruby code and in JavaScript.
Here is the class in question:
class PropertyTypes
def self.[](id)
##types[id]
end
def self.options_for_select
##for_select
end
private
##types = {
1 => "Residential",
2 => "Commercial",
3 => "Land",
4 => "Multi-Family",
5 => "Retail",
6 => "Shopping Center",
7 => "Industrial",
8 => "Self Storage",
9 => "Office",
10 => "Hospitality"
}
##for_select = ##types.each_pair.map{|id, display_name| [display_name, id]}
end
What is the best way to verify the selection? I need to perform specific logic and display user interface elements based on each type of property type.
Since I am storing the id, I would be verifying that the id is a particular property type. Something like:
PropertyTypes.isResidential?(id)
Then this method would look like this:
def self.isResidential?(id)
##types[id] == "Residential"
end
But now I am duplicating the string "Residential".
For JavaScript, I assume I would make an ajax call back to the model to keep the verification code DRY, but this seems like over kill.
Do I need to manually create a verification method for each property type or can I use define_method?
This seems so basic yet I am confused and burned out on this problem.
Thanks
===
Here's my solution:
class << self
##types.values.each do |v|
# need to remove any spaces or hashes from the found property type
v = v.downcase().gsub(/\W+/, '')
define_method "is_#{v}?", do |i|
type_name = ##types[i]
return false if type_name == nil #in case a bogus index is passed in
type_name = type_name.downcase().gsub(/\W+/, '')
type_name == v
end
end
end

It sounds like you can benefit from some Ruby meta-programming. Try googling "ruby method_missing". You can probably do something quick & dirty along the lines of:
class PropertyTypes
def method_missing(meth, *args, &block)
if meth.to_s =~ /^is_(.+)\?$/
##types[args.first] == $1
else
super
end
end
end

On the ruby side you could also use something like this to define dynamically these methods:
class << self
##types.values.each do |v|
define_method "is_#{v}?", do |i|
##types[i] == v
end
end
end

Related

Rails 5 - iterate until field matches regex

In my app that I am building to learn Rails and Ruby, I have below iteration/loop which is not functioning as it should.
What am I trying to achieve?
I am trying to find the business partner (within only the active once (uses a scope)) where the value of the field business_partner.bank_account is contained in the field self_extracted_data and then set the business partner found as self.sender (self here is a Document).
So once a match is found, I want to end the loop. A case exists where no match is found and sender = nil so a user needs to set it manually.
What happens now, is that on which ever record of the object I save (it is called as a callback before_save), it uses the last identified business partner as sender and the method does not execute again.
Current code:
def set_sender
BusinessPartner.active.where.not(id: self.receiver_id).each do |business_partner|
bp_bank_account = business_partner.bank_account.gsub(/\s+/, '')
rgx = /(?<!\w)(#{Regexp.escape(bp_bank_account)})?(?!\‌​w)/
if self.extracted_data.gsub(/\s+/, '') =~ rgx
self.sender = business_partner
else
self.sender = nil
end
end
end
Thanks for helping me understand how to do this kind of case.
p.s. have the pickaxe book here yet this is so much that some help / guidance would be great. The regex works.
Using feedback from #moveson, this code works:
def match_with_extracted_data?(rgx_to_match)
extracted_data.gsub(/\s+/, '') =~ rgx_to_match
end
def set_sender
self.sender_id = matching_business_partner.try(:id) #unless self.sender.id.present? # Returns nil if no matching_business_partner exists
end
def matching_business_partner
BusinessPartner.active.excluding_receiver(receiver_id).find { |business_partner| sender_matches?(business_partner) }
end
def sender_matches?(business_partner)
rgx_registrations = /(#{Regexp.escape(business_partner.bank_account.gsub(/\s+/, ''))})|(#{Regexp.escape(business_partner.registration.gsub(/\s+/, ''))})|(#{Regexp.escape(business_partner.vat_id.gsub(/\s+/, ''))})/
match_with_extracted_data?(rgx_registrations)
end
In Ruby you generally want to avoid loops and #each and long, procedural methods in favor of Enumerable iterators like #map, #find, and #select, and short, descriptive methods that each do a single job. Without knowing more about your project I can't be sure exactly what will work, but I think you want something like this:
# /models/document.rb
class Document < ActiveRecord::Base
def set_sender
self.sender = matching_business_partner.try(:id) || BusinessPartner.active.default.id
end
def matching_business_partners
other_business_partners.select { |business_partner| account_matches?(business_partner) }
end
def matching_business_partner
matching_business_partners.first
end
def other_business_partners
BusinessPartner.excluding_receiver_id(receiver_id)
end
def account_matches?(business_partner)
rgx = /(?<!\w)(#{Regexp.escape(business_partner.stripped_bank_account)})?(?!\‌​w)/
data_matches_bank_account?(rgx)
end
def data_matches_bank_account?(rgx)
extracted_data.gsub(/\s+/, '') =~ rgx
end
end
# /models/business_partner.rb
class BusinessPartner < ActiveRecord::Base
scope :excluding_receiver_id, -> (receiver_id) { where.not(id: receiver_id) }
def stripped_bank_account
bank_account.gsub(/\s+/, '')
end
end
Note that I am assigning an integer id, rather than an ActiveRecord object, to self.sender. I think that's what you want.
I didn't try to mess with the database relations here, but it does seem like Document could include a belongs_to :business_partner, which would give you the benefit of Rails methods to help you find one from the other.
EDIT: Added Document#matching_business_partners method and changed Document#set_sender method to return nil if no matching_business_partner exists.
EDIT: Added BusinessPartner.active.default.id as the return value if no matching_business_partner exists.

Interpolating an attribute's key before save

I'm using Rails 4 and have an Article model that has answer, side_effects, and benefits as attributes.
I am trying to create a before_save method that automatically looks at the side effects and benefits and creates links corresponding to another article on the site.
Instead of writing two virtually identical methods, one for side effects and one for benefits, I would like to use the same method and check to assure the attribute does not equal answer.
So far I have something like this:
before_save :link_to_article
private
def link_to_article
self.attributes.each do |key, value|
unless key == "answer"
linked_attrs = []
self.key.split(';').each do |i|
a = Article.where('lower(specific) = ?', i.downcase.strip).first
if a && a.approved?
linked_attrs.push("<a href='/questions/#{a.slug}' target=_blank>#{i.strip}</a>")
else
linked_attrs.push(i.strip)
end
end
self.key = linked_attrs.join('; ')
end
end
end
but chaining on the key like that gives me an undefined method 'key'.
How can I go about interpolating in the attribute?
in this bit: self.key you are asking for it to literally call a method called key, but what you want, is to call the method-name that is stored in the variable key.
you can use: self.send(key) instead, but it can be a little dangerous.
If somebody hacks up a new form on their browser to send you the attribute called delete! you don't want it accidentally called using send, so it might be better to use read_attribute and write_attribute.
Example below:
def link_to_article
self.attributes.each do |key, value|
unless key == "answer"
linked_attrs = []
self.read_attribute(key).split(';').each do |i|
a = Article.where('lower(specific) = ?', i.downcase.strip).first
if a && a.approved?
linked_attrs.push("<a href='/questions/#{a.slug}' target=_blank>#{i.strip}</a>")
else
linked_attrs.push(i.strip)
end
end
self.write_attribute(key, linked_attrs.join('; '))
end
end
end
I'd also recommend using strong attributes in the controller to make sure you're only permitting the allowed set of attributes.
OLD (before I knew this was to be used on all attributes)
That said... why do you go through every single attribute and only do something if the attribute is called answer? why not just not bother with going through the attributes and look directly at answer?
eg:
def link_to_article
linked_attrs = []
self.answer.split(';').each do |i|
a = Article.where('lower(specific) = ?', i.downcase.strip).first
if a && a.approved?
linked_attrs.push("<a href='/questions/#{a.slug}' target=_blank>#{i.strip}</a>")
else
linked_attrs.push(i.strip)
end
end
self.answer = linked_attrs.join('; ')
end

ActiveAdmin custom filter from model attributes on the fly

I am updating an old ruby\rails application that has an ActiveAdmin component (ActiveAdmin 0.6, Ruby 1.9.3 and Rails 3.2). The user has requested a filter that searches all fields in a given model. I don't think this is practical because you can't search a date or numeric value for "a" so I have compromised on just searching text with the filter.
Having looked at the ActiveAdmin documentation this states that you can create a filter for several attributes using "or" between the attributes. So if I wanted to search the "circumstances" or "accident_type" attributes I would use the filter below:
filter :circumstances_or_accident_type, :as => :string, label: "Search All Text Fields"
If I use this syntax the filter works as expected.
I now want to find all the string\text attributes to create by filter attributes which I did using this code (there are probably neater ways of doing this but it works):
xfilter_text = ""
Notification.columns.each do |xfield|
if xfield.type == :string or xfield.type == :text
if xfilter_text.length == 0
xfilter_text = xfield.name
else
xfilter_text << "_or_"
xfilter_text << xfield.name
end
end
end
I used the result to hard-code the values into the filter which gave me the following (yes there are a few attributes in the model):
filter :circumstances_or_accident_type_or_author_type_or_location_or_immediate_action_or_injury_details_or_outcome_type_or_investigation_findings_or_action_to_prevent_recurrence_or_building_or_classification_or_manager_email_or_manager_name_or_current_stage_or_injured_last_name_or_injured_first_name_or_injured_gender_or_injured_address_or_injured_home_telephone_or_injured_work_status_or_injured_job_title_or_injured_working_pattern_or_injured_email_or_riddor_document_or_body_part_or_kind_of_accident_or_injury_type_or_service_or_team_or_defects_or_witness_details_or_location_details_or_hse_reference_number_or_riddor_category_or_address_or_details_of_treatment_or_processor_actions_or_business_unit_or_other_author_type_or_lost_time_details_or_changed_by_or_details_of_hospital_treatment, :as => :string, label: "Search All Text Fields"
I tested this and it worked. All good so far. I could just leave it here but I wanted to ensure the code is self maintaining so any changes in the model would not require changes to the custom filter. This is the part I am having trouble with. I would like to change the hardcoded attributes to use the results of the code that creates the filter attributes somehow. Something like this:
filter :get_filter, :as => :string, label: "Search All Text Fields"
def get_filter
xfilter_text = ""
Notification.columns.each do |xfield|
if xfield.type == :string or xfield.type == :text
if xfilter_text.length == 0
xfilter_text = xfield.name
else
xfilter_text << "_or_"
xfilter_text << xfield.name
end
end
return xfilter
end
end
I expect that I would need something that checks that attributes are returned otherwise the filter would fail. I can add that once I get the code working.
Appreciate any help or suggestions.
I'd be inclined to take the messy business of generating the query and delegate it to the model, using its own scope/class method. Then you just need to inform MetaSearch/Ransack (depending on your ActiveAdmin version) that it can search that scope, and you can add it as a filter.
For bonus points, you could drop the search method into a concern that you can include into any model.
app/admin/notifications.rb
filter :containing_text, as: :string, label: 'Text Search:'
app/models/notification.rb
# for MetaSearch
# search_methods :containing_text
# for Ransack
def self.ransackable_scopes(_opts)
[:containing_text]
end
# this could be dropped into a concern as-is
def self.containing_text(query)
# select text-type columns
cols = columns.select { |c| [:string, :text].include?(c.type) }
# generate query fragment
fragment = cols.map { |c| "#{ table_name }.#{ c.name } LIKE ?" }
.join(' OR ')
# execute sanitized query
where(fragment, *Array.new(cols.size, "%#{ query }%"))
end
### EDIT by OP ###
I had never used concerns before so eventually worked out how to get it working:
1) Add the concern path to your application.rb
config/application.rb
class Application < Rails::Application
config.autoload_paths += %W(#{config.root}/app/models/concerns)
end
2) Add the include to the Searchable concern and method call into the notifcation model
app/models/notification.rb
include Searchable
search_methods :containing_text
3) Created the concern:
/app/models/concerns/searchable.rb
module Searchable
extend ActiveSupport::Concern
module ClassMethods
def self.containing_text(query)
# select text-type columns (string and text)
cols = columns.select { |c| [:string, :text].include?(c.type) }
# generate query fragment
fragment = cols.map { |c| "#{ table_name }.#{ c.name } LIKE ?" }
.join(' OR ')
# execute sanitized query
where(fragment, *Array.new(cols.size, "%#{ query }%"))
end
end
end
That then seemed to work. I probably should rename the searchable into something better but it works.

Alternatives to nested conditionals Rails controller

I have a page that presents bits of information in a grid format. What shows up in each grid tile depends on:
Whether the user is logged in or not
What the user clicks on
Other factors
I'm using AJAX to send the controller what the user clicks on and grab fresh content for the tiles.
# Simplified pseudocode example
def get_tile_content
tile_objects = []
if current_user.present?
if params[:user_selected_content] == 'my stuff'
tile_objects = [... some model scope source here...]
elsif params[:user_selected_content] == 'new stuff'
tile_objects = [... some model scope source here...]
elsif params[:user_selected_content] == 'other stuff'
tile_objects = [... some model scope source here...]
end
else
tile_objects = [... some model scope source here...]
end
render :json => tile_objects.to_json || {}
end
Any ideas on how to approach this differently? I tried moving the complexity to models but I found it to be less readable and harder to figure out what is going on.
Looks like a decent case for a case statement (...see what I did there? ;P)
def get_tile_content
tile_objects = []
if current_user.present?
tile_objects = case params[:user_selected_content]
when 'my stuff' then Model.my_stuff
when 'new stuff' then Model.new_stuff
when 'other stuff' then Model.other_stuff
end
else
tile_objects = Model.public_stuff
end
render :json => tile_objects.to_json || {}
end
Sometimes case statements are necessary. Can't really say if that's the situation here since I can't see the larger design of your app, but this at least will clean it up a bit to fewer, easier to read lines, depending on your style preference.
You could wrap the case statement into its own method if you prefer, passing it the value of your parameter.
Another style point is that you don't typically use get_ at the beginning of ruby method names. It's assumed that .blah= is a setter and .blah is a getter, so .get_blah is redundant.

Ruby on Rails: how do I set a variable where the variable being changed can change?

i want to do
current_user.allow_????? = true
where ????? could be whatever I wanted it to be
I've seen it done before.. just don't remember where, or what the thing is called.
foo = "bar"
current_user.send("allow_#{foo}=", true)
EDIT:
what you're asking for in the comment is another thing. If you want to grab a constant, you should use for instance
role = "admin"
User.const_get(role)
That's a "magic method" and you implement the method_missing on your current_user object. Example from Design Patterns
#example method passed into computer builder class
builder.add_dvd_and_harddisk
#or
builder.add_turbo_and_dvd_dvd_and_harddisk
def method_missing(name, *args)
words = name.to_s.split("_")
return super(name, *args) unless words.shift == 'add'
words.each do |word|
#next is same as continue in for loop in C#
next if word == 'and'
#each of the following method calls are a part of the builder class
add_cd if word == 'cd'
add_dvd if word == 'dvd'
add_hard_disk(100000) if word == 'harddisk'
turbo if word == 'turbo'
end
end

Resources