I am trying to create a rspec test for custom validation in a spree extension(like a gem)
I need to validate uniqueness of a variants
option values for a product (all Spree models)
Here is the basic structure of models(Although they are part of spree, a rails based e-commerce building):
class Product
has_many :variants
has_many :option_values, through: :variants #defined in the spree extension, not in actual spree core
has_many :product_option_types
has_many :option_types, through: :product_option_types
end
class Variant
belongs_to :product, touch: true
has_many :option_values_variants
has_many :option_values, through: option_values
end
class OptionType
has_many :option_values
has_many :product_option_types
has_many :products, through: :product_option_types
end
class OptionValue
belongs_to :option_type
has_many :option_value_variants
has_many :variants, through: :option_value_variants
end
So I have created a custom validation to check the uniqueness of a variants option values for a certain product. That is a product(lets say product1) can have many variants. And a variant with option values lets say (Red(Option_type: Color) and Circle(Option_type: Shape)) have to unique for this product
Anyway this is the custom validator
validate :uniqueness_of_option_values
def uniqueness_of_option_values
#The problem is in product.variants, When I use it the product.variants collection is returning be empty. And I don't get why.
product.variants.each do |v|
#This part inside the each block doesn't matter though for here.
variant_option_values = v.option_values.ids
this_option_values = option_values.collect(&:id)
matches_with_another_variant = (variant_option_values.length == this_option_values.length) && (variant_option_values - this_option_values).empty?
if !option_values.empty? && !(persisted? && v.id == id) && matches_with_another_variant
errors.add(:base, :already_created)
end
end
end
And finally here are the specs
require 'spec_helper'
describe Spree::Variant do
let(:product) { FactoryBot.create(:product) }
let(:variant1) { FactoryBot.create(:variant, product: product) }
describe "#option_values" do
context "on create" do
before do
#variant2 = FactoryBot.create(:variant, product: product, option_values: variant1.option_values)
end
it "should validate that option values are unique for every variant" do
#This is the main test. This should return false according to my uniqueness validation. But its not since in the custom uniqueness validation method product.variants returns empty and hence its not going inside the each block.
puts #variant2.valid?
expect(true).to be true #just so that the test will pass. Not actually what I want to put here
end
end
end
end
Anybody know whats wrong here. Thanks in advance
I have a guess at what's happening. I think a fix would be to change your validation with the following line:
product.variants.reload.each do |v|
What I think is happing is that when you call variant1 in your test, it is running the validation for variant1, which calls variants on the product object. This queries the database for related variants, and gets an empty result. However, since variant2 has the same actual product object, that product object will not re-query the database, and remembers (incorrectly) that its variants is an empty result.
Another change which might make your test run is to change your test as follows:
before do
#variant2 = FactoryBot.create(:variant, product_id: product.id, option_values: variant1.option_values)
end
It is subtle and I'd like to know if it works. This sets the product_id field on variant2, but does not set the product object for the association to be the actual same product object that variant1 has. (In practice this is more likely to happen in your actual code, that the product object is not shared between variant objects.)
Another thing for your correct solution (if all this is right) is to do the reload but put all your save code (and your update code) in a transaction. That way there won't be a race condition of two variants which would conflict, because in a transaction the first must complete the validation and save before the second one does its validation, so it will be sure to detect the other one which just saved.
Some suggested debugging techniques:
If possible, watch the log to see when queries are made. You might have caught that the second validation did not query for variants.
Check the object_id. You might have caught that the product objects were in fact the same object.
Also check new_record? to make sure that variant1 saved before you tested variant2. I think it does save, but it would have be nice to know you checked that.
Related
I have what i feel could be a simple question, and i have this working, but my solution doesn't feel like the "Rails" way of doing this. I'm hoping for some insight on if there is a more acceptable way to achieve these results, rather than the way i would currently approach this, which feels kind of ugly.
So, lets say i have a simple has_many :through setup.
# Catalog.rb
class Catalog < ActiveRecord::Base
has_many :catalog_products
has_many :products, through: :catalog_products
end
# Products.rb
class Product < ActiveRecord::Base
has_many :catalog_products
has_many :catalogs, through: :catalog_products
end
# CatalogProduct.rb
class CatalogProduct < ActiveRecord::Base
belongs_to :catalog
belongs_to :product
end
The data of Catalog and the data of Product should be considered independent of each other except for the fact that they are being associated to each other.
Now, let's say that for Catalog, i have a form with a list of all Products, in say a multi-check form on the front end, and i need to be able to check/uncheck which products are associated with a particular catalog. On the form field end, i would return a param that is an array of all of the checked products.
The question is: what is the most accepted way to now create/delete the catalog_product records so that unchecked products get deleted, newly checked products get created, and unchanged products get left alone?
My current solution would be something like this:
#Catalog.rb
...
def update_linked_products(updated_product_ids)
current_product_ids = catalog_products.collect{|p| p.product_id}
removed_products = (current_product_ids - updated_product_ids)
added_products = (updated_product_ids - current_product_ids)
catalog_products.where(catalog_id: self.id, product_id: removed_products).destroy_all
added_products.each do |prod|
catalog_products.create(product_id: prod)
end
end
...
This, of course, does a comparison between the current associations, figures out which records need to be deleted, and which need to be created, and then performs the deletions and creations.
It works fine, but if i need to do something similar for a different set of models/associations, i feel like this gets even uglier and less DRY every time it's implemented.
Now, i hope this is not the best way to do this (ignoring the quality of the code in my example, but simply what it is trying to achieve), and i feel that there must be a better "Rails" way of achieving this same result.
Take a look at this https://guides.rubyonrails.org/association_basics.html#methods-added-by-has-many-collection-objects
You don't have to remove and create manually each object.
If you have already the product_ids array, I think this should work:
#Catalog.rb
...
def update_linked_products(updated_product_ids)
selected_products = Product.where(id: updated_product_ids)
products = selected_products
end
...
First,
has_many :products, through: :catalog_products
generate some methods for you like product_ids, check this under auto-generated methods to know more about the other generated methods.
so we don't need this line:
current_product_ids = catalog_products.collect{|p| p.product_id}
# exist in both arrays are going to be removed
will_be_removed_ids = updated_product_ids & product_ids
# what's in updated an not in original, will be appended
will_be_added_ids = updated_product_ids - product_ids
Then, using <<, and destroy methods which are also generated from the association (it gives you the ability to deal with Relations as if they are arrays), we are going to destroy the will_be_removed_ids, and append the will_be_added_ids, and the unchanged will not be affected.
Final version:
def update_linked_products(updated_product_ids)
products.destroy(updated_product_ids & product_ids)
products << updated_product_ids - product_ids
end
I have the following models and associations:
SuccessCriterion
has_many :requirements
has_many :findings, through: :requirements
Requirement
belongs_to :success_criterion
has_many :findings
Finding
belongs_to :requirement
has_one :success_criterion, through: :requirement
Each finding can have a status critical
If a requirement has at least one critical finding, its status is critical, too
And if a success criterion has at least one critical requirement, its status is critical, too
So the critical status is inherited.
When showing a list of success criteria, I want to show whether its status is critical or not. To do that, I calculate the status by iterating through all requirements and there again iterating through all findings and searching for at least one critical finding.
This takes a lot of DB queries, so I'm thinking about caching the status in the success criterion, and every time I'm adding/modifying/deleting a finding, the status should be updated in the success criterion.
What's the best way to do that? I thought about something like an after_save filter like this:
model Finding
belongs_to :requirement
has_one :success_criterion, through: :requirement
after_save :update_success_criterion_status
private
def update_success_criterion_status
if status.critical?
success_criterion.update_attribute :status, :critical
else
success_criterion.calculate_status! # Iterate through all findings and look for at least one critical
end
end
end
In development, this would work well, I guess.
But what about testing? For unit testing the success criterion, I would have to provide the necessary associations for each and every single test, otherwise the after_save filter would crash. This means a lot of overhead for each test.
For sure, I could do something hacky like
after_save :update_success_criterion_status, unless: -> { Rails.env.test? }
but I don't think this is a good way.
Is there a better way to do this? Or am I going completely the wrong route? Maybe there are even gems that handle stuff like this (modifying attributes of associated resources)?
PS
A similar requirement would be the possibility to counter cache a deeply associated element. Let's say, we have another model, e.g.
Project
has_many :success_criteria
When we want to know how many findings there are in a project, we have to count them through both success criteria, requirements, and findings. A counter cache would save a lot of queries here, too. But there would need to be taken care of a lot of create/update/delete stuff of all the associated models to update the counter cache...
I think the best way would be to create another model that all associated models can efficiently look at. You can create a Status model with boolean columns for the different status types—at least one of them being the critical column, obviously—in which you store the count.
You will be able to retrieve any model's status with <model_instance>.status and with that, you can find out if it's critical or not. For example, finding.status.critical?, or a_success_criterion.status.critical? or even a_success_criterion.critical?.
model Finding
belongs_to :requirement
has_one :success_criterion, through: :requirement
has_one :status
def critical?
status.critical? # Note: Rails should define this method for you on the status because it's a boolean column
end
end
Similarly, to retrieve a Requirements's status you would just do so with requirement.status and requirement.critical? to figure out if the requirement is critical or not.
model Requirements
belongs_to :success_criterion
has_many :findings
has_many :statuses
def critical?
return true if statuses.where(critical: true).any?
end
end
And to retrieve a SuccessCriterion's status you would just do so with success_criterion.status because:
model SuccessCriterion
has_many :requirements
has_many :findings, through: :requirements
has_many :statuses
def critical?
return true if statuses.where(critical: true).any?
end
end
A critical part (no pun intended—initially:) is when you create a finding and therefore a status: you must or should give the status the ids of the the finding, requirement, and success criterion that it belongs to, so you might want to add a validation for their presence. Additionally, you might want to add a before_validation upon creating a status, either in Status or in Finding, which would look something like this (in Status):
model Status
belongs_to :finding, :requirement, success_criterion
before_validation :populate_ids
validates_presence_of :finding_id, :requirement_id, :success_criterion_id
def populate_ids
self.finding_id = finding.id
self.requirement_id = finding.requirement.try(:id)
self.success_criterion_id = finding.requirement.try(:success_criterion).try(:id)
end
end
In your tests, you just have to provide integers for the ids but they don't have to be of actual related models if you don't want to test for such relations—this is the beauty of it, aside from efficient querying :).
You get the general idea. I hope this helps!
So I've got a User model, a Building model, and a MaintenanceRequest model.
A user has_many :maintenance_requests, but belongs_to :building.
A maintenance requests belongs_to :building, and belongs_to: user
I'm trying to figure out how to send a new, then create a maintenance request.
What I'd like to do is:
#maintenance_request = current_user.building.maintenance_requests.build(permitted_mr_params)
=> #<MaintenanceRequest id: nil, user_id: 1, building_id: 1>
And have a new maintenance request with the user and building set to it's parent associations.
What I have to do:
#maintenance_request = current_user.maintenance_requests.build(permitted_mr_params)
#maintenance_request.building = current_user.building
It would be nice if I could get the maintenance request to set its building based of the user's building.
Obviously, I can work around this, but I'd really appreciate the syntactic sugar.
From the has_many doc
You can pass a second argument scope as a callable (i.e. proc or lambda) to retrieve a specific set of records or customize the generated query when you access the associated collection.
I.e
class User < ActiveRecord::Base
has_many :maintenance_requests, ->(user){building: user.building}, through: :users
end
Then your desired one line should "just work" current_user.building.maintenance_requests.build(permitted_mr_params)
Alternatively, if you are using cancancan you can add hash conditions in your ability file
can :create, MaintenanceRequest, user: #user.id, building: #user.building_id
In my opinion, I think the approach you propose is fine. It's one extra line of code, but doesn't really increase the complexity of your controller.
Another option is to merge the user_id and building_id, in your request params:
permitted_mr_params.merge(user_id: current_user.id, building_id: current_user.building_id)
#maintenance_request = MaintenanceRequest.create(permitted_mr_params)
Or, if you're not concerned about mass-assignment, set user_id and building_id as a hidden field in your form. I don't see a tremendous benefit, however, as you'll have to whitelist the params.
My approach would be to skip
maintenance_request belongs_to :building
since it already belongs to it through the user. Instead, you can define a method
class MaintenanceRequest
belongs_to :user
def building
user.building
end
#more class stuff
end
Also, in building class
class Building
has_many :users
has_many :maintenance_requests, through: :users
#more stuff
end
So you can completely omit explicit building association with maintenance_request
UPDATE
Since users can move across buildings, you can set automatic behavior with a callback. The job will be done like you do it, but in a more Railsey way
class MaintenanceRequest
#stuff
before_create {
building=user.building
}
end
So, when you create the maintenance_request for the user, the building will be set accordingly
I am new to rails so beware of the ugly code.
I have these models
class User < ActiveRecord::Base
has_many :games_playeds
has_many :games, :through => :games_playeds
end
class Game < ActiveRecord::Base
has_many :games_playeds
has_many :users, :through => :games_playeds
end
class GamesPlayed < ActiveRecord::Base
validates_presence_of :user_id, :game_id
belongs_to :user
belongs_to :game
end
Game describe a game independent of any user
GamesPlayed describe how users behaved on that game (deaths, current stage, wins, etc)
In each stage of this game the user can choose among several choices, some will get to later stages, some will make him go back. The point is that once a choice is made in one stage I don't allow to choose anything else.
To implement this I have a steps attribute that encode the previous choices like "0:1;1:6;6:2" and so on. This attribute in on the GamesPlayed model.
The pages the user navigates are automatically generated so I don't know their names but I know they are called XX_to_YY. I have a method in my controller that will get them all and do something as ugly as this:
#get the game name, we have several games
game = Game.find_by_name (params[:game])
#get the previous and current stage
from, to = params[:page].to_s.split("_to_")
to = to.split(".html")[0]
played = current_user.games_playeds.find_by_game_id (game.id)
steps = []
played.steps.split(";").each {|a| steps << a.split(":").first}
if steps.include? from
render :inline => "You already chose for this, go back"
else
played.steps << "#{from}:#{to};"
played.save
# pl = current_user.games_playeds.find_by_game_id (game.id)
# raise pl.steps
render "games/choosePath/#{game.name}/#{params[:page]}.html"
end
I guess it is a horrible code. I am new to Ruby as well :P
Now, the question:
played.save gives me no errors.
# pl = current_user.games_playeds.find_by_game_id (game.id)
# raise pl.steps
will "print" the correct data but it is not saved in the database! I use sqlitebrowser to visually inspect it and I am sure it is not saved.
BTW, as second question, if someone knows how to get to the association object without that ugly code above very thankful also.
and third and last question:
steps = []
played.steps.split(";").each {|a| steps << a.split(":").first}
This is also horrible but don't know how to make it better (want to get the aa and bb from "aa:cc;bb:dd;" I don't know what is aa and bb, it can be numbers or words.
If you want to raise an exception when save fails, call save!; otherwise if you continue to use save you should check the returned boolean to see if the save was successful.
A false return value will indicate that validations have failed. The details of the failures will be in the error information on the model.
About getting the association in a better way: there is probably something you could do with scopes or even just by writing a method to encapsulate what you are trying to do.
With regard to decoding the steps, you could use inject instead of each but it would still be quite logic heavy. I would suggest encapsulating it in a method with a descriptive name, like decode_steps or similar.
I have two ActiveRecord classes. A simplified view of these classes:
class Account < ActiveRecord::Base
has_many :user_account_roles
end
class UserAccountRole < ActiveRecord::Base
belongs_to :account
# Has a boolean attribute called 'administrator'.
end
What I'm struggling with is that I'd like to be able to apply two validation rules to this:
* Ensuring that the last UserAccountRole cannot be removed.
* Ensuring that the last UserAccountRole that is an administrator cannot be removed.
I'm really struggling to understand the best way of achieving this kind of structural validation. I've tried adding a before_remove callback to the association, but I don't like that this has to throw an error which would need to be caught by the controller. I'd rather this be treated as 'just another validation'.
class Account < ActiveRecord::Base
has_many :user_account_roles, :before_remove => check_remove_role_ok
def check_remove_relationship_ok(relationship)
if self.user_account_relationships.size == 1
errors[:base] << "Cannot remove the last user from this account."
raise RuntimeError, "Cannot remove the last user from this account."
end
end
end
I don't think this makes any difference, but I'm also using accepts_nested_attributes_for.
Why not use a simple validation on Account?
class Account < ActiveRecord::Base
has_many :user_account_roles
validate :at_least_one_user_account_role
validate :at_least_one_administrator_role
private
def at_least_one_user_account_role
if user_account_roles.size < 1
errors.add_to_base('At least one role must be assigned.')
end
end
def at_least_one_administrator_role
if user_account_roles.none?(&:administrator?)
errors.add_to_base('At least one administrator role must be assigned.')
end
end
end
This way nothing needs to be raised, and the record won't be saved unless there's at least one role, and at least one administrator role. Thus when you re-render your edit form on error, this message will show up.
You could place the validation on UserAccountRole. If it is the only UserAccountRole associated with the Account, then it can't be deleted.
An easier solution may be to question an underlying assumption of your design. Why have UserAccountRole be an AR backed model? Why not just make it a plain ruby class? Is the end user going to dynamically define roles? If not, then you could greatly simplify your dilemma by making it a regular ruby class.