Incrementing enums in Ruby? - ruby-on-rails

Say I have enum stage [:one, :two, :three, :four] in a model.
When the controller action next_stage is called (button clicked by the user to send it to the next stage), I want to go incrementally from stage X to Y. What's the easiest way to do this? I currently use a big, gross case statement, but I feel like I can do it better. I'll provide my use-case:
Class MyController
def next_stage
# #my_controller.stage => "two"
#my_controller.stage.value++ unless #my_controller.stage.four?
# #my_controller.stage => "three"
end
end

Honestly, if you're trying to store state that moves in a certain order, you should use a state machine. https://github.com/aasm/aasm supports using enums to store the state. You could do something like this;
aasm :column => :stage, :enum => true do
state :stage1, :initial => true
state :stage2
state :stage3
state :stage4
event :increment_stage do
transitions from: :stage1, to: :stage2
transitions from: :stage2, to: :stage3
transitions from: :stage3, to: :stage4
end
end
it not only cleans up your logic, but the tests will be simpler, and you can do all sorts of callbacks on different events. It's really good for any sort of workflow as well (say moving a post from review to approved etc.)
EDIT: Can I also suggest that if you don't want to use a state machine then you at least move this state shifting logic into your model? Controllers are about access control, models are about state.

Sort of hackish but the only way to get an integer out of an enum that I have found is doing
model.read_attribute('foo')
So you could try to do
def next_stage
#my_controller.update_column(:stage, #my_controller.read_attribute('stage')+1) unless #my_controller.four?
end

Your code seems a bit strange, what is #my_controller? What is stage?
Besides that, if I understand correctly, what you want is this:
#my_controller[:stage] += 1 unless #my_controller.four?
Enumerations are stored as integers in the database, and they start at 0, with increments of 1. So, simply access the raw attribute data and increment it.

There exists a gem simple_enum which lets you use more powerful enums.
The enum will let you use both integers and/or symbols, then you can do something like
#my_controller.stage.value_cd += 1 unless #my_controller.stage.last_stage?
You can use it with ActiveRecord/Mongoid, but also any other object.
For an ORM like Mongoid, your model could look like this
class Project
as_enum :status, {
idea: 0,
need_estimate: 1,
in_progress: 2,
finished: 3,
paid: 4,
field: { type: Integer, default: 0 }
def next_step!
self.status_cd += 1 unless self.paid?
end
end
project = Project.new
project.status # => :idea
project.need_estimate? #=> false
project.next_step!
project.need_estimate? #=> true

I don't know whether I get this right. Maybe you can try this:
Class MyController
def next_stage
#my_controller.stage.increment! :value rescue nil
end
end

NEW ANSWER
Ok, I think I better understand what you are looking for. How about this:
In your model set up your Enum with a hash rather than an array:
class MyClass < ActiveRecord::Base
enum stage: {one: 1, two: 2, three: 3, four: 4} #just makes more sense when talking about stages
end
Then you can use the current status' index:
Class MyController
def next_stage
# #my_controller.stage => "two"
current_stage = MyClass.stages[#my_controller.stage] # returns => 1
current_stage += 1 unless current_stage == 4 # returns => 2
#my_controller.update! stage: current_stage
# #my_controller.stage => "three"
end
end
If I understand the docs correctly, this may also work:
Class MyController
def next_stage
# #my_controller.stage => "two"
#my_controller.update! stage: MyClass.stages[#my_controller.stage] + 1 unless #my_controller.stage == :four
# #my_controller.stage => "three"
end
end
this is off the cuff and could probably be cleaned up in some ways. I can't experiment very much because I don't have something setup in a rails app with an enum to mess around with.
old answer left for archival purposes.
def next_stage
self.next
end
edit I saw enums and thought you were shortening enumerable to enum (as in x.to_enum). Not so sure this won't work for you in some form. You asked for something other than an ugly case statement. You could use an enumerator that takes the current enum from that column to set the starting point, and the end point is 4 (or :four depending on how you write it) and have the rescue at the end of the enumerator return your max value.

Related

grouped_collection_select with I18n

I've been trying to come up with a way to declare array constants in a class, and then present the members of the arrays as grouped options in a select control. The reason I am using array constants is because I do not want the options being backed by a database model.
This can be done in the basic sense rather easily using the grouped_collection_select view helper. What is not so straightforward is making this localizable, while keeping the original array entries in the background. In other words, I want to display the options in whatever locale, but I want the form to submit the original array values.
Anyway, I've come up with a solution, but it seems overly complex. My question is: is there a better way? Is a complex solution required, or have I overlooked a much easier solution?
I'll explain my solution using a contrived example. Let's start with my model class:
class CharacterDefinition < ActiveRecord::Base
HOBBITS = %w[bilbo frodo sam merry pippin]
DWARVES = %w[gimli gloin oin thorin]
##TYPES = nil
def CharacterDefinition.TYPES
if ##TYPES.nil?
hobbits = TranslatableString.new('hobbits', 'character_definition')
dwarves = TranslatableString.new('dwarves', 'character_definition')
##TYPES = [
{ hobbits => HOBBITS.map {|c| TranslatableString.new(c, 'character_definition')} },
{ dwarves => DWARVES.map {|c| TranslatableString.new(c, 'character_definition')} }
]
end
##TYPES
end
end
The TranslatableString class does the translation:
class TranslatableString
def initialize(string, scope = nil)
#string = string;
#scope = scope
end
def to_s
#string
end
def translate
I18n.t #string, :scope => #scope, :default => #string
end
end
And the view erb statement look like:
<%= f.grouped_collection_select :character_type, CharacterDefinition.TYPES, 'values[0]', 'keys[0].translate', :to_s, :translate %>
With the following yml:
en:
character_definition:
hobbits: Hobbits of the Shire
bilbo: Bilbo Baggins
frodo: Frodo Baggins
sam: Samwise Gamgee
merry: Meriadoc Brandybuck
pippin: Peregrin Took
dwarves: Durin's Folk
gimli: Gimli, son of Glóin
gloin: Glóin, son of Gróin
oin: Óin, son of Gróin
thorin: Thorin Oakenshield, son of Thráin
The result is:
So, have I come up with a reasonable solution? Or have I gone way off the rails?
Thanks!
From the resounding silence my question received in response, I am guessing that there is not a better way. Anyway, the approach works and I am sticking to it until I discover something better.

Which is the best way to test if a model instance is "empty" in Ruby on Rails?

I want to implement a method that checks if a model's instance has only nil or empty attributes, except from its id or timestamps.
I've made use of an auxiliary method that removes a key from Hash and return the remaining hash ( question 6227600)
class ActiveRecord::Base
def blank?
self.attributes.remove("id","created_at","updated_at").reject{|attr| self[attr].blank?}.empty?
end
end
I guess that there may be much simpler, efficient or safer way to do this. Any suggestion?
def blank?
self.attributes.all?{|k,v| v.blank? || %w(id created_at updated_at).include?(k)}
end
My response is almost the same that tadman gave, but expressed in a more concise way.
Be careful with two situations:
- **blank?** is not a good choice as name, since if you call **object_a.object_b.blank?** trying to know if there is or not a object_b inside object_a, you'll get true event if the object exists. **empty?** seems a better name
- If databases sets defaults values, it can be tricky.
EDIT: Since build an array every iteration is slow (thanks tadman), a beter solution is:
def empty?
ignored_attrs = {'id' => 1, 'created_at' => 1, 'updated_at' => 1}
self.attributes.all?{|k,v| v.blank? || ignored_attrs[k]}
end
You could just check that all the properties in the attributes hash are not present, or the converse:
class ActiveRecord::Base
def blank?
!self.attributes.find do |key, value|
case (key)
when 'id', 'created_at', 'updated_at'
false
else
value.present?
end
end
end
end
Unfortunately this will not account for things that are set with a default in your database, if any relationship keys are assigned, among other things. You will have to add those as exceptions, or compare the values to a known default state of some sort.
This sort of thing is probably best implemented on a case by case basis.

Adding data of an unknown type to a hash

Ruby seems like a language that would be especially well suited to solving this problem, but I'm not finding an elegant way to do it. What I want is a method that will accept a value and add it to a hash like so, with specific requirements for how it is added if the key already exists:
Adding 'foo' to :key1
{:key1 => 'foo'}
Adding 'bar' to :key1
{:key1=> 'foobar'}
Adding ['foo'] to :key2
{:key2 = ['foo']}
Adding ['bar'] to :key2
{:key2 => [['foo'], ['bar']]
Adding {:k1 => 'foo'} to :key3
{:key3 => {:k1 => 'foo'}}
Adding {:k2 => 'bar'} to :key3
{:key3 => {:k1 => 'foo', :k2 => 'bar'}}
Right now I can do this but it looks sloppy and not like idiomatic Ruby. What is a good way to do this?
To make it more Ruby-like you might want to extend the Hash class to provide this kind of functionality across the board, or make your own subclass for this specific purpose. For instance:
class FancyHash < Hash
def add(key, value)
case (self[key])
when nil
self[key] = value
when Array
self[key] = [ self[key], value ]
when Hash
self[key].merge!(value)
else
raise "Adding value to unsupported #{self[key].class} structure"
end
end
end
This will depend on your exact interpretation of what "adding" means, as your examples do seem somewhat simplistic and don't address what happens when you add a hash to a pre-existing array, among other things.
The idea is that you define a handler that accommodates as many possibilities as reasonable and throw an exception if you can't manage.
If you want to utilize the polymorphic feature of oop, you might want to do:
class Object; def add_value v; v end end
class String; def add_value v; self+v end end # or concat(v) for destructive
class Array; def add_value v; [self, v] end end # or replace([self.dup, v]) for destructive
class Hash; def add_value v; merge(v) end end # or merge!(v) for destructive
class Hash
def add k, v; self[k] = self[k].add_value(v) end
end

Rails / ActiveRecord: field normalization

I'm trying to remove the commas from a field in a model. I want the user to type a number, i.e. 10,000 and that number should be stored in the database as 10000. I was hoping that I could do some model-side normalization to remove the comma. I don't want to depend on the view or controller to properly format my data.
I tried:
before_validation :normalize
def normalize
self['thenumber'] = self['thenumber'].to_s.gsub(',','')
end
no worky.
http://github.com/mdeering/attribute_normalizer looks like a promising solution to this common problem. Here are a few examples from the home page:
# By default it will strip leading and trailing whitespace
# and set to nil if blank.
normalize_attributes :author, :publisher
# Using one of our predefined normalizers.
normalize_attribute :price, :with => :currency
# You can also define your normalization block inline.
normalize_attribute :title do |value|
value.is_a?(String) ? value.titleize.strip : value
end
So in your case you might do something like this:
normalize_attribute :title do |value|
value.to_s.gsub(',', '')
end
I think you're doing it right. This test passes:
test "should remove commas from thenumber" do
f = Foo.new(:thenumber => "10,000")
f.save
f = Foo.find(f.id)
assert f.thenumber == "10000"
end
And I used your code.
class Foo < ActiveRecord::Base
before_validation :normalize
def normalize
self['thenumber'] = self['thenumber'].to_s.gsub(',','')
end
end
Now, my schema is set up for thenumber to be a string though, not an integer.
Started
.
Finished in 0.049666 seconds.
1 tests, 1 assertions, 0 failures, 0 errors
If you wanted to store this in the db as an integer, then you definitely need to override the setter:
def thenumber=(value)
self['thenumber'] = value.to_s.gsub(',','').to_i
end
If you do it your way, with an integer column, it gets truncated by AR....
>> f.thenumber = "10,000"
=> "10,000"
>> f.thenumber
=> 10
That's a little-known thing with Ruby and integers... it auto-casts by truncating anything that's no longer an integer.
irb(main):004:0> i = "155-brian-hogan".to_i
=> 155
Can be cool for things like
/users/155-brian-hogan
#user = User.find_by_id(params[:id])
But not so cool for what you're doing.
So either change the col to a string and use the filter, or change the setter :)
Good luck!
The problem with doing it that way is that for a while, the non-normalized stuff will exist in the object; if you have code that works on the attributes before stuff gets normalised, then that will be a problem.
You could define a setter:
def thenumber=(value)
# normalise stuff here, call write_attribute
end
Unfortunately I think a lot of the Rails form stuff writes the attributes directly, which is one of the reasons I don't tend to use it.
Or you could normalise the params in the controller before you pass them through.
Does ruby let you interchange between a . and [''] ?
I don't know, I'll try later, but I think you are supposed to use .
self.thenumber = self.thenumber.to_s.gsub(',','')
You should return true from your before_validation method, otherwise if the expression being assigned to self['thenumber'] ends up being nil or false, the data will not be saved, per the Rails documention:
If a before_* callback returns false,
all the later callbacks and the
associated action are cancelled.
Ostensibly, you are trying to normalize here then check the result of the normalization with your Rails validations, which will decide if nil/false/blank are okay or not.
before_validation :normalize
def normalize
self['thenumber'] = self['thenumber'].to_s.gsub(',','')
return true
end

What is the best way in Rails to determine if two (or more) given URLs (as strings or hash options) are equal?

I'm wanting a method called same_url? that will return true if the passed in URLs are equal. The passed in URLs might be either params options hash or strings.
same_url?({:controller => :foo, :action => :bar}, "http://www.example.com/foo/bar") # => true
The Rails Framework helper current_page? seems like a good starting point but I'd like to pass in an arbitrary number of URLs.
As an added bonus It would be good if a hash of params to exclude from the comparison could be passed in. So a method call might look like:
same_url?(projects_path(:page => 2), "projects?page=3", :excluding => :page) # => true
Here's the method (bung it in /lib and require it in environment.rb):
def same_page?(a, b, params_to_exclude = {})
if a.respond_to?(:except) && b.respond_to?(:except)
url_for(a.except(params_to_exclude)) == url_for(b.except(params_to_exclude))
else
url_for(a) == url_for(b)
end
end
If you are on Rails pre-2.0.1, you also need to add the except helper method to Hash:
class Hash
# Usage { :a => 1, :b => 2, :c => 3}.except(:a) -> { :b => 2, :c => 3}
def except(*keys)
self.reject { |k,v|
keys.include? k.to_sym
}
end
end
Later version of Rails (well, ActiveSupport) include except already (credit: Brian Guthrie)
Is this the sort of thing you're after?
def same_url?(one, two)
url_for(one) == url_for(two)
end
def all_urls_same_as_current? *params_for_urls
params_for_urls.map do |a_url_hash|
url_for a_url_hash.except(*exclude_keys)
end.all? do |a_url_str|
a_url_str == request.request_uri
end
end
Wherein:
params_for_urls is an array of hashes of url parameters (each array entry are params to build a url)
exclude_keys is an array of symbols for keys you want to ignore
request.request_uri may not be exactly what you should use, see below.
Then there are all sorts of things you'll want to consider when implementing your version:
do you want to compare the full uri with domain, port and all, or just the path?
if just the path, do you want to still compare arguments passed after the question mark or just those that compose the actual path path?

Resources