rails 4 complex model validation - ruby-on-rails

I have a rails app. Assigner(current_user) must assign a task to executor(other user). I'm using getter setter method for jquery autocomplete and with the validation so I can make sure if there is an existing user who the task is assigned to. I'm using the rescue set to nil so either if the field empty or the user is non-existing can be validated with presence of. I'd like to change this, so users either could leave the field empty or choosing from existing users. As I'm validating the executor object I'm not sure how I can do that.
task.rb
class Task < ActiveRecord::Base
belongs_to :assigner, class_name: "User"
belongs_to :executor, class_name: "User"
validates :assigner, presence: true
validates :executor, presence: { message: "must be valid"}
def task_name_company
[executor.try(:profile).try(:first_name), executor.try(:profile).try(:last_name), executor.try(:profile).try(:company)].join(' ')
end
def task_name_company=(name)
self.executor = User.joins(:profile).where("CONCAT_WS(' ', first_name, last_name, company) LIKE ?", "%#{name}%").first if name.present?
rescue ArgumentError
self.executor = nil
end

Related

Rails - new User builds dependent Email and validates both?

I'm building a quick Rails project that allows users to manage their email addresses. Users can have many emails, but one (and only one) of those emails has to be marked as 'primary' (for login), and a user cannot exist without a primary email.
I've been struggling to get this to work right - it seems so circular to me. I need to build a User, and then the Email, but I don't want to save the User into the database unless the Email is valid, which it won't be until the User is saved (because of the validates :user, presence: true constraint).
Accepts nested resources for doesn't seem to work with .new (works fine with .create), and if my Email fails its validations, the User still shows as valid.
Been having a difficult time trying to find good resources (or SO questions) for building/validating multiple/dependent models from a single form.
What's the most Rails way to do this?
User
has_many :emails
has_one :primary_email, -> { where(primary: true) }, class_name: "Email"
accepts_nested_attributes_for :primary_email
validates :first_name, presence: true
validates :last_name, presence: true
validates :birthday, presence: true
validates :password_digest, presence: true
Email
belongs_to :user
validates :user, presence: true
validates :address, presence: true, uniqueness: {
case_sensitive: false
}
UsersController
def new
#user = User.new
end
def create
#user = User.new(user_params)
if #user.save
# do something
else
# show #user.errors
end
end
private
def user_params
params.require(:user).permit(
:first_name,
:last_name,
:birthday,
:password,
:password_confirmation,
:primary_email_attributes => [:address]
)
end
EDIT
The Email model also contains the following fields:
label = string, eg. 'Personal', 'Work', etc
primary = boolean, whether it's marked as primary email or not
confirmation_code = autogenerated on creation, used to confirm ownership
confirmed = boolean, whether it's been confirmed or not
class User
user has_many :emails
user has_one :primary_email, -> { where(primary: true) }, class_name: "Email", autosave: true
after_initialize {
build_primary_email if new_record?
}
end
class Email
# use gem https://github.com/balexand/email_validator
validates :my_email_attribute, :email => true
end
So after a user initialized its building a primary_email so that record is already associated, or at least it will be if it can be saved. the autosave is working pretty cool - if the primary-email can't be saved due validation error, the user can't neither. should work out of the box, im in a bus right now, can't check it. cheers
futher information: http://api.rubyonrails.org/classes/ActiveRecord/AutosaveAssociation.html
If validations for any of the associations fail, their error messages will be applied to the parent. That means, the Parent Model (in your case User) is having errors, and thats why the saving is not possible! that's what you are looking for.
I would store a primary email as a common field and additional emails some another way. I would prefer to store additional emails in another field too that is Array rather than in an associated table. You shouldn't store a primary email in another table. Just imagine, every time you need authorize user or just get his email you will perform an extra request to db.
Meant to post this months ago.
The solution, keeping users and emails normalized across different models without storing a primary email as an attribute on the user, is to use inverse_of:
User.rb
class User < ActiveRecord::Base
has_many :emails, inverse_of: :user, dependent: :destroy
accepts_nested_attributes_for :emails
validates :emails, presence: true
end
Email.rb
class Email < ActiveRecord::Base
belongs_to :user, inverse_of: :emails
validates :user, presence: true
end
This allows validations to be performed using in-memory objects, rather than via database calls (ie the object associations are being validated, rather than the presence of an id/record in the database). Therefore they both pass validation and can both be saved in the same transaction.
See: https://viget.com/extend/exploring-the-inverse-of-option-on-rails-model-associations

Rails: destroying multiple user-to-user associations

I have a model called Block that has a blocker_id (a user_id) and a blocked_user_id field (also a user_id). The Block model lets one user block another. When one user blocks another, I want it to destroy the Relationship between them using a before_save method for the Block class. The Relationship table has a follower_id and a followed_id.
This is where things get tricky. I know I could achieve this goal by using multiple return if Relationship.xyz.nil? statements and then using multiple Relationship.find_by(follower_id: , followed_id: ).destroy statements, but this gets to be way over complicated because each blocker and blocked_user could be either the follower and followed id, both, or neither. Is there any easier way to do this?
Here's my models for reference: (also the Block class has a blocked_post field, which I'm having no trouble with)
class Block < ActiveRecord::Base
validates :blocker_id, presence: true
validates :blocked_user_id, uniqueness: {scope: :blocker_id}, allow_nil: true
validates :blocked_post_id, uniqueness: {scope: :blocker_id}, allow_nil: true
validate :blocked_user_or_post
after_validation :validate_block
before_save :destroy_blocked_relationships
belongs_to(
:blocker,
class_name: "User"
)
has_one(
:blocked_user,
class_name: "User"
)
has_one(
:blocked_post,
class_name: "Post"
)
private
def blocked_user_or_post
blocked_user_id.blank? ^ blocked_post_id.blank?
end
def validate_block
if blocked_user_id.present?
!(blocker_id == blocked_user_id)
elsif blocked_post_id.present?
blocked_post = Post.find_by(id: self.blocked_post_id).user_id
!(blocker_id == blocked_post)
else
false
end
end
def destroy_blocked_relationships
#my over-complex code was here
end
end
relationship.rb:
class Relationship < ActiveRecord::Base
validates :follower_id, :followed_id, presence: {message: 'Need an eligible follower and followee id'}
validates :followed_id, uniqueness: { scope: :follower_id}
belongs_to(
:follower,
class_name: "User"
)
belongs_to(
:followed,
class_name: "User"
)
end
If there is any way to do this that doesn't require massive amounts of code, I'd really like to know. Thanks in advance.
I'm not sure of your exact use case, but my thoughts about a system where people can follow each other, it seems that the blocker would always be the person being followed. If this is the case, here's an implementation:
def destroy_blocked_relationships
Relationship.where(follower_id:blocked_user_id, followed_id:blocker_id).destroy_all
true
end
If it makes sense to also block someone from being being followed, you could add this:
Relationship.where(follower_id:blocker_id, followed_id:blocked_user_id).destory_all
Here it is all together, and stopping the save of the Block if there are no relationships:
before_save :destroy_blocked_relationships
def destroy_blocked_relationships
relationships = Relationship.where("(follower_id = ? AND followed_id = ?) OR (followed_id = ? AND follower_id = ? )", blocked_user_id, blocker_id, blocked_user_id, blocker_id)
relationships.destroy_all
relationships.present? # Omit this line if the save should continue regardless
end
here is my understanding:
a Block is a relationship between two users, OR between a User and a Post
when a Block is created between user A and Post X, an implicit block is also created between User A and User B, where User B is Post X's author
Consider making two models, BlockedPost and BlockedUser. Then, make two #make methods. This makes all the related logic easier to reason about.
# class BlockedPost
def make(user, post)
transaction do
create!(user: user, post: post)
BlockedUser.make(user, post.author)
end
end
# class BlockedUser
def make(user, blocked_user)
transaction do
create!(user: user, blocked_user: blocked_user)
Relationship.where(follower: user, following: blocked_user).destroy_all
Relationship.where(follower: blocked_user, following: user).destroy_all
end
end

Rails 3: validates_presence_of validation errors on default value and in associated model

I have a basic invoice setup with models: Invoice, Item, LineItems.
# invoice.rb
class Invoice < ActiveRecord::Base
has_many :line_items, :dependent => :destroy
validates_presence_of :status
before_save :default_values
def default_values
self.status = 'sent' unless self.status
end
end
# item.rb
class Item < ActiveRecord::Base
has_many :line_items
validates_presence_of :name, :price
end
# line_item.rb
class LineItem < ActiveRecord::Base
belongs_to :item
belongs_to :invoice
before_save :default_values
validates_presence_of :invoice_id
validates :item_id, :presence => true
end
There is more in the model but I only presented the above for simplicity.
I get the following errors:
2 errors prohibited this invoice from being saved:
Line items invoice can't be blank
Status can't be blank
So two problems:
If I remove validates :invoice_id, :presence => true I don't get the Line items invoice can't be blank error message anymore, but why? I do want to validate the invoice_id on line_items, ALL line_items are supposed to have an invoice_id. How can I validate the invoice_id on line_items without getting an error?
Why am I getting the Status can't be blank error if I set it as a default value? I can probably set it up on the invoices_controller but I think the default value should be set in the model, right? How can I validate the presence of status and still have a default value in the model for it?
Both of these validation errors are occurring because validations get called before save (and before the before_save callback).
I'm assuming that you're using a nested_form to create the invoice and it's line items at the same time. If this is the case, you don't want to validates :invoice_id, :presence => true on the line items - the invoice and the line items are coming in at the same time, and the invoice hasn't been saved yet, so it doesn't have an id. If you leave the validation in, you'll need to create and save an empty invoice first, and then create the line items later so the invoice_id is available. If you only want to make sure invoice_id is still set after any edits, you can enforce this via validates :invoice_id, :presence => true, :on => :update this will skip the validation when the line item is being created (and the invoice_id isn't available yet).
You're running into problems with validates :status, :presence => true for similar reasons - the values coming in via the request are being validated against, and the "status" value isn't there. The before_save callback runs after validation. You can set the default value in the before_validation or after_initialization callback and the values will be there when validations are run.
Check out the Callbacks documentation for Rails for more info.
I'll start with 2:
before save is being executed only before save, meaning, after the object passed validation and is about to be saved. If the validation fails - it won't be executed.
as for 1:
Can you give an example of how you're trying to create an invoice?
Problem 1
Try validates_associated which checks that the associated models are all valid
Problem 2
Like most of the answers say before_save gets called after validations. The magic you're looking for is after_initialize which gets run after an object's initialize method is called.
class Invoice < ActiveRecord::Base
after_initialize :default_values
validates :status, presence: true
private
def default_values
self.status ||= 'sent'
end
end

rails validate a belongs_to relation

Given a simple relationship where Person has_many Telephones. And a telephone only contains a telephonenumber which must be unique!
class Telephone < ActiveRecord::Base
validates_presence_of :contact_id
belongs_to :contact
validates :telephone, {:presence => true, :uniqueness => true}
end
class Contact < ActiveRecord::Base
has_many :telephones
validates_associated :telephones
has_many :emails
has_many :addresses
validates_presence_of :firstname
accepts_nested_attributes_for :telephones, :allow_destroy=>true
validates_presence_of :lastname
end
test "telephone number must be unique" do
john = contacts :johndoe #johndoe is a person with 1 existing number
2.times do
john.telephones.build :telephone=> "123" # 123 doesnt exist yet
end
puts Telephone.count # this gives 1
john.save
puts Telephone.count # this gives 3 !!!! ???
assert not(john.valid?) # This validates unless I remove the save above
end
Can someone explain the outcome of this test.
just calling valid? fails, but that is mentioned in the rdoc (must save first)
saving first does make valid? pass
BUT now I actually have 3 records in the database which breaks my unique requirement.
Is there a better way to do this? I don't understand the outcome of this test, it really goes against my expectations.
Ok if you read the ruby documentation you will notice that they mention that validating a model is not sufficient for uniqueness. YOU MUST use database unique constraints whenever possible. Otherwise it is possible when using two processes/threads/whatever that both will do a validation check, pass as unique, and then insert same values.
tl;dr: Add a unique constraint to the db column.

Rails setting OR conditions in validate_presence_of in a model?

In a rails model, is it possible to do something like
class Example < ActiveRecord::Base
#associations
validates_presence_of :item_id, (:user_id OR :user_email)
#functions
end
Where the model has 3 columns of :item_id, :user_id, and :user_email?
I want the model to be valid as long as I have a :user_id or a :user_email.
Idea being that if the item is recommended to a person who isn't currently signed up, it can be associated via email address for when the recommended person signs up.
Or is there a different method that I can use instead?
One approach is to wrap those fields as a virtual attribute, say:
class Example < ActiveRecord::Base
validates_presence_of :referral
def referral
user_id || user_email
end
end
or you can just throw a custom validate validation method. See custom validations on the Rails API
If both user_id and user_email come from another model, perhaps it's better to add the association instead
class Example
belongs_to :user
validates_associated :user
before_validate :build_user_from_id_or_email
def build_user_from_id_or_email
# ... Find something with the parameters
end
end
validates_presence_of :item_id
validates_presence_of :user_id, :if => Proc.new{ |x| x.user_email.blank? }
validates_presence_of :user_email, :if => Proc.new{ |x| x.user_id.blank? }

Resources