I've created a little app based on an assignment that shows a random set of countries and facts about them, including the countries they share borders with. If they don't share any borders the field is currently empty which I would like to change to "I'm an island", or something.
https://countries-display.herokuapp.com/
This should be easy, but I'm very new, and am not sure how to approach it. Any help or points in the right direction would be greatly appreciated.
I tried initialising a default value:
class Country
include HTTParty
default_options.update(verify: false) # Turn off SSL verification
base_uri 'https://restcountries.eu/rest/v2/'
format :json
def initialize(countries = 'water')
#countries = countries
end
def self.all
#countries = get('/all')
#countries.each do |country|
country['borders'].map! do |country_code|
#countries.find { |country| country['alpha3Code'] == country_code } ['name']
end
country['languages'].map! { |language| language['name'] }
country['currencies'].map! { |currency| currency['name'] }
end
#countries
end
end
Setting a default in active record:
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
class Country < ActiveRecord::Base
before_create :set_defaults
def set_defaults
self.countries_to_display = 'water' if #countries_to_display.nil?
end
end
I also tried implementing some if statements that were equally unsuccessful.
Solution:
#countries.each do |country|
if country['borders'].empty?
country['borders'] << "I'm an island"
else country['borders'].map! do |country_code|
#countries.find { |country| country['alpha3Code'] == country_code } ['name']
end
end
...
I'm assuming that country['borders'] maps to the field that should contain I'm an island. I'm also assuming that you directly render the output of Country.all.
What you'll need to do is to add a check whether there are any borders and if there aren't write that string to the field instead of the list of bordering countries.
#countries.each do |country|
if country['borders'].empty?
borders = "I'm an island"
else
borders = country['borders'].map do |country_code|
#countries.find { |country| country['alpha3Code'] == country_code }['name']
end
end
country['borders'] = borders
...
end
Note that we use map instead of map! here to not modify the existing collection. You may need to adjust your rendering logic, as country['borders'] previously contained a list of countries and now contains a string.
Related
As far as I've tested it, this helper method works exactly as it's meant to, however I want to know if there is any easier, built-in, or smarter way to run this check! I also am aware that having this in the ApplicationHelper probably isn't ideal. Not sure if I should just put it in the parent object (the Inspection), some other model, or leave as is.
With is_model_empty? I need to run through every field of any one of eleven different (but similar) models to check to see if all of them are Empty. All of them except the :id, :inspection_id, :created_at, and :updated_at fields which will never be blank. Empty can be nil, can be [], or can be ['']. An empty string would actually imply that the user entered something so that won't be included. The value can be either a string or an array so .empty? won't work.
def is_model_empty?(model)
model.attributes.each do |k, v|
unless ['id', 'inspection_id', 'created_at', 'updated_at'].include?(k)
return false unless v.nil? || v == [] || v == [""]
end
end
true
end
The eleven models all belong to the Inspection and each has a has_one relationship:
has_one :first_info_section
has_one :second_info_section
has_one :third_info_section
Any advice/feedback would be much appreciated. Thanks for reading!
-Dave
Your method can be simplifed as an instance method on each of the models. If the attribute exceptions are the same for all the models you can create a shared library and include it each of the models.
app/models/empty_detection.rb:
module EmptyDetection
def empty?
attributes.all? do |k, v|
['id', 'inspection_id', 'created_at', 'updated_at'].include?(k) || v.nil? || v == [] || v == [""]
end
end
end
Include that module in each model you want to be able to check for the empty conditions. For example, the Widget model:
class Widget < ActiveRecord::Base
include EmptyDetection
end
Now you can use it on any instance of a Widget:
widget = Widget.find(45)
widget.empty?
Here's a really basic refactor:
def is_empty?(model)
whitelist = %w[ id inspection_id created_at updated_at ]
model.attributes.all? do |attr, val|
whitelist.exclude?(attr) || val.nil? || val == [] || val == [""]
end
end
What you really want, though, is a validator, which is described in the Active Record Validations Rails Guide:
In this case it would look like this:
class EmptyValidator < ActiveModel::Validator
WHITELIST = %w[ id inspection_id created_at updated_at ].freeze
def validate(record)
return unless model.attributes.all? do |attr, val|
WHITELIST.exclude?(attr) || empty?(val)
end
record.errors[:base] << "You missed one!"
end
private
def empty?(val)
val.nil? || val == [] || val == [""]
end
end
Then, in each of your models...
class MyModel < ActiveRecord::Base
validates_with EmptyValidator
end
I hope that's helpful!
user.attributes.values.all?(&:blank?)
For example in my Car model i have such fields:
color, price, year
and in form partial i generate form with all this fields. But how to code such logic:
user could enter color and year and i must find with this conditions, user could enter just year or all fields in same time...
And how to write where condition? I could write something like:
if params[:color].present?
car = Car.where(color: params[:color])
end
if params[:color].present? && params[:year].present?
car = Car.where(color: params[:color], year: params[:year])
end
and so over....
But this is very ugly solution, i'm new to rails, and want to know: how is better to solve my problem?
Check out the has_scope gem: https://github.com/plataformatec/has_scope
It really simplifies a lot of this:
class Graduation < ActiveRecord::Base
scope :featured, -> { where(:featured => true) }
scope :by_degree, -> degree { where(:degree => degree) }
scope :by_period, -> started_at, ended_at { where("started_at = ? AND ended_at = ?", started_at, ended_at) }
end
class GraduationsController < ApplicationController
has_scope :featured, :type => :boolean
has_scope :by_degree
has_scope :by_period, :using => [:started_at, :ended_at], :type => :hash
def index
#graduations = apply_scopes(Graduation).all
end
end
Thats it from the controller side
I would turn those into scopes on your Car model:
scope :by_color, lambda { |color| where(:color => color)}
scope :by_year, lambda { |year| where(:year => year)}
and in your controller you would just conditionally chain them like this:
def index
#cars = Car.all
#cars = #cars.by_color(params[:color]) if params[:color].present?
#cars = #cars.by_year(params[:year]) if params[:year].present?
end
user_params = [:color, :year, :price]
cars = self
user_params.each do |p|
cars = cars.where(p: params[p]) if params[p].present?
end
The typical (naive, but simple) way I would do this is with a generic search method in my model, eg.
class Car < ActiveRecord::Base
# Just pass params directly in
def self.search(params)
# By default we return all cars
cars = all
if params[:color].present?
cars = cars.where(color: params[:color])
end
if params[:price1].present? && params[:price2].present?
cars = cars.where('price between ? and ?', params[:price1], params[:price2])
end
# insert more fields here
cars
end
end
You can easily keep chaining wheres onto the query like this, and Rails will just AND them all together in the SQL. Then you can just call it with Car.search(params).
I think you could use params.permit
my_where_params = params.permit(:color, :price, :year).select {|k,v| v.present?}
car = Car.where(my_where_params)
EDIT: I think this only works in rails 4, not sure what version you're using.
EDIT #2 excerpt from site I linked to:
Using permit won't mind if the permitted attribute is missing
params = ActionController::Parameters.new(username: "john", password: "secret")
params.permit(:username, :password, :foobar)
# => { "username"=>"john", "password"=>"secret"}
as you can see, foobar isn't inside the new hash.
EDIT #3 added select block to where_params as it was pointed out in the comments that empty form fields would trigger an empty element to be created in the params hash.
I need to select available taxons and their childs.
I'm using this custom rule:
module Spree
class Promotion
module Rules
class TaxonPromotionRule < Spree::PromotionRule
has_and_belongs_to_many :taxon, class_name: '::Spree::Taxon', join_table: 'spree_taxons_promotion_rules', foreign_key: 'promotion_rule_id'
validate :only_one_promotion_per_product
MATCH_POLICIES = %w(any all)
preference :match_policy, :string, default: MATCH_POLICIES.first
# scope/association that is used to test eligibility
def eligible_taxons
taxon
end
def applicable?(promotable)
promotable.is_a?(Spree::Order)
end
def eligible?(order, options = {})
return false if eligible_taxons.empty?
if preferred_match_policy == 'all'
eligible_taxons.all? {|p| order.products.include_taxon?(p) }
else
order.products.any? {|p| eligible_taxons.any? {|t| t.include_product?(p)} }
end
end
def taxon_ids_string
taxon_ids.join(',')
end
def taxon_ids_string=(s)
self.taxon_ids = s.to_s.split(',').map(&:strip)
end
private
def only_one_promotion_per_product
if Spree::Promotion::Rules::TaxonPromotionRule.all.map(&:taxon).flatten.uniq!
errors[:base] << "You can't create two promotions for the same product"
end
end
end
end
end
end
and decorator:
Spree::Taxon.class_eval do
def include_product? p
products.include? p
end
end
I want eligible_taxons to be taxon from rules table and all child id. So if I set some root category this rule would apply for all child categories. I hope my question is understandable and clear. :)
Found it. Looks complicated for a newbie (me) on RoR. But here it is:
def eligible_taxons
taxon_with_childs = []
taxon.each { |t| t.self_and_descendants.each{|s| taxon_with_childs << s} }
taxon_with_childs.uniq
end
It builds new list of descendants and self. More details about these functions are here https://github.com/collectiveidea/awesome_nested_set/blob/master/lib/awesome_nested_set/model/relatable.rb
Because after building this list some rows are identical and repeats few times we return only unique taxon_with_childs.uniq
This probably not the best performing algorithm but it does what I needed and fits well with amount of data.
I have a simple relationship:
class Item
belongs_to :container, :counter_cache => true
end
class Container
has_many :items
end
Let's say I have two containers. I create an item and associate it with the first container. The counter is increased.
Then I decide to associate it with the other container instead. How to update the items_count column of both containers?
I found a possible solution at http://railsforum.com/viewtopic.php?id=39285 .. however I'm a beginner and I don't understand it. Is this the only way to do it?
It should work automatically. When you are updating items.container_id it will decreament old container's counter and increament new one. But if it isn't works - it is strange. You can try this callback:
class Item
belongs_to :container, :counter_cache => true
before_save :update_counters
private
def update_counters
new_container = Container.find self.container_id
old_container = Container.find self.container_id_was
new_container.increament(:items_count)
old_container.decreament(:items_count)
end
end
UPD
To demonstrate native behavior:
container1 = Container.create :title => "container 1"
#=> #<Container title: "container 1", :items_count: nil>
container2 = Container.create :title => "container 2"
#=> #<Container title: "container 2", :items_count: nil>
item = container1.items.create(:title => "item 1")
Container.first
#=> #<Container title: "container 1", :items_count: 1>
Container.last
#=> #<Container title: "container 1", :items_count: nil>
item.container = Container.last
item.save
Container.first
#=> #<Container title: "container 1", :items_count: 0>
Container.last
#=> #<Container title: "container 1", :items_count: 1>
So it should work without any hacking. From the box.
Modified it a bit to handle custom counter cache names
(Don't forget to add after_update :fix_updated_counter to the models using counter_cache)
module FixUpdateCounters
def fix_updated_counters
self.changes.each { |key, (old_value, new_value)|
# key should match /master_files_id/ or /bibls_id/
# value should be an array ['old value', 'new value']
if key =~ /_id/
changed_class = key.sub /_id$/, ''
association = self.association changed_class.to_sym
case option = association.options[ :counter_cache ]
when TrueClass
counter_name = "#{self.class.name.tableize}_count"
when Symbol
counter_name = option.to_s
end
next unless counter_name
association.klass.decrement_counter(counter_name, old_value) if old_value
association.klass.increment_counter(counter_name, new_value) if new_value
end
} end end
ActiveRecord::Base.send(:include, FixUpdateCounters)
For rails 3.1 users.
With rails 3.1, the answer doesn't work.
The following works for me.
private
def update_counters
new_container = Container.find self.container_id
Container.increment_counter(:items_count, new_container)
if self.container_id_was.present?
old_container = Container.find self.container_id_was
Container.decrement_counter(:items_count, old_container)
end
end
here is an approach that works well for me in similar situations
class Item < ActiveRecord::Base
after_update :update_items_counts, if: Proc.new { |item| item.collection_id_changed? }
private
# update the counter_cache column on the changed collections
def update_items_counts
self.collection_id_change.each do |id|
Collection.reset_counters id, :items
end
end
end
additional information on dirty object module http://api.rubyonrails.org/classes/ActiveModel/Dirty.html and an old video about them http://railscasts.com/episodes/109-tracking-attribute-changes and documentation on reset_counters http://apidock.com/rails/v3.2.8/ActiveRecord/CounterCache/reset_counters
Updates to #fl00r Answer
class Container
has_many :items_count
end
class Item
belongs_to :container, :counter_cache => true
after_update :update_counters
private
def update_counters
if container_id_changed?
Container.increment_counter(:items_count, container_id)
Container.decrement_counter(:items_count, container_id_was)
end
# other counters if any
...
...
end
end
I recently came across this same problem (Rails 3.2.3). Looks like it has yet to be fixed, so I had to go ahead and make a fix. Below is how I amended ActiveRecord::Base and utilize after_update callback to keep my counter_caches in sync.
Extend ActiveRecord::Base
Create a new file lib/fix_counters_update.rb with the following:
module FixUpdateCounters
def fix_updated_counters
self.changes.each {|key, value|
# key should match /master_files_id/ or /bibls_id/
# value should be an array ['old value', 'new value']
if key =~ /_id/
changed_class = key.sub(/_id/, '')
changed_class.camelcase.constantize.decrement_counter(:"#{self.class.name.underscore.pluralize}_count", value[0]) unless value[0] == nil
changed_class.camelcase.constantize.increment_counter(:"#{self.class.name.underscore.pluralize}_count", value[1]) unless value[1] == nil
end
}
end
end
ActiveRecord::Base.send(:include, FixUpdateCounters)
The above code uses the ActiveModel::Dirty method changes which returns a hash containing the attribute changed and an array of both the old value and new value. By testing the attribute to see if it is a relationship (i.e. ends with /_id/), you can conditionally determine whether decrement_counter and/or increment_counter need be run. It is essnetial to test for the presence of nil in the array, otherwise errors will result.
Add to Initializers
Create a new file config/initializers/active_record_extensions.rb with the following:
require 'fix_update_counters'
Add to models
For each model you want the counter caches updated add the callback:
class Comment < ActiveRecord::Base
after_update :fix_updated_counters
....
end
Here the #Curley fix to work with namespaced models.
module FixUpdateCounters
def fix_updated_counters
self.changes.each {|key, value|
# key should match /master_files_id/ or /bibls_id/
# value should be an array ['old value', 'new value']
if key =~ /_id/
changed_class = key.sub(/_id/, '')
# Get real class of changed attribute, so work both with namespaced/normal models
klass = self.association(changed_class.to_sym).klass
# Namespaced model return a slash, split it.
unless (counter_name = "#{self.class.name.underscore.pluralize.split("/")[1]}_count".to_sym)
counter_name = "#{self.class.name.underscore.pluralize}_count".to_sym
end
klass.decrement_counter(counter_name, value[0]) unless value[0] == nil
klass.increment_counter(counter_name, value[1]) unless value[1] == nil
end
}
end
end
ActiveRecord::Base.send(:include, FixUpdateCounters)
Sorry I don't have enough reputation to comment the answers.
About fl00r, I may see a problem if there is an error and save return "false", the counter has already been updated but it should have not been updated.
So I'm wondering if "after_update :update_counters" is more appropriate.
Curley's answer works but if you are in my case, be careful because it will check all the columns with "_id". In my case it is automatically updating a field that I don't want to be updated.
Here is another suggestion (almost similar to Satish):
def update_counters
if container_id_changed?
Container.increment_counter(:items_count, container_id) unless container_id.nil?
Container.decrement_counter(:items_count, container_id_was) unless container_id_was.nil?
end
end
I have an object now:
class Items
attr_accessor :item_id, :name, :description, :rating
def initialize(options = {})
options.each {
|k,v|
self.send( "#{k.to_s}=".intern, v)
}
end
end
I have it being assigned as individual objects into an array...
#result = []
some loop>>
#result << Items.new(options[:name] => 'name', options[:description] => 'blah')
end loop>>
But instead of assigning my singular object to an array... how could I make the object itself a collection?
Basically want to have the object in such a way so that I can define methods such as
def self.names
#items.each do |item|
item.name
end
end
I hope that makes sense, possibly I am overlooking some grand scheme that would make my life infinitely easier in 2 lines.
A few observations before I post an example of how to rework that.
Giving a class a plural name can lead to a lot of semantic issues when declaring new objects, as in this case you'd call Items.new, implying you're creating several items when in fact actually making one. Use the singular form for individual entities.
Be careful when calling arbitrary methods, as you'll throw an exception on any misses. Either check you can call them first, or rescue from the inevitable disaster where applicable.
One way to approach your problem is to make a custom collection class specifically for Item objects where it can give you the information you need on names and such. For example:
class Item
attr_accessor :item_id, :name, :description, :rating
def initialize(options = { })
options.each do |k,v|
method = :"#{k}="
# Check that the method call is valid before making it
if (respond_to?(method))
self.send(method, v)
else
# If not, produce a meaningful error
raise "Unknown attribute #{k}"
end
end
end
end
class ItemsCollection < Array
# This collection does everything an Array does, plus
# you can add utility methods like names.
def names
collect do |i|
i.name
end
end
end
# Example
# Create a custom collection
items = ItemsCollection.new
# Build a few basic examples
[
{
:item_id => 1,
:name => 'Fastball',
:description => 'Faster than a slowball',
:rating => 2
},
{
:item_id => 2,
:name => 'Jack of Nines',
:description => 'Hypothetical playing card',
:rating => 3
},
{
:item_id => 3,
:name => 'Ruby Book',
:description => 'A book made entirely of precious gems',
:rating => 1
}
].each do |example|
items << Item.new(example)
end
puts items.names.join(', ')
# => Fastball, Jack of Nines, Ruby Book
Do you know the Ruby key word yield?
I'm not quite sure what exactly you want to do. I have two interpretations of your intentions, so I give an example that makes two completely different things, one of them hopefully answering your question:
class Items
#items = []
class << self
attr_accessor :items
end
attr_accessor :name, :description
def self.each(&args)
#items.each(&args)
end
def initialize(name, description)
#name, #description = name, description
Items.items << self
end
def each(&block)
yield name
yield description
end
end
a = Items.new('mug', 'a big cup')
b = Items.new('cup', 'a small mug')
Items.each {|x| puts x.name}
puts
a.each {|x| puts x}
This outputs
mug
cup
mug
a big cup
Did you ask for something like Items.each or a.each or for something completely different?
Answering just the additional question you asked in your comment to tadman's solution: If you replace in tadman's code the definition of the method names in the class ItemsCollection by
def method_missing(symbol_s, *arguments)
symbol, s = symbol_s.to_s[0..-2], symbol_s.to_s[-1..-1]
if s == 's' and arguments.empty?
select do |i|
i.respond_to?(symbol) && i.instance_variables.include?("##{symbol}")
end.map {|i| i.send(symbol)}
else
super
end
end
For his example data you will get following outputs:
puts items.names.join(', ')
# => Fastball, Jack of Nines, Ruby Book
puts items.descriptions.join(', ')
# => Faster than a slowball, Hypothetical playing card, A book made entirely of precious gems
As I don't know about any way to check if a method name comes from an attribute or from another method (except you redefine attr_accessor, attr, etc in the class Module) I added some sanity checks: I test if the corresponding method and an instance variable of this name exist. As the class ItemsCollection does not enforce that only objects of class Item are added, I select only the elements fulfilling both checks. You can also remove the select and put the test into the map and return nil if the checks fail.
The key is the return value. If not 'return' statement is given, the result of the last statement is returned. You last statement returns a Hash.
Add 'return self' as the last line of initialize and you're golden.
Class Item
def initialize(options = {})
## Do all kinds of stuff.
return self
end
end