Ruby on Rails get next enum from current model's enum value - ruby-on-rails

I'm using an enum in my model defined as such:
enum role: [:member, :content_creator, :moderator, :admin]
I wanted an easy way to get the next role from a user's current role, so I came up with this:
def self.next_role(user)
begin
User.roles.drop(User.roles[user.role] + 1).to_enum.next
rescue StopIteration
nil
end
end
In the view, I'd likely add onto this the following method chain: [...].first.humanize.titleize.
I'm only somewhat concerned about my solution here, but mainly wanted to know if there was a better (read: more built-in) way to get what I'm after? I know there are only four enums there and I admit I started my implementation with if ... elsif ... etc.. In other words, I find myself more proficient in Rails than I do with Ruby itself. Can someone elaborate how one "should" do this?

User.roles is just an ActiveSupport::HashWithIndifferentAccess that looks like:
{ 'member' => 0,
'content_creator' => 1,
'moderator' => 2,
'admin' => 3 }
Using that, this solution is pretty close to yours but without the exception handling. I would also be doing this as an instance method on User, not a class method.
Returns the subsequent role as a String, nil if the current role is :admin
def next_role
User.roles.key(User.roles[role] + 1)
end
You can then call (ruby 2.3 required for the &. safe navigation operator)
user.next_role&.humanize

How's this grab you? Okay so it's not really more "built in." But I thought it was different enough from your solution (which was pretty clever imo) to throw into the mix, at least to offer some different elements to potentially incorporate.
def self.next_role(user)
next_role = user.role.to_i + 1
next_role == self.roles.length ? nil : self.roles.invert[next_role]
end

Related

rails 5 update attribute only if it's currently nil

What is the preferred way in Rails 5 with activerecord to update the attribute only if it is currently nil.
car = Car.first
car.connected_at = Time.zone.now
car.save
OR
car = Car.first
car.update!(connected_at: Time.zone.now)
it should update only if car.connected_at is nil
You can simply check for #nil?
car = Car.first
car.update_attribute(:connected_at, Time.zone.now) if car.connected_at.nil?
That's not generic enough. I want something like before_validation etc. I am just not sure which way is the preferred one.
Well if you want to go for validation, it would look something like this..
before_save :validate_connected_at
private
def validate_connected_at
connected_at = connected_at_was if connected_at_changed? && connected_at_was.present?
end
OR
before_save :set_connected_at
private
def set_connected_at
connected_at = Time.zone.now if connected_at.nil?
end
As you can see, more checks, more methods. I would definitely go for the first one.
However, if you want to add error message, then this is the way
errors.add(:connected_at, 'Already present!')
So "#{attr}_was" is always available on all the defined attrs in before_save method?
They are available in general and not only in before_save, e.g. in the console..
car = Car.first
car.connected_at
=> 'some value'
car.connected_at = 'some other value'
car.connected_at
=> 'some other value'
car.connected_at_was
=> 'some value'
It sounds like you're saying you want to modify the behaviour of how a particular attribute works so it quietly ignores you. I think the instinct behind why you want to seal this off is reasonable one but if you think about it a bit more you might consider that if you do this kind of thing in a lot of places then using your objects will start to become confusing particularly for someone else who doesn't know the code well.
Perhaps you want to do this because there's other code using the Car model that wants to make connections but doesn't really have the full picture so it tries stuff which you only want to succeed the first time. It's much better to handle such operations solely inside a class which does have the full picture such as the Car model or a service object.
If you still really want to control this "connecting" behaviour outside the Car then you can override the attr_writer completely in the Car class. I'd definitely recommend doing this on before_save callback instead though.
def connected_at=(new_value)
if #connected_at
raise StandardError, 'connected_at has already been set'
end
#connected_at = new_value
end
That will work whichever way you try to assign the value. If you're wondering about what's going on above have a read about attr_accessor in ruby.
this is my understanding of your question.
Car can update only if connected_at is nil
class Car < ApplicationRecord
before_save :updatable?
def updatable?
connected_at.blank?
end
end
The point is return false when before_save.
You could:
car = Car.first
car.connected_at ||= Time.zone.now
car.save
That will only assign if connected_at is nil of false.
I would propose to use the before_update callback and rephrase the intention of the OP as "discard updates if my attribute already has a value".
I came up with this solution (which works well with mass assignments such as Car.update(car_params)):
before_update :ignore_updates_to_connected_at
def ignore_updates_to_connected_at
return unless connected_at.present? && connected_at_changed?
clear_attribute_change(:connected_at)
end
The <attribute_name>_changed? and clear_attribute_change methods come from ActiveModel::Dirty.

Is this caused by attr_accessible?

I just lately update my model with attr_accessible fields and suddenly some tests would not work, as i would expect. However, i have a spec like:
context "when user buys a game item" do
let(:inventory) {#user.inventory << Factory(:inventory)}
it "should present an error if the id ..." do
GameItem.stub(:find_by_id).and_return(Factory(:game_item))
#user.inventory.should == 1 # TEST
post :buy, :id => (game_item.id + 1)
flash[:error].should == I18n.t('error.invalid_post')
response.should redirect_to melee_url('Weapon')
end
end
The line #user.inventory.should == 1 is just a check that i made now. The inventory is nil for some reason. Does this happen because of the << operation? I would guess that this is most probable, due to the inventory_id attribute of the User model.
I have to say that attr_accessible generally seems like a hack to me and i kinda don't like it, though i can see why it should be used. Do you think this is the case? If so, how can i stay clear of that check?
let is lazy; it won't call the block unless the variable you're defining is used, and I don't see you accessing inventory anywhere. You access #user.inventory, but that's not the same thing.
Either lose the let definition and just put it in your it block, or make sure you call it first before you make sure it did what it was supposed to.

Rails - Allowing for calling in Model Validation in a Controller

I have the following in my user.rb model:
INVALID_EMAILS = %w(gmail.com hotmail.com)
validates_format_of :email, :without => /#{INVALID_EMAILS.map{|a| Regexp.quote(a)}.join('|')}/, :message => "That email domain won't do.", :on => :create
For various reasons, I want to be able to use this logic in my controller to check an email's input before it is user.created, which is when the above normall runs.
How can I turn the above into a method that I can call in controllers other than user? Possible?
And if is called and returned false I then want to do errors.add so I can let the user know why?
Thanks
Trying:
def validate_email_domain(emailAddy)
INVALID_EMAILS = %w(gmail.com googlemail.com yahoo.com ymail.com rocketmail.com hotmail.com facebook.com)
reg = Regexp.new '/#{INVALID_EMAILS.map{|a| Regexp.quote(a)}.join('|')}/'
self.errors.add('rox', 'Hey, Ruby rox. You have to say it !') unless reg.match attribute
end
Update:
..
Rails.logger.info validate_email_domain(email)
...
def valid_email_domain(emailAddy)
reg = Regexp.new '/#{User::INVALID_EMAILS.map{|a| Regexp.quote(a)}.join("|")}/'
return true if emailAddy.scan(reg).size == 0
end
You cannot assign a constant inside a method, because that would make it "dynamic constant assignment". Instead, define this constant in your model class and then reference it in your controller by using User::INVALID_EMAILS
Okay, if I understand you.
You want to do something like below:
u = User.new
u.email = "jsmith#gmail.com"
if !u.valid?
puts u.errors.to_xml
//do something
return
end
What you do with those errors is going to come down to how you want those reported back, usually I just shoot them back as xml into a flash[:error], which is the normal default behavior if you're doing scaffolds. The puts is there so you can see how to access the errors.
Additional
As a rule try to avoid duplicating validation logic. Rails provides everything you need for validating without creating different methods in different places to accomplish the same thing.

Using blocks to define abilities in CanCan raises an exception

I am using Ryan Bate's CanCan gem to define abilities and some basic functionality is failing. I have a Product model and Products controller where my index action looks like this:
def index
#req_host_w_port = request.host_with_port
#products = Product.accessible_by(current_ability, :index)
end
I get an error when I try to retrieve the products with the accessible_by method, I get this error:
Cannot determine SQL conditions or joins from block for :index Product(id: integer, secret: string, context_date: datetime, expiration_date: datetime, blurb: text, created_at: datetime, updated_at: datetime, owner_id: integer, product_type_id: integer, approved_at: datetime)
My ability class looks like this:
can :index, Product do |product|
product && !!product.approved_at
end
This seems like a very simple example so I am surprised it is failing and wondering if I am overlooking something simple (i.e. staring at my code for too long).
I did probe a bit more and ran a simple test. If you look at the code below, one example works fine and one fails where they should actually do the same exact thing.
# This works
can :index, Product, :approved_at => nil
# This fails
can :index, Product do |product|
product && product.approved_at.nil?
end
So the problem seems to be in the way CanCan is processing these blocks. I dove deeper into the library and found where the error was raised - in CanCan's ability class definition:
def relevant_can_definitions_for_query(action, subject)
relevant_can_definitions(action, subject).each do |can_definition|
if can_definition.only_block?
raise Error, "Cannot determine SQL conditions or joins from block for #{action.inspect} #{subject.inspect}"
end
end
end
So I checked out what this only_block? method is. The method returns true if a can definition has a block but no conditions which makes no sense to me because the whole point of the block is to define the conditions within the block when they are too complicated for the following syntax:
can :manage, [Account], :account_manager_id => user.id
Any insight on this issue would be great! I also filed an issue on the CanCan github page but I'm getting to the point where I may need to ditch the library. However, I understand that many people are using CanCan successfully and this is such basic functionality I think I must be doing something wrong. Especially since the git repo was updated 3 days ago and Ryan Bates mentions Rails 3 support in the README.
Thanks!
Ok so I went through the wiki and saw that accessible_by will not work when defining blocks. Seems a bit odd to have this restriction and not mention it in the README, but at least I know it's a limitation of the library and not a bug in mine or Ryan Bates' code. For those interested, the proper way to define my ability above is as follows:
# will fail
can :index, Product do |product|
product && product.approved_at.nil?
end
# will pass
can :index, Product
cannot :index, Product, :approved_at => nil
I've run into this problem as well, when sometimes you can only express an ability/permission via a block (Ruby code) and can't express it as an SQL condition or named scope.
As a workaround, I just do my finding as I would normally do it without CanCan, and then I filter that set down using select! to only contain the subset of records that the user actually has permission to view:
# Don't use accessible_by
#blog_posts = BlogPost.where(...)
#blog_posts.select! {|blog_post| can?(:read, blog_post)}
Or more generically:
#objects = Object.all
#objects.select! {|_| can?(:read, _)}
I filed a ticket to get this added to the documentation:
https://github.com/ryanb/cancan/issues/issue/276

Rails: Why does the model method not work?

its the first time I post here. I have a problem that i can somehow not solve. Just for the record, I know what instance and class methods are (even if I may not understand them completely ;-)
Here is my model code:
class Car < ActiveRecord::Base
has_many :drives
has_many :users, :through => :drives
def self.user_ids()
ids = []
self.users.each do |user|
ids += user.id
end
ids
end
def self.common_times()
start_times = []
stop_times = []
self.drives.each do |drive|
drive.start_date_time += start_times
drive.stop_date_time += stop_times
end
times = { :start => start_times.sort.last, :stop => stop_times.sort.first}
end
what I want is an array of all users using the car (which I use to check if a given user is already connected to the car for permissions etc.. Is there a better way to check if two datasets are already connected without doing SQL queries all the time?) and which start and stop times they prefer. I need than a hash with the latest starting time and the earliest stop time.
Somehow the user_ids method works (even if I think it should be an instance method) and the common_times is always missing. if I define them both as an instance method I have problems with fixnum and array stuff (like "+").
user_id:
"TypeError: can't convert Fixnum into Array"
common_times:
"NoMethodError: You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occurred while evaluating nil.+"
I guess the best way is to make them instance methods. But then I need to refer differently to the other models as users and drives.
Why does user_ids work as an instance method even if declared as a class method?
How do I call already loaded models [c=Car.find(:all, :include => :drives)] inside an instance method?
Funny thing was also, that as long as they were class methods I could delete them and restart mongrel and they would still work (user_ids) and not work (common_times).
I am pretty confused right now and hop you can help me. And sorry for my bad english (I am german :-)
Because of your users association, Rails already pre-builds the user_ids and users instance methods. Using #car.users should be your best bet.
As for instance and class methods: instance methods are for specific objects, whereas class methods are just put under the class name for your convenience. #car.id is an instance method, since it returns the ID of a single car. Cars.find is a class method, since it is independent of any single object and instead is grouped under the Cars class for organizational purposes. (It could just as easily be its own global find_cars method and work just as well, though it would be horrible design.)
So both of your methods should be instance methods, and the first one Rails creates for you because it loves you so much.
As for your individual errors, adding objects to an array is done with the << operator, not the plus sign. Ruby thinks you are trying to add two arrays, so is confused why you are trying to use a Fixnum in the same way you would typically use an array. Try making those fixes and see if you still get errors.
Got it to work (thnx #Matchu). Here is the final code for the method (no self-made user_ids anymore ;-)
def common_times()
start_times = []
stop_times = []
drives.each do |drive|
start_times << drive.start_date_time
stop_times << drive.stop_date_time
end
times = { :start => start_times.sort.last, :stop => stop_times.sort.first}
end
the biggest error was the switched start_times << drive.start_date_time
Really silly error..
thanks again!

Resources