Rails uniqueness validation with nested has_many - ruby-on-rails

I am building a Polls website and I want to achieve that a user (ip address) can vote once per poll. I have the following models:
class Poll < ApplicationRecord
has_many :answers, :dependent => :destroy
accepts_nested_attributes_for :answers, allow_destroy: true
end
class Answer < ApplicationRecord
belongs_to :poll
has_many :votes
end
class Vote < ApplicationRecord
validates_uniqueness_of :ip_address
belongs_to :answer
end
With my current solution a user can vote once on one poll and then he can't vote on another.
What should I use to validates_uniqueness_of :ip_address per poll?
I tried to use scope by poll but it didn't work.
PS: I know IP address is not the best solution for validating unique votes.

You need scoped uniqueness
validates :ip_address, uniqueness: { scope: :answer_id }
It will not allow a duplicate ip_address per answer
https://guides.rubyonrails.org/active_record_validations.html#uniqueness
You can also enforce it at database level by adding this to migration, this is optional though
add_index :votes, [:ip_address, :answer_id], unique: true
But the above solution will only keep user from not voting for an answer more than once. If you want uniqueness with poll, one option is to save poll_id in answers table as well and then define uniqueness on that,
or, second option is to define custom validation in Vote model
validate :uniqueness_with_poll
def uniqueness_with_poll
errors.add(:answer_id, 'already voted for poll') if answer_for_poll_exists?
end
def answer_for_poll_exists?
answer_voted = Vote.where(ip_address: ip_address).pluck(:answer_id)
return false if answer_voted.blank?
answer.poll.answers.where(id: answer_voted).exists?
end
Then defining a method answer_for_poll_exists? to check whether poll is already voted, but this could be costly though.
Hope that helps!

Related

Rails: Validating an array of ids

Application
I am working on a college admissions system where a student can make an application to up to 5 courses. The way I have designed this is to have an Application model and a CourseApplication model. An application can consist of many course_applications:
class Application < ActiveRecord::Base
# Assosciations
belongs_to :user
has_many :course_applications, dependent: :destroy
has_many :courses, through: :course_applications
has_one :reference
# Validations
validates :course_applications, presence: true
end
Course Application
class CourseApplication < ActiveRecord::Base
# Intersection entity between course and application.
# Represents an application to a particular course, has an optional offer
# Associations
belongs_to :application
belongs_to :course
has_one :offer, dependent: :destroy
end
I want to make sure that a student cannot apply to the same course twice. I have already done some research but have had no success. This is the UI for a student making an application:
Screenshot of application form
When a course is selected, the course id is added to an array of course ids:
def application_params
params.require(:application).permit(:user, course_ids: [])
end
Right now a student can select the same course twice, I want to prevent them from doing this. Any help is much appreciated.
For the rails side, I would do on the CourseApplication
validates :course, uniqueness: { scope: :application }
For your reference this can be found at: http://guides.rubyonrails.org/active_record_validations.html#uniqueness
Also suggest on the database side to make a migration
add_index :course_applications, [:course, :application], :unique => true
For the validating on the form you will have to write javascript to make sure two things are selected, this will just return an error when someone tries to do it.

validating uniqueness of has_many association with a string "checksum"

I had an idea to validate the uniqueness of a has_many association: what if we generate a string based on the ids of the associated records?
For example:
class Exam
has_many :problems #problems are unique and can be in multiple exams
validate :checksum, uniqueness: true #string
before_validate :check
def check
checksum = problems.map {|p| p.id}.join
end
end
The edge case we want to solve is:
Given distinct problems 3x4, sqrt(4), 5+5, etc.., we don't want all of them to be in more than one exam.
Does anyone have thoughts on this approach? Is there a better way to validate uniqueness of has_many?
(P.S. I'm not sure if "checksum" is the right term.)
Base on the following:
You have an Exam model and a Problem model
Each Exam has_many Problems
Each problem must be unique per Exam
I think it makes more sense to place a uniqueness validation on an attribute of Problem but scope the validation to its Exam so that that multiple Exams can have the same Problems but each Exam has a unique set of Problems.
So for example if there was an attribute named value, we place the uniqueness validation on it and scope it to exam_id.
class Exam < ActiveRecord::Base
has_many :problems
end
class Problem < ActiveRecord::Base
belongs_to :exam
validates :value, :uniqueness => { :scope => :exam_id }
end

Nested form and has_many :through

I have a little sample app where there are 3 models: Members, Groups and Subscriptions. The idea is that member can subscribe to groups.
class Member < ActiveRecord::Base
has_many :subscriptions, dependent: :delete_all
has_many :groups, through: :subscriptions
attr_accessible :email
validates :email, presence: true
end
class Group < ActiveRecord::Base
has_many :subscriptions, dependent: :delete_all
has_many :members, through: :subscriptions
accepts_nested_attributes_for :subscriptions
attr_accessible :name, :subscriptions_attributes
validates :name, presence: true, uniqueness: true
end
class Subscription < ActiveRecord::Base
belongs_to :group
belongs_to :member
attr_accessible :group_id, :introduction
validates :group_id, presence: true
validates :introduction, presence: true
end
I'm trying to create a form for new groups, and nest the introduction attribute inside.
My controller methods:
def new
#group = Group.new
#group.subscriptions.build
end
def create
#member = Member.first
#group = #member.groups.build(params[:group])
if #group.save
flash[:success] = "Saved"
redirect_to group_path(#group)
else
render :new
end
end
But it does not work. It throws the error group_id can't be blank. So I don't know how to assign the new group to the subscription.
Also, the member_id is being created as nil. But as you can see, I'm creating the group from the #member variable, so I think it should be initialized, but it does not.
Anyone can show me the light?
You can see the sample app here: https://github.com/idavemm/nested_form
Make sure all the attributes you're trying to assign are attr_accessible. You may just disable it and see if it works, or see at the warnings in the Rails server log.
Update: you should add accepts_nested_attributes_for to the Member model and use a multimodel form with fields_for.
I think you're thinking about your models in the wrong way. Will each member have a different introduction for each group. So, for example, will member1 have one introduction and member2 have a different introduction?
The Subscriptions model should store information about the relationship between and member and group. In that case, introduction would be better to have in the group model. The reason you are getting an error is because you are trying to create a subscription(when you set the introduction attribute) for a group that hasn't been made yet.
So, move introduction to the group model and then, if you want the creator of a group to be automatically subscribed to it (which you should), add the code to create a subscription to the controller in the create action after the record is saved. Then, on the subscription model, you can do cool things like having a state machine that tracks a member's status with the group (moderator, newbie, veteran member, etc).
After many hours of investigation and frustration, I reported it to Rails devs and I finally have a solution:
Rails 3 is unable to initialize group_id and member_id automatically in the way it is defined in the question.
So, for now, there are two ways to make it work:
Add the member_id as a hidden field in the view.
Change everything so is the Subscription model who has accepts_nested_attributes_for. That way, the new object to be created is the Subscription, and Group will be the nested model.
The first option has an important security hole, so I don't recommend it.
The second option, although not much logical, is the cleaner and supposedly the "Rails way" to fix this problem.
I just ran into the same issue, the solution was to remove the presence validation from the related model (based on this question), in your case, remove:
validates :group_id, presence: true
Once that validation it's gone, everything runs like clockwork

Soft-delete on has_many through association

What is the easiest way to implement soft-deletes on has_many through association?
What I want is something like this:
class Company > ActiveRecord::Base
has_many :staffings
has_many :users, through: :staffings, conditions: {staffings: {active: true}}
end
I want to use Company#users the following way:
the Company#users should be a normal association so that it works with forms and doesn't break existing contract.
when adding a user to the company, a new Staffing with active: true is created.
when removing a user from a company, the existing Staffing is updated active: false (currently it just gets deleted).
when adding a previously removed user to the company (so that Staffing#active == false) the Staffing is updated to active: true.
I thought about overriding the Company#users= method, but it really isn't good enough since there are other ways of updating the associations.
So the question is: how to achieve the explained behaviour on the Company#users association?
Thanks.
has_many :through associations are really just syntactic sugar. When you need to do heavy lifting, I would suggest splitting up the logic and providing the appropriate methods and scopes. Understanding how to override callbacks is useful for this sort of thing also.
This will get you started with soft deletes on User and creating Staffings after a User
class Company < ActiveRecord::Base
has_many :staffings
has_many :users, through: :staffings, conditions: ['staffings.active = ?', true]
end
class Staffing < ActiveRecord::Base
belongs_to :company
has_one :user
end
class User < ActiveRecord::Base
belongs_to :staffing
# after callback fires, create a staffing
after_create {|user| user.create_staffing(active: true)}
# override the destroy method since you
# don't actually want to destroy the User
def destroy
run_callbacks :delete do
self.staffing.active = false if self.staffing
end
end
end

Prevent join table being empty

Relationships
class Promotion < ActiveRecord::Base
has_many :promotion_sweepstakes,
has_many :sweepstakes,
:through => :promotion_sweepstakes
end
class PromotionSweepstake < ActiveRecord::Base
belongs_to :promotion
belongs_to :sweepstake
end
class Sweepstake < ActiveRecord::Base
# Not relevant in this question, but I included the class
end
So a Promotion has_many Sweepstake through join table PromotionSweepstake. This is a legacy db schema so the naming might seem a bit odd and there are some self.table_name == and foreign_key stuff left out.
The nature of this app demands that at least one entry in the join table is present for a promotionId, because not having a sweepstake would break the app.
First question
How can I guarantee that there is always one entry in PromotionSweepstake for a Promotion? At least one Sweepstake.id has to be included upon creation, and once an entry in the join table is created there has to be a minimum of one for each Promotion/promotion_id.
Second question (other option)
If the previous suggestion would not be possible, which I doubt is true, there's another way the problem can be worked around. There's a sort of "default Sweepstake" with a certain id. If through a form all the sweepstake_ids would be removed (so that all entries for the Promotion in the join table would be deleted), can I create a new entry in PromotionSweepstake?
pseudo_code (sort of)
delete promotion_sweepstake with ids [1, 4, 5] where promotion_id = 1
if promotion with id=1 has no promotion_sweepstakes
add promotion_sweepstake with promotion_id 1 and sweepstake_id 100
end
Thank you for your help.
A presence validation should solve the problem in case of creation and modification of Promotions.
class Promotion < ActiveRecord::Base
has_many :promotion_sweepstakes
has_many :sweepstakes,
:through => :promotion_sweepstakes
validates :sweepstakes, :presence => true
end
In order to assure consistency when there's an attempt to delete or update a Sweepstake or a PromotionSweepstake you'd have to write your own validations for those two classes. They would have to check whether previously referenced Promotions are still valid, i.e. still have some Sweepstakes.
A simple solution would take and advantage of validates :sweepstakes, :presence => true in Promotion. After updating referenced PromotionSweepstakes or Sweepstakes in a transaction you would have to call Promotion#valid? on previously referenced Promotions. If they're not valid you roll back the transaction as the modification broke the consistency.
Alternatively you could use before_destroy in both PromotionSweepstake and Sweepstake in order to prevent changes violating your consistency requirements.
class PromotionSweepstake < ActiveRecord::Base
belongs_to :promotion
belongs_to :sweepstake
before_destroy :check_for_promotion_on_destroy
private
def check_for_promotion_on_destroy
raise 'deleting the last sweepstake' if promotion.sweepstakes.count == 1
end
end
class Sweepstake < ActiveRecord::Base
has_many :promotion_sweepstakes
has_many :promotions, :through => :promotion_sweepstakes
before_destroy :check_for_promotions_on_destroy
private
def check_for_promotions_on_destroy
promotions.each do |prom|
raise 'deleting the last sweepstake' if prom.sweepstakes.count == 1
end
end
end

Resources