P.S: I've been working with Ruby for less than 48 hours, so expect me to be dumb!
I have set up validation on a model which all works fine. However I want to modify the message retuned by the uniqueness constraint. The message needs to be returned by a method which does some additional processing.
So for example if i try to create a new user model which the "name" attribute set to "bob", and the bob named account already exists, i want to display a message like "Bob isn't available, click here to view the current bob's account".
So the helper method will lookup the current bob account, get the localised string, and do the usual string placement of the localised string, placing the name, link to account and any other information i want into the message.
Essentially i want something like:
validates_uniqueness_of :text, :message => self.uniquefound()
def uniquefound()
return "some string i can make up myself with #{self.name}'s info in it"
end
No doubt that's completely wrong...
If this isn't possible, i've found i can use users.errors.added? to detect if the name attribute has a unique error added, from there i can probably do some hash manipulation to remove the unique error, and place my own in there either in an "after_validation" callback or in the controller... haven't worked out exactly how to do that, but that's my fallback plan.
So, is there a way of providing a class method callback for the message, AND either passing the current model to that method (if its a static method) or calling it so self inside that method is the model being validated.
Update
Trawling through the rails sourcecode, i have found this private method that is called during adding an error to the error class
private
def normalize_message(attribute, message, options)
case message
when Symbol
generate_message(attribute, message, options.except(*CALLBACKS_OPTIONS))
when Proc
message.call
else
message
end
end
end
So if i pass a method as the message, i presume that this method is used to call my function/method and get teh return data. However it appears it's not called in the scope of the current object, nor does it pass in the object for which the error pertains...
So, if my digging is on the right path, it appears that calling a method on the current object to the message, or calling a static method passing the object, is not possible.
You could do something like this if you really need this functionality.
Class User < ActiveRecord::Base
include Rails.application.routes.url_helpers
....
validate :unique_name
def unique_name
original_user = User.where("LOWER(name) = ?", name.downcase)
if original_user
message = "#{name} is not available "
message << ActionController::Base.helpers.link_to("Click Here",user_path(original_user))
message << "to view the current #{name}'s account."
errors.add(:name,message)
end
end
end
EDIT
with a Proc Object instead
Class User < ActiveRecord::Base
include Rails.application.routes.url_helpers
....
validates_uniqueness_of :name, message: Proc.new{|error,attributes| User.non_unique(attributes[:value]) }
def self.non_unique(name)
original_user = User.where("LOWER(name) = ? ", name.downcase)
message = "#{name} is not available "
message << ActionController::Base.helpers.link_to("Click Here",Rails.application.routes.url_helpers.user_path(original_user))
message << "to view the current #{name}'s account."
end
end
These will both add the following error message to :name
"Name Bob is not available Click Hereto view the current Bob's account."
Related
I am checking whether a book exists, and if so, render some action. I'd like this action to be a popup message or redirecting to another page.
Here's the code in my model:
def
existing_book = Book.find_by('author LIKE ? AND name LIKE ?', "#{self.author}", "#{self.name}")
if existing_book != nil
errors.add(:name, message: "This book may already exist.")
end
end
The problem is that besides errors.add to show the error text on the same screen, I can't get it to do popup message or redirect. redirect_to or such code that work in controller doesn't work in this model file. How can I do a popup message or redirect in this case?
If that is impossible, how can I at least make the error message show more than one model? Above, there's only :name, but I'd like to show both name and author. Right now the error message shows as: "Name This book may already exist."
I'd really appreciate any help.
The User model has an id column that is used all throughout your schema, but you'd like to create other models using the username rather than the id:
User.create!(username: "phillip")
User.create!(username: "joe")
Message.create!(from_username: "phillip", to_username: "joe", message: "hello")
Here, 'from_username' and 'to_username' don't actually exist on the table. Rather, there is a 'from_id' and 'to_id'. You can add 'from_username' and 'to_username' as methods to the model, but then you can't create a new Message using them.
What is the recommended way to add these 'virtual attributes' to a model?
Most idiomatic way
You could define a custom create function, like this:
class Message < ActiveRecord::Base
def self.create_from_usernames(from_username, to_username)
from_user = User.find_by_username(from_username)
to_user = User.find_by_username(to_username)
self.create(from_user: from_user, to_user: to_user)
end
end
Making call-site code handle this itself
This is still somewhat idiomatic, but not very DRY.
from_user = User.find_by_username("phillip")
to_user = User.find_by_username("joe")
Message.create!(from_user: from_user, to_user: to_user)
Using create directly
This is not really recommended, but you could do something like this:
class Message < ActiveRecord::Base
attr_accessor :from_username, :to_username
before_create :find_users
private
def find_users
self.from_user = User.find_by_username(from_username)
self.to_user = User.find_by_username(to_username)
end
end
This leads to somewhat "magical" behavior though, and there should be more error checking / handling if you're going to do it.
I think the normal way to do that would be to include these attributes into your Message model. So instead of saving the user_id you can also save the user_name. Otherwise, just work around it and get the username by the user_id. May be I did not fully understand your question, though..
A model method starts with the following logic:
def calculate_vat
if self.country.blank?
flash[:danger] = "Please select country first"
redirect_to edit_organization_path(self) and return
end
if ["BE", "CZ", "ES", "LV", "LT"].include? self.country
return 0.21
elsif etc. for other countries.
end
end
Is this not possible within a model method? In the controller, the model method gets called upon using #organization.model_method. It produces the following error in development: undefined local variable or method 'flash' for #<Organization:0x00000007298a68>.
Update: I understand now that it's impossible to use flash and redirect in model method. But how to solve this? Only a specific group of users will ever deal with this model method; so I don't want to make country required for all users. A custom requirement seems impossible because there are no model variables upon which to base such a validation, it's a matter whether a user will ever get to certain checkout pages.
Would the best solution perhaps be to define a private controller method that is called upon from inside the controller just before calculate_vat is called upon?
This is senseless.
Fire the desired flash message in the controller's action after the method is being called and that's it.
Actually, everything you do in the model_method is a pure validation, so just define it properly:
validates :country, presence: true
For example I have a user registering, during registration all thats needed is a password and username. As soon as they click "shop", they must input address and city to continue.
At the moment every user must input username, password, city, and address to register. How do I split this and only require it if the user clicks "shop"?
I'm using devise.
I'd like to send the user to a page saying, "if you would like to continue, please let us know your address and city"
Just add a method to your model called can_shop? or similar checking if all required fields are given:
def can_shop?
[city, address].all?(&:present?)
end
Then you can use this either to disable the button or to create a before_action in your controller:
before_action :check_if_can_shop
private
def check_if_can_shop
return if current_user.can_shop?
redirect_to edit_user_path(current_user), notice: "if you would like to continue, please let us know your address and city"
end
Naturally we are left with an UX element - you will need to mark those fields in the form as "optional, but required to shop". Later on you could pass original_url as an extra param, which you can then use to redirect user back to when he completed all the required fields.
Another thing is you could use validation contexts and use the right context in your user_update action - that way once you redirect user back to form, he will see proper validation errors when he miss normally-optional-but-now-required fields.
You can do that by nested forms for that and do custom validations in rails.
Ex:
class User < ActiveRecord::Base
validates :my_method_name, if: :page_reached?
def page_reached?
-- do stuff for page number --
end
def my_method_name
errors.messages :base, "Cities should not be empty."if cities.empty?
end
def my_method_name
errors.messages :base, "Address should not be empty."if address.empty?
end
end
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.