I would like to implement a server side validation for the following:
Subscriptions for a certain posting can only be created as long as the number of subscriptions doesn't exceed the number of spots for this posting.
class Cposting < ActiveRecord::Base
belongs_to :user
has_many :subscriptions,
foreign_key: "post_id",
dependent: :destroy
...
def spots_left #returns the number of places left in this class
self.spots.to_i - Subscription.where(post_id: self.id).count.to_i
end
...
end
In the Subscription model I tried to call the spots_left method to determine whether there are any spots left for the Cposting a new subscription belongs to.
class Subscription < ActiveRecord::Base
belongs_to :subscriber, class_name: "User"
belongs_to :post, class_name: "Cposting"
...
validate :class_not_full
def class_not_full
Cposting.find_by(id: self.post_id).spots_left > 0
end
end
Running tests on the Subscription model returned a nil error
NoMethodError: undefined method `spots_left' for nil:NilClass
It seems I can not use find_by, find or where methods to point to this Cposting.
I would like to know how to refer to the Cposting that belongs to the Subscription being validated, or an alternative way to implement this validation.
Thanks
EDIT adding tests
require 'test_helper'
class SubscriptionTest < ActiveSupport::TestCase
def setup
#cposting = cpostings(:one) #has one spot
#customer = users(:customer)
#customer2 = users(:customer2)
#subscription = Subscription.new(post_id: #cposting.id, subscriber_id: #customer.id)
end
...
test "subscriptions cannot exceed spots" do
#subscription.save
assert #cposting.subscriptions.count == #cposting.spots
#subscription2 = Subscription.new(post_id: #cposting.id, subscriber_id: #customer2.id)
assert_not #subscription2.valid?
end
end
Running rake test TEST=test/models/subscription_test.rb gives
1) Failure:
SubscriptionTest#test_subscriptions_cannot_exceed_spots [/~/test/models/subscription_test.rb:37]:
Expected true to be nil or false
5 runs, 7 assertions, 1 failures, 0 errors, 0 skips
EDIT 2 adding create method
class SubscriptionsController < ApplicationController
def create
#posting = Cposting.find(params[:post_id])
current_user.subscriptions.create(post_id: #posting.id)
flash[:success] = "Subscribed!"
redirect_to subscriptions_path
end
end
Make use of the rails relations. You dont need to query everything again.
Try the following:
class Cposting < ActiveRecord::Base
belongs_to :user
has_many :subscriptions,
foreign_key: "post_id",
dependent: :destroy
def spots_left
self.spots - self.subscriptions.count # i assume that spots is an integer db field
end
end
and
class Subscription < ActiveRecord::Base
belongs_to :subscriber, class_name: "User"
belongs_to :post, class_name: "Cposting"
validate :class_not_full
def class_not_full
post.spots_left > 0
end
end
And the creation of a subscription:
#cposting.build_subscription(subscriber: #customer2)
Rails offers you a bunch of methods to choose from. You don't even need to work with the ids. Just use the relations. In general I discovered Rails to work so much smoother when you stick to the AR methods (it would even be a good idea to stick to the rails conventions when naming your tables)
Please read this carefully, it'll help you a lot.
The nil error was fixed by adding a nil check and an error if the test didn't pass.
validate :class_not_full
def class_not_full #checks if there are spots left for the posting
errors.add(:post, "posting is already full") unless !post.nil? && post.spots > post.subscriptions.count
end
Related
There are models with has has_many through association:
class Event < ActiveRecord::Base
has_many :event_categories
has_many :categories, through: :event_categories
validates :categories, presence: true
end
class EventCategory < ActiveRecord::Base
belongs_to :event
belongs_to :category
validates_presence_of :event, :category
end
class Category < ActiveRecord::Base
has_many :event_categories
has_many :events, through: :event_categories
end
The issue is with assigning event.categories = [] - it immediately deletes rows from event_categories. Thus, previous associations are irreversibly destroyed and an event becomes invalid.
How to validate a presence of records in case of has_many, through:?
UPD: please carefully read sentence marked in bold before answering.
Rails 4.2.1
You have to create a custom validation, like so:
validate :has_categories
def has_categories
unless categories.size > 0
errors.add(:base, "There are no categories")
end
end
This shows you the general idea, you can adapt this to your needs.
UPDATE
This post has come up once more, and I found a way to fill in the blanks.
The validations can remain as above. All I have to add to that, is the case of direct assignment of an empty set of categories. So, how do I do that?
The idea is simple: override the setter method to not accept the empty array:
def categories=(value)
if value.empty?
puts "Categories cannot be blank"
else
super(value)
end
end
This will work for every assignment, except when assigning an empty set. Then, simply nothing will happen. No error will be recorded and no action will be performed.
If you want to also add an error message, you will have to improvise. Add an attribute to the class which will be populated when the bell rings.
So, to cut a long story short, this model worked for me:
class Event < ActiveRecord::Base
has_many :event_categories
has_many :categories, through: :event_categories
attr_accessor :categories_validator # The bell
validates :categories, presence: true
validate :check_for_categories_validator # Has the bell rung?
def categories=(value)
if value.empty?
self.categories_validator = true # Ring that bell!!!
else
super(value) # No bell, just do what you have to do
end
end
private
def check_for_categories_validator
self.errors.add(:categories, "can't be blank") if self.categories_validator == true
end
end
Having added this last validation, the instance will be invalid if you do:
event.categories = []
Although, no action will have been fulfilled (the update is skipped).
use validates_associated, official documentaion is Here
If you are using RSpec as your testing framework, take a look at Shoulda Matcher. Here is an example:
describe Event do
it { should have_many(:categories).through(:event_categories) }
end
I have the following classes:
class Product < ApplicationRecord
belongs_to :product_category
def destroy
puts "Product Destroy!"
end
end
class ProductCategory < ApplicationRecord
has_many :products, dependent: :destroy
def destroy
puts "Category Destroy!"
end
end
Here, I am trying to override the destroy method where I eventually want to do this:
update_attribute(:deleted_at, Time.now)
When I run the following statement in Rails console: ProductCategory.destroy_all I get the following output
Category Destroy!
Category Destroy!
Category Destroy!
Note: I have three categories and each category has more than one Products. I can confirm it by ProductCategory.find(1).products, which returns an array of products. I have heard the implementation is changed in Rails 5. Any points on how I can get this to work?
EDIT
What I eventually want is, to soft delete a category and all associated products in one go. Is this possible? Or will ave to iterate on every Product object in a before destroy callback? (Last option for me)
You should call super from your destroy method:
def destroy
super
puts "Category destroy"
end
But I definitely wouldn't suggest that you overide active model methods.
So this is how I did it in the end:
class Product < ApplicationRecord
belongs_to :product_category
def destroy
run_callbacks :destroy do
update_attribute(:deleted_at, Time.now)
# return true to escape exception being raised for rollback
true
end
end
end
class ProductCategory < ApplicationRecord
has_many :products, dependent: :destroy
def destroy
# run all callback around the destory method
run_callbacks :destroy do
update_attribute(:deleted_at, Time.now)
# return true to escape exception being raised for rollback
true
end
end
end
I am returning true from the destroy does make update_attribute a little dangerous but I am catching exceptions at the ApplicationController level as well, so works well for us.
I have a Lesson model which has many Completions like this:
class Lesson < ActiveRecord::Base
has_many :completions, as: :completable
belongs_to :course
end
And each Completion belongs to a User as well:
class Completion < ActiveRecord::Base
belongs_to :user
belongs_to :completable, polymorphic: true
end
From my application perspective I'm only interested in the amount of completions for a certain lesson, so I've included a counter cache. In regard to the individual Completions, I'm only interested if the Lesson is completed by the current user (I'm using Devise).
Is there some way to create a dynamic has_one relationship of some kind, that uses the information from the current_user to query the Completion table?
for instance:
has_one :completion do
def from_user current_user
Completion.where(completable: self, user: current_user)
end
end
Although this could work, I'm also having a polymorphic relationship. Rails is complaining that there's no foreign key called lesson_id. When I add a foreign_key: symbol, the do-end block stops working.
Any ideas?
Why not passing both block and options to has_many?
class Lesson < ActiveRecord::Base
has_many :completions, as: :completable do
def from_user user
if loaded?
find {|c| c.user_id = user.id}
else
find(user_id: user.id)
end
end
end
belongs_to :course
end
#lesson = Lesson.last
# Association not loaded - executing sql query
#lesson.completions.from_user(current_user)
#lesson.completions
# Association loaded - no sql query
#lesson.completions.from_user(current_user)
NOTE: You cannot treat it as an association, so it cannot be preloaded on its own.
I have 2 models: Dealer & Location.
class Dealer < AR::Base
has_many :locations
accepts_nested_attributes_for :locations
validate :should_has_one_default_location
private
def should_has_one_default_location
if locations.where(default: true).count != 0
errors.add(:base, "Should has exactly one default location")
end
end
end
class Location < AR::Base
# boolean attribute :default
belongs_to :dealer
end
As you understood, should_has_one_location adds error everytime, because .where(default: true) makes an sql query. How can I avoid this behaviour?
The very dirty solution is to use combination of inverse_of and select instead of where, but it seems very dirty. Any ideas?
I actually got an answer to a similar question of my own. For whatever it's worth, If you wanted to do a validation like you have above (but without the db query), you would do the following:
errors.add(:base, ""Should have exactly one default location") unless locations.any?{|location| location.default == 'true'}
on a Ruby on Rails project I'm trying to access association objects on an ActiveRecord prior to saving everything to the database.
class Purchase < ActiveRecord::Base
has_many :purchase_items, dependent: :destroy
has_many :items, through: :purchase_items
validate :item_validation
def item_ids=(ids)
ids.each do |item_id|
purchase_items.build(item_id: item_id)
end
end
private
def item_validation
items.each do |item|
## Lookup something with the item
if item.check_something
errors.add :base, "Error message"
end
end
end
end
If I build out my object like so:
purchase = Purchase.new(item_ids: [1, 2, 3]) and try to save it the item_validation method doesn't have the items collection populated yet, so even though items have been set set it doesn't get a chance to call the check_something method on any of them.
Is it possible to access the items collection before my purchase model and association models are persisted so that I can run validations against them?
If I change my item_validation method to be:
def item_validation
purchase_items.each do |purchase_item|
item = purchase_item.item
## Lookup something with the item
if item.something
errors.add :base, "Error message"
end
end
end
it seems to work the way I want it to, however I find it hard to believe that there is no way to directly access the items collection with rails prior to my purchase and associated records being saved to the database.
Try to adding the argument inverse_of: in the has_many and belongs_to definitions. The inverse_of argument it's the name of the relation on the other model, For example:
class Post < ActiveRecord::Base
has_many :comments, inverse_of: :post
end
class Comment < ActiveRecord::Base
belongs_to :post, inverse_of: :comments
end
Don't forget to add it also on the other classes, such as PurchaseItem and Item
Hope it helps
Remove your own item_ids= method - rails generates one for you (see collection_singular_ids=ids). This might already solve your problem.
class Purchase < ActiveRecord::Base
has_many :purchase_items, dependent: :destroy
has_many :items, through: :purchase_items
validate :item_validation
private
def item_validation
items.each do |item|
## Lookup something with the item
if item.check_something
errors.add :base, "Error message"
end
end
end
end
The second thing that comes in my mind looking at your code: Move the validation to the Item class. So:
class Purchase < ActiveRecord::Base
has_many :purchase_items, dependent: :destroy
has_many :items, through: :purchase_items
end
class Item < ActiveRecord::Base
has_many :purchase_items
has_many :purchases, through: :purchase_items
validate :item_validation
private
def item_validation
if check_something
errors.add :base, "Error message"
end
end
end
Your Purchase record will also be invalid if one of the Items is invalid.
Do you have documentation that indicates purchase = Purchase.new(item_ids: [1, 2, 3]) does what you're expecting?
To me that looks like you are just setting the non-database attribute 'item_ids' to an array (i.e. not creating an association).
Your Purchase model should not even have any foreign key columns to set directly. Instead there are entries in the purchase_items table that have a purchase_id and item_id. To create a link between your purchase and the three items you need to create three entries in the joiner table.
What happens if you just do this instead?:
purchase = Purchase.new
purchase.items = Item.find([1,2,3])
You can use model.associations = [association_objects] and an Association Callback http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Association+callbacks
I assume you can't access them because id of Purchase isn't available before record saved. But as you mention you have access to first level association purchase_items, so you can extract all ids and pass them in where for Item:
items = Item.where(purchase_item_id: purchase_items.map(&:id))