Rails: Validating an array of ids - ruby-on-rails

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.

Related

Rails uniqueness validation with nested has_many

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!

Find or create nested attributes in a polymorphic join table, in one transaction

Background
I want to save skills on different types of objects. Typically, a user can have a project or experience, and tag it with different skills etc. Ruby , python or Excel. These skills are global and used across the various possible objects that are "skillable".
The problem consists of three models:
Project - contains info about the project
Skill - contains the name of the skill
SkillObject - join table between skill and projects
(polymorphic)
Please look at this ER-diagram for a more detailed picture.
The problem
When I create or update a project, I also want the skills to be added in the same transaction, just by sending the names of the skills from the front-end.
I want a kind of find_or_create_by to avoid duplicate skills. At the same time, validations must ensure that two identical skills cannot be put on the same project (skill_object.rb) and that the skill name cannot be zero (skill.rb)
The wanted behavior is if the validation of skills is not successful, then either the project nor the skills are stored in the database. If you enter two different skill names and one is already in the database, the one that exists should be found and connected to the project, and the other created and so connected through the join table, if not validations fails.
project.rb
class Project < ApplicationRecord
include Skillable
[...]
end
skill_object.rb
class SkillObject < ApplicationRecord
belongs_to :skill, inverse_of: :skill_objects
belongs_to :skillable, polymorphic: true
delegate :delete_if_empty_skill_objects, to: :skill
after_destroy :delete_if_empty_skill_objects
# Avoiding duplicates of the same skill
validates :skill, uniqueness: { scope: :skillable }
end
skill.rb
class Skill < ApplicationRecord
has_many :skill_objects, inverse_of: :skill
has_many :projects, through: :skill_objects, source: :sectorable, source_type: 'Project'
has_many :experiences, through: :skill_objects, source: :sectorable, source_type: 'Experience'
validates :name, uniqueness: true, presence: true
def delete_if_empty_skill_objects
self.destroy if self.skill_objects.empty? and not self.is_global
end
end
I use a concern that is included on the different types of skillable objects:
concercns/skillable.rb
module Skillable
extend ActiveSupport::Concern
included do
attr_accessor :skills_attributes
after_save :set_skills
# Adding needed relations for skillable objects
has_many :skill_objects, -> { order(created_at: :asc) }, as: :skillable, dependent: :destroy
has_many :skills, through: :skill_objects
private
def set_skills
# THIS CODE IS THE ONE I AM STRUGGLING WITH
# Parsing out all the skill names
skill_names = skills_attributes.pluck(:name)
# Removing existing skills
self.skills = []
# Building new skills
skill_names.each do |name|
existing = Skill.find_by(name: name)
if existing
self.skills << existing
else
self.skills.new(name: name)
end
end
raise ActiveRecord::Rollback unless self.valid?
self.skills.each(&:save)
end
end
end
Does anyone know how I can write the set_skill function in skillable.rb to be able to save and update skills, validate the parent object and the skills, do a rollback if not validated and add the appropriate errors to the project object if it failes?

Validation for the count of has_many relationship in Rails

I have been through other questions, but the scenario is little different here:
class User < ApplicationRecord
has_many :documents, as: :attachable
validate :validate_no_of_documents
private
def validate_no_of_documents
errors.add(:documents, "count shouldn't be more than 2") if self.documents.size > 2
end
end
class Document < ApplicationRecord
belongs_to :attachable, polymorphic: true
validates_associated :attachable
end
Now, consider User.find(2) that already has two documents, so doing the following:
user.documents << Document.new(file: File.open('image.jpg', 'rb'))
This successfully creates the document, and doesn't validate the attachable: User. After the document is created in the database, both user & Document.last are invalid, but of what use, they have been created now.
I'm trying to create a Document object on the run time, and that may be causing it, but for that purpose, I'm using size instead of count in my validation.
inverse_of to rescue here again.
user = User.find(1) # This user has already 2 associated documents.
Doing user.documents << Document.new(file: file) won't change the count for the associated documents of a user unless a document is actually created, and since the count will remain 2 while creating a 3rd document, so Rails won't stop you creating a third document associated with the user, killing the very purpose of putting the validation.
So following is what I did:
# In User model
has_many :documents, as: :attachable, inverse_of: :attachable
# In Document model
belongs_to :attachable, polymorphic: true, inverse_of: :attachments
Related article to read: https://robots.thoughtbot.com/accepts-nested-attributes-for-with-has-many-through
You can use a standard validation for this, rather than a custom one:
validates :documents, length: { maximum: 2 }
You could always wrap this in a transaction, though I'm a little surprised it isn't properly rolling back the Document save if it doesn't end up being valid.

Rails: Address model being used twice, should it be separated into two tables?

I am making an ecommerce site, and I have Purchases which has_one :shipping_address and has_one :billing_address
In the past the way I've implemented this is to structure my models like so:
class Address < ActiveRecord::Base
belongs_to :billed_purchase, class_name: Purchase, foreign_key: "billed_purchase_id"
belongs_to :shipped_purchase, class_name: Purchase, foreign_key: "shipped_purchase_id"
belongs_to :state
end
class Purchase < ActiveRecord::Base
INCOMPLETE = 'Incomplete'
belongs_to :user
has_one :shipping_address, class: Address, foreign_key: "shipped_purchase_id"
has_one :billing_address, class: Address, foreign_key: "billed_purchase_id"
...
end
As you can see, I reuse the Address model and just mask it as something else by using different foreign keys.
This works completely find, but is there a cleaner way to do this? Should I be using concerns? I'm sure the behavior of these two models will always be 100% the same, so I'm not sure if splitting them up into two tables is the way to go. Thanks for your tips.
EDIT The original version of this was wrong. I have corrected it and added a note to the bottom.
You probably shouldn't split it into two models unless you have some other compelling reason to do so. One thing you might consider, though, is making the Address model polymorphic. Like this:
First: Remove the specific foreign keys from addresses and add polymorphic type and id columns in a migration:
remove_column :addresses, :shipping_purchase_id
remove_column :addresses, :billing_purchase_id
add_column :addresses, :addressable_type, :string
add_column :addresses, :addressable_id, :integer
add_column :addresses, :address_type, :string
add_index :addresses, [:addressable_type, :addressable_id]
add_index :addresses, :address_type
Second: Remove the associations from the Address model and add a polymorphic association instead:
class Address < ActiveRecord::Base
belongs_to :addressable, polymorphic: true
...
end
Third: Define associations to it from the Purchase model:
class Purchase < ActiveRecord::Base
has_one :billing_address, -> { where(address_type: "billing") }, as: :addressable, class_name: "Address"
has_one :shipping_address, -> { where(address_type: "shipping") }, as: :addressable, class_name: "Address"
end
Now you can work with them like this:
p = Purchase.new
p.build_billing_address(city: "Phoenix", state: "AZ")
p.build_shipping_address(city: "Indianapolis", state: "IN")
p.save!
...
p = Purchase.where(...)
p.billing_address
p.shipping_address
In your controllers and views this will work just like what you have now except that you access the Purchase for an Address by calling address.addressable instead of address.billed_purchase or address.shipped_purchase.
You can now add additional address joins to Purchase or to any other model just by defining the association with the :as option, so it is very flexible without model changes.
There are some disadvantages to polymorphic associations. Most importantly, you can't eager fetch from the Address side in the above setup:
Address.where(...).includes(:addressable) # <= This will fail with an error
But you can still do it from the Purchase side, which is almost certainly where you'd need it anyway.
You can read up on polymorphic associations here: Active Record Association Guide.
EDIT NOTE: In the original version of this, I neglected to add the address_type discriminator column. This is pernicious because it would seem like it is working, but you'd get the wrong address records back after the fact. When you use polymorphic associations, and you want to associate the model to another model in more than one way, you need a third "discriminator" column to keep track of which one is which. Sorry for the mixup!
In addtion to #gwcoffey 's answer.
Another option would be using Single Table Inhertinace which perhaps suits more for that case, because every address has a mostly similar format.

Rails model complex inheritance associations

I have two classes Ads and Zones which both inherit from a class called Tracked which in turn contains several Events. The idea is that Ads as well as Zones can be associated with various events (i.e. 'View', 'Click', 'Conversion'). Those events must be tracked.
How would I go about and model that using ActiveRecords? What would the migration code look like?
This is what I got so far:
Event:
class Event < ActiveRecord :: Base
attribute :event_date
belongs_to :trackable, polymorphic: true
[...]
end
Tracked:
class Tracked < ActiveRecord :: Base
self.abstract_class = true
has_many :views, class_name: "Event"
has_many :clicks, class_name: "Event"
has_many :conversions, class_name: "Event"
belongs_to :trackable, polymorphic: true
[...]
end
Ad:
class Ad < Tracked
attribute :size, :content
attr_accessor :width, :height
belongs_to :advertisers
has_and_belongs_to_many :campaigns
[...]
end
Campaign:
require 'date'
class Campaign < ActiveRecord :: Base
attribute :name, :target_url,
:expiration_date,:categories,
:billing_type, :budget, :budget_type,
:cp_click, :cp_view, :cp_conversion
belongs_to :advertiser
has_and_belongs_to_many :ads
has_many :zones
[...]
end
At first I thought that I may want to use the through association but since it is paramount for me to distinguish between those three events (View, Click, Conversion) I think that I can' apply that pattern. Therefore I think I got to use Polymorphic Associations.
Please note that the code I pasted contains all the necessary information to create the model i.e. there are no attributes or associations that I left out. Also I know how to write migration code for all the attributes/associations that don't belong to the problem described above.
Polymorphic Association
To implement a polymorphic associations here, Ads and Zones should both use:
has_one :tracked, as: :trackable
You wouldn't need this line in Events: belongs_to :trackable, polymorphic: true since you already have it in Tracked.
You then need to run some migrations to set up Tracked to handle the trackable association (this explains that better than I could).
I think you would need to also remove self.abstract_class = true in Tracked since it will now be represented in the db.
There is perhaps a better way to approach this:
Perhaps you would be better off creating the 'views', 'clicks' and 'conversion' associations directly on Ads and Zones and then including any shared logic with a module.
You could take the logic of Tracked, move it into a 'trackable' module and including that module in Ads and Zones (if you're using Rails 4 you can wrap this up in a 'trackable' concern).
You would also move the following associations into Ads and Zones:
has_many :views, class_name: "Event"
has_many :clicks, class_name: "Event"
has_many :conversions, class_name: "Event"
You then wouldn't need the Tracked class any more.
This approach would mean you could call Ad.first.views as opposed to Ad.first.tracked.views which would be the case if you used a polymorphic association.

Resources