Rails 5 belongs_to weird behaviour - ruby-on-rails

Client Class
class Client < ApplicationRecord
validates_presence_of :name, :email
validates_email_format_of :email, :message => 'is not looking good'
validates_uniqueness_of :email
has_many :projects
end
Project Class
class Project < ApplicationRecord
belongs_to :client, optional: false
validates_presence_of :name
end
And schema of my tables
create_table "clients", force: :cascade do |t|
t.string "name"
t.string "email"
t.string "phone"
t.string "address"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "projects", force: :cascade do |t|
t.string "name"
t.integer "client_id"
t.string "description"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
Sometime I want to create Project without having client specified. So I added option :option false. And so now I'm able to create project without specifying any client id. However when I try to create project with client_id, it accepts any value for client_id i.e. if enter 8 and if this id is not present in client then also its accepted. I want in such cases it shouldn't save this project.
How can I achieve this behaviour?
I have rails version 5.1.4

First some changes, is optional: true, optional: false is the default behavior and expects to get a associated record every time, with that said, you could do something like this:
class Project < ApplicationRecord
belongs_to :client, optional: true
# Sorry, small bug, client_id? with value zero would return always false and it becomes zero when you input strings or false
# validates_presence_of :client, if: client_id?
validates_presence_of :client, unless: Proc.new{ |d| d.client_id.blank? }
end
This would validate the client record when the client_id is present
p = Project.new # => #<Project id: nil, client_id: nil, ...
p.valid? # => true
p.client = Client.first # => #Client id: 1, ...
p.valid? # => true
p.client = nil # => nil
p.valid? # => true
#Non-existent id 10
p.client_id = 10 # => 10
p.valid? # => false
p.errors.full_messages # => ["Client can't be blank"]
p.client_id = 1 # => 1
p.client # => #Client id: 1, ...
p.valid? # => true
You can even use a custom message
validates_presence_of :client, message: 'invalid client', if: :client_id?
Finally, just a recommendation, use t.references on your migrations to be able to use foreign keys and get this fields indexed
t.references :client, foreign_key: true

I would write your own custom validation to ensure that the id of the client actually exists in your database before persisting the project record:
class Project < ApplicationRecord
belongs_to :client, optional: false
validates_presence_of :name
validate :validate_client_id, if: :client_id?
private
def validate_client_id
errors.add(:client_id, "is invalid") unless Client.exists?(self.client_id)
end
end
The validation will only run if the field is not blank. Which is convenient since client_id is optional in your case.

Related

ActiveModel::MissingAttributeError (can't write unknown attribute `flights_count`):

I am doing some refactoring and I have seen this project for a while and it worked from what I last recall. But the issue is, I am trying to create a flight and I keep getting "ActiveModel::MissingAttributeError (can't write unknown attribute flights_count):" when trying create a new flight.
As far my models in place:
My Flight, Pilot models
class Flight < ActiveRecord::Base
has_many :passengers
belongs_to :destination
belongs_to :pilot, counter_cache: true
accepts_nested_attributes_for :passengers
belongs_to :user, class_name: "Flight" ,optional: true
validates_presence_of :flight_number
validates :flight_number, uniqueness: true
scope :order_by_flight_international, -> { order(flight_number: :asc).where("LENGTH(flight_number) > 3") }
scope :order_by_flight_domestic, -> { order(flight_number: :asc).where("LENGTH(flight_number) <= 2 ") }
def dest_name=(name)
self.destination = Destination.find_or_create_by(name: name)
end
def dest_name
self.destination ? self.destination.name : nil
end
def pilot_name=(name)
self.pilot = Pilot.find_or_create_by(name: name)
end
def pilot_name
self.pilot ? self.pilot.name : nil
end
end
class Pilot < ActiveRecord::Base
belongs_to :user, optional: true
has_many :flights
has_many :destinations, through: :flights
validates_presence_of :name, :rank
validates :name, uniqueness: true
scope :top_pilot, -> { order(flight_count: :desc).limit(1)}
end
Edit
Flight Controller
class FlightsController < ApplicationController
before_action :verified_user
layout 'flightlayout'
def index
#flights = Flight.order_by_flight_international
#dom_flights = Flight.order_by_flight_domestic
end
def new
#flight = Flight.new
10.times {#flight.passengers.build}
end
def create
#flight = Flight.new(flight_params)
# byebug
if #flight.save!
redirect_to flight_path(current_user,#flight)
else
flash.now[:danger] = 'Flight Number, Destination, and Pilot have to be selected at least'
render :new
end
end
private
def flight_params
params.require(:flight).permit(:flight_number,:date_of_flight, :flight_time, :flight_id, :destination_id, :pilot_id, :pilot_id =>[], :destination_id =>[], passengers_attributes:[:id, :name])
end
end
Edit
Flights, Pilot Schemas
create_table "flights", force: :cascade do |t|
t.integer "pilot_id"
t.integer "destination_id"
t.string "flight_number"
t.string "date_of_flight"
t.string "flight_time"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "pilots", force: :cascade do |t|
t.string "name"
t.string "rank"
t.integer "user_id"
t.integer "flight_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "flight_count", default: 0
end
As I said before when I last worked on this project everything was working fine, but I am faced with this issue. What am I doing wrong this time.
You have defined a counter_cache in your Flight model for pilots. When you just use counter_cache: true to define it, ActiveRecord will look for a column named flights_count in your pilots table but I see that you have named it as flight_count instead. You can either rename the column to flights_count or pass the custom column name to it by using counter_cache: :flight_count
Source https://guides.rubyonrails.org/association_basics.html#options-for-belongs-to-counter-cache

User_id is nil on rails association

I have a user model and a shout model. I am trying to have a user be associated with a shout. I did not make this association upon creation of the shouts table so I had to run a new migration. Below is my table, the models of each, and the output when from my console I run a command to try and find the user_id of a shout. Can you see what I am doing wrong?
schema:
create_table "shouts", force: :cascade do |t|
t.string "title"
t.text "description"
t.integer "user_id"
end
add_index "shouts", ["user_id"], name: "index_shouts_on_user_id"
create_table "users", force: :cascade do |t|
t.string "email", null: false
t.string "password_digest", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "username"
end
User Model:
class User < ActiveRecord::Base
validates :email, presence: true, uniqueness: true
validates :password_digest, presence: true
has_many :shouts
end
Shout Model:
class Shout < ActiveRecord::Base
belongs_to :user
end
Shout Controller:
class ShoutsController < ApplicationController
def new
#new_shout = Shout.new
end
def create
#new_shout = Shout.new(shouts_params)
if #new_shout.user_id == nil
render new_shout_path
elsif #new_shout.save
redirect_to dashboard_path
else
render new_shout_path
end
end
private
def shouts_params
params.require(:shout).permit(:title, :description, :user_id)
end
end
Some test code:
> Shout.find(4)
> #<Shout id: 4, title: "four", description: "four", user_id: nil>
Creating an instance of user from the console, working:
> User.first.shouts.create(title: 'four', description: 'four')
>[["title", "four"], ["description", "four"], ["user_id", 1]
Migration file:
class AddUserRefToShouts < ActiveRecord::Migration
def change
add_reference :shouts, :user, index: true, foreign_key: true
end
end
Here are a couple admittedly hacky options (but they'll work) if you don't want to follow the approach suggested in the comments. You could pass the user_id as a hidden field so it'll be included in the params or you can expressly set it in the create action.
If you want to pass as a hidden field, on your shout form add:
<%= f.hidden_field :user_id, value: current_user.id %>
Alternatively, if you want to handle in your create action:
def create
#shout = Shout.new(shout_params)
#shout.user_id = current_user.id
#shout.save
end

How to set values of nested parameters in Rails 4 from controller method?

I have request model and nested model filled_cartridges
when I create new request object i also need to set the values of nested params inside create controller method. I have many nested params so i need to iterate through them, something like that:
params[:request][:filled_cartridges_attributes].each do |_,value|
# here i try to set :client_id parameter, which is nested
# where #client.id is defined
value[:client_id] = #client.id # i am pretty sure that the problem is here
# is it a correct way of ding that?
end
EDIT:
has_many :filled_cartridges, inverse_of: :request, dependent: :destroy
accepts_nested_attributes_for :filled_cartridges, reject_if: proc { |attributes| attributes['title'].blank? },allow_destroy: true
and my nested model:
create_table :filled_cartridges do |t|
t.integer :client_id, null: false
t.string :cartridge_name, null: false
t.integer :cartridge_id, null: false
t.integer :request_id, null: false
t.integer :count, default: 1
t.datetime :fill_date
t.timestamps null: false
end
here client_id,request_id and cartridge_id are all should should be set inside controller.
And my strong params:
def request_params
params.require(:request).permit(:name, :address, :phone, :mobile,:date,
:filled_cartridges_attributes => [:client_id,:cartridge_name,:cartridge_id,
:request_id,:count,:fill_date,:_destroy,:id],
end

Counting Members Based on Invitations - Query

I am counting many user generated actions and for the most part it's easy, but with regard to one, more complex query, I am having trouble.
I have an invitations model and a user model and I can easily count the number of invitations the user sent, but I want to count the number of new members that signed up based on the invitations the existing member sent out.
In invitations, the invitees email is saved as recipient_email
Then, I know I can check that against new members email some how, but am not clear on the syntax.
Any help will be greatly appreciated. More information below.
Invitation Model:
class Invitation < ActiveRecord::Base
attr_accessible :recipient_email, :sender_id, :sent_at, :token
belongs_to :sender, :class_name => 'User'
has_one :recipient, :class_name => 'User'
validates_presence_of :recipient_email
validates_uniqueness_of :recipient_email, :message => '%{value} has already been invited'
validate :recipient_is_not_registered
validate :sender_has_invitations, :if => :sender
default_scope order: 'invitations.created_at DESC'
before_create :generate_token
before_create :decrement_sender_count, :if => :sender
after_create do |invitation|
InvitationMailer.delay.invitation_email(self)
end
def invitee
User.find_by_email(self.recipient_email)
end
def invitee_registered?
!invitee.blank?
end
def invitee_submitted?
!invitee.try(:submissions).blank?
end
private
def recipient_is_not_registered
errors.add :recipient_email, 'is already registered' if User.find_by_email(recipient_email)
end
def sender_has_invitations
unless sender.invitation_limit > 0
errors.add_to_base "You have reached your limit of invitations to send.
You can contact Lumeo if you'd like to request more."
end
end
def generate_token
self.token = Digest::SHA1.hexdigest([Time.now, rand].join)
end
def decrement_sender_count
sender.decrement! :invitation_limit
end
end
User Model:
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable, :mailchimp
attr_accessible :name, :email, :password, :password_confirmation,
:remember_me, :role_id, :role_ids, :image_attributes,
:terms, :profile_attributes, :current, :image, :roles,
:invitation_token, :join_mailing_list, :affiliate_id,
:invitation_affiliate_token, :affiliation, :referrer_id
validates_uniqueness_of :email
VALID_NAME_REGEX = /[\w]+([\s]+[\w]+){1}+/
validates :name, presence: true,
format: {with: VALID_NAME_REGEX}
#invitation
has_many :sent_invitations, :class_name => 'Invitation', :foreign_key => 'sender_id'
belongs_to :invitation
def invitation_token
invitation.token if invitation
end
def invitation_token=(token)
self.invitation = Invitation.find_by_token(token)
end
before_create :set_invitation_limit
has_one :invitation_affiliate, :class_name => "Affiliate", :foreign_key => 'token', :primary_key => 'invitation_affiliate_token'
private
def set_invitation_limit
self.invitation_limit = 100
end
end
Invitation and User Tables:
create_table "users", :force => true do |t|
t.string "email", :default => "", :null => false
t.string "encrypted_password", :default => "", :null => false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.integer "sign_in_count", :default => 0
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
t.integer "role_id"
t.string "name"
t.integer "invitation_id"
t.integer "invitation_limit"
end
create_table "invitations", :force => true do |t|
t.integer "sender_id"
t.string "recipient_email"
t.string "token"
t.datetime "sent_at"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
I could think of two different ways:
Add accepted field in invitations
You could add a boolean field for invitations named accepted, the default value will be false and you set it to true when the receipent accepts the invitation. Then you create a scope named accepted that returns only accepted invitations
scope :accepted, where(accepted: true)
You get what you want by #user.sent_invitations.accepted.count
2 . Do the following query
User.where(email: #user.sent_invitations.map(&:recipient_email)).count

Rspec testing of counter_cache column's returning 0

For days now I have been trying to get to the bottom of what seam to be something that should be very easy to do... I am however still very new to the world of rails and ruby and I just cant work this one out... :p
Anyway the problem I am having is that I have a number of :counter_cache columns in my model's, which are all working quite nicely when testing them manually. However I am wanting to do the TDD thing and I cant seam to test them in rspec for some unknown reason??
Anyway here is an example of my model's (User's, comments & Media):
class User < ActiveRecord::Base
has_many :comments
has_many :media, dependent: :destroy
end
class Comment < ActiveRecord::Base
attr_accessible :content, :user_id
belongs_to :commentable, polymorphic: true, :counter_cache => true
belongs_to :user, :counter_cache => true
validates :user_id, :presence => true
validates :content, :presence => true, :length => { :maximum => 255 }
end
class Medium < ActiveRecord::Base
attr_accessible :caption, :user_id
belongs_to :user, :counter_cache => true
has_many :comments, as: :commentable
validates :user_id, presence: true
validates :caption, length: { maximum: 140 }, allow_nil: true, allow_blank: true
default_scope order: 'media.created_at DESC'
end
Here are sample's of the table's schema setup:
create_table "users", :force => true do |t|
t.integer "comments_count", :default => 0, :null => false
t.integer "media_count", :default => 0, :null => false
end
create_table "comments", :force => true do |t|
t.text "content"
t.integer "commentable_id"
t.string "commentable_type"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
t.integer "user_id"
end
create_table "media", :force => true do |t|
t.integer "user_id"
t.string "caption"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
t.integer "comments_count", :default => 0, :null => false
end
And now here is an sample of an rspec example I have tried:
require 'spec_helper'
describe "Experimental" do
describe "counter_cache" do
let!(:user) { FactoryGirl.create(:user)}
subject { user }
before do
#media = user.media.create(caption: "Test media")
end
its "media array should include the media object" do
m = user.media
m.each do |e|
puts e.caption # Outputting "Test media" as expected
end
user.media.should include(#media) #This works
end
it "media_count should == 1 " do # This isnt working?
puts user.media_count #Outputting 0
user.media_count.should == 1
end
end
end
And finally the error message that rspec is giving me:
Failures:
1) Experimental counter_cache media_count should == 1
Failure/Error: user.media_count.should == 1
expected: 1
got: 0 (using ==)
# ./spec/models/experimental_spec.rb:24:in `block (3 levels) in <top (required)>'
Finished in 0.20934 seconds
2 examples, 1 failure
Also note that this is happening for all my counter_cache column in all my model's. I have also tried a number of different way's of testing this, but they are all returning the above error message.
Really hoping someone can help me out with this. :)
Thanks heaps in advance!
Luke
Update: This affects counter_culture the same way, and the solution below also fixes the problem for counter_culture.
The counter_cache is getting updated in the database directly. This will not affect the copy of the model you have loaded into memory so you need to reload it:
it "media_count should == 1 " do
user.reload
user.media_count.should == 1
end
But, I don't think that is how I would test this. As you have it, your test is very tightly coupled to setup code that seem like it doesn't need to be there at all. How about something like this for a stand alone spec:
it "has a counter cache" do
user = FactoryGirl.create(:user)
expect {
user.media.create(caption: "Test media")
}.to change { User.last.media_count }.by(1)
end
For testng counter_cache you can use Shoulda-Matchers gem, there is method out of the box

Resources