Rails 5: validate HABTM association - ruby-on-rails-5

How is it possible to validate an HBTM association ?
For example you have the following 2 models with HABTM relation:
class Country < ApplicationRecord
has_and_belongs_to_many :languages
validates :code, uniqueness: { case_sensitive: false }
end
class Language < ApplicationRecord
has_and_belongs_to_many :countries
validates :tag, presence: true, uniqueness: { case_sensitive: false }
end
How to validate a language before adding it to country languages ?
I have an idea to do smth like this:
def check_for_existing_language(language)
languages.include?(language) == true
end
But where to put this callback method ? In before_save of Country model ?

Related

ActiveRecord_Associationundefined method `reviews' for #<Post::ActiveRecord_Associations_CollectionProxy:>s_CollectionProxy

I have 3 models in my application namely - user, post and comments. They are associated like this
A user can have posts
A posts belongs to a user
A post can have many reviews
A review belongs to a user
Posts Model
class Post < ApplicationRecord
belongs_to :user
has_many :comments, dependent: :destroy
validates :title, presence: true
validates :body, presence: true
end
User Model
class User < ApplicationRecord
before_create { generate_token(:auth_token) }
before_save { self.email = email.downcase }
has_secure_password
has_many :posts
validates :name, presence: true
validates :email, presence: true, uniqueness: true
validates :password, confirmation: true
validates :password_confirmation, presence: true, unless: Proc.new { |a| !a.new_record? && a.password.blank? }
def send_password_reset
generate_token(:reset_password_token)
self.reset_password_sent_at = Time.zone.now
save!
UserMailer.password_reset(self).deliver
end
def generate_token(column)
begin
self[column] = SecureRandom.urlsafe_base64
end while User.exists?(column => self[column])
end
end
Review Model
class Review < ApplicationRecord
belongs_to :user
end
User Controller - show method
def show
#user = User.find(params[:id])
#posts = #user.posts
#reviews = #posts.reviews //This line shows error
end
I think something is wrong in the way i am associating these models.
I want to show comments made on a post with that post. I show from posts users controller....but i when i tried to display comments the same way. I
I had manually gone and made a comment to post in rails console.
Review table from schema
create_table "reviews", force: :cascade do |t|
t.string "comment"
t.string "user_id"
t.string "post_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
Can see two issues in the code.
1 - you haven't define the relationship between post and review in review model.
class Review < ApplicationRecord
belongs_to :user
belongs_to :post
end
2 - you are trying to get reviews out of posts relation. If you want to get all the reviews for a given user. you should probably need
def show
#user = User.find(params[:id])
#posts = #user.posts
#reviews = #user.reviews
end
or you may have to load reviews for each post in the view by
post.reviews

Validate multiple belongs_to relations

Let's say we have the following data structure (imagine as a loop).
MyModel * -- 1 User 1 -- * Clients 0 -- * MyModel
Thus, the MyModel looks like:
class MyModel < ActiveRecord::Base
belongs_to :client
belongs_to :user
attr_accessible :user_id, :client_id, # ...
validates :user_id, :presence => true
So it can belong to a client but must belong to a user. Also a client must belong to a user.
But how can I assert, that if someone saves a MyModel instance belonging to a client, that the client actually belongs to the user? Do I have to write a custom validator for this or is there any validation shortcut I overlooked?
Solution:
Following p.matsinopoulos' answer, I now did the following. I don't assign the foreign keys to MyModel before saving but rather the objects directly.
# my_model_controller.rb
def create
m = MyModel.create(whitelist_parameters(params[:my_model]))
m.user = current_user
if(params[:my_model][:client_id].present?)
client = Client.find params[:my_model][:client_id]
end
# ...
end
This way I can at first validate if there is a Client with such an ID at all. Then I implemented the custom validator by p.matsinopoulos. It's not tested yet, but since I am now working with the objects, it should do the trick.
If I were you, I would have written a custom validator.
validate :client_belongs_to_user
protected
def client_belongs_to_user
errors[:client] << 'must own the user' if client.present? && client.user != user
end
I would suggest, building on what you have:
class MyModel < ActiveRecord::Base
belongs_to :client
has_one :user, :through => :client
attribute_accessor :client_id
validates :client_id, :presence => true
...
end
class Client < ActiveRecord::Base
has_many :my_models
belongs_to :user
attribute_accessor :user_id
validates :user_id, :presence => true
...
end
class User < ActiveRecord::Base
has_many :clients
...
end

Undefined method 'build' in rails 3

I am getting a "NoMethodError in ProjectsController#create" with the following code:
def create
#project = current_user.project.build(params[:project])
if #project.save
flash[:success] = "Project created!"
redirect_to root_url
end
end
I have tried using #project = current_user.project.create(params[:project]) as well, but I get the same error, albeit for .create.
My Project model looks like this:
class Project < ActiveRecord::Base
attr_accessible :title,
:sub_title,
:desc,
:category
validates :user_id, presence: true
validates :title, presence: true, length: { maximum: 35 }
validates :category, presence: true
belongs_to :user
...
end
and my User model looks like this:
class User < ActiveRecord::Base
attr_accessible :name,
:surname,
:email,
:email_confirmation,
:password,
:password_confirmation
has_secure_password
has_one :project
...
end
From what I can tell, this should create a new Project with an association to the user.id and project.user_id. Any ideas why I get the error instead of successful creation?
For has_one associations you want:
#project = current_user.build_project(params[:project])
The same pattern is used for create:
#project = current_user.create_project(params[:project])
If you look at the has_one documentation they list the methods that get created when you declare the association.

Rails Rspec & FactoryGirl testing Association

I have to model's where I accept Nested Attributes. I would like to build a test to make sure the nested attribute cant be blank etc. I really don't understand how I can make the test.
My two simple models:
# SeoMapping Model
class SeoMapping < ActiveRecord::Base
belongs_to :mappingtable, :polymorphic => true
attr_accessible :seo_url
validates :seo_url, :presence => true, :uniqueness => true
end
# Page Model
class Page < ActiveRecord::Base
has_one :seo_mappings, :as => :mappingtable, :dependent => :destroy
accepts_nested_attributes_for :seo_mappings
attr_accessible :content, :h1, :meta_description, :title, :seo_mappings_attributes
.........
end
Here are my factories for Page and Seo:
FactoryGirl.define do
factory :page do |f|
seo_mapping
f.title { Faker::Name.name }
f.h1 { Faker::Lorem.words(5) }
f.meta_description { Faker::Lorem.words(10) }
f.content { Faker::Lorem.words(30) }
end
end
FactoryGirl.define do
factory :seo_mapping do |f|
f.seo_url { Faker::Internet.domain_word }
end
end
And my tests:
require 'spec_helper'
describe Page do
it "has a valid factory" do
expect(create(:page)).to be_valid
end
# Cant get this spec to work?
it "it is invalid without a seo_url" do
page = build(:page)
seo_mapping = build(:seo_mapping, seo_url: nil)
page.seo_mapping.should_not be_valid
# expect(build(:page, :seo_mapping_attributes[:seo_url] => nil)).to_not be_valid
end
it "is invalid without a title" do
expect(build(:page, title: nil)).to_not be_valid
end
...............
end
Usually for this sort of thing I use a gem called shoulda_matchers. It lets you simply assert that your model validates presence of specific attributes.
it { should validate_presence_of(:seo_url) }
it { should validate_uniqueness_of(:seo_url) }
If you don't want to use the gem, try something like this:
seo_mapping = build(:seo_mapping, seo_url: nil)
page = build(:page, seo_mapping: seo_mapping)
page.should_not be_valid

Options for validates_with

I'm not able to access the values, passed as option in 'validates_with'
My model:
class Person < ActiveRecord::Base
include ActiveModel::Validations
attr_accessible :name, :uid
validates :name, :presence => "true"
validates :uid, :presence => "true"
validates_with IdValidator, :attr => :uid
My Custom Validator:
Class IdValidator < ActiveModel::Validator
def validate(record)
puts options[:attr]
...
...
end
end
For testing purpose, I'm printing "options[:attr]" and all I see is ":uid" in the terminal and not the value in it. Please help!
When you pass in :attr => :uid, you're just passing in a symbol. There's no magic happening hereā€”it just takes the hash of options you've attached and delivers it as the options hash. So when you write it, you see the symbol you've passed.
What you probably want is
Class IdValidator < ActiveModel::Validator
def validate(record)
puts record.uid
...
...
end
end
Because validates_with is a class method, you can't get the values of an individual record in the options hash. If you are interested in a more DRY version, you could try something like:
class IdValidator < ActiveModel::Validator
def validate(record)
puts record[options[:field]]
end
end
class Person < ActiveRecord::Base
include ActiveModel::Validations
attr_accessible :name, :uid
validates :name, :presence => "true"
validates :uid, :presence => "true"
validates_with IdValidator, :field => :uid
end
Where you pass in the name of the field you want evaluated.