Rails has_many through save fail - ruby-on-rails

I've tried to save my model, but failed to save it.
#starship.rb
class Starship < ApplicationRecord
has_many :crew_members,inverse_of: :starship
accepts_nested_attributes_for :crew_members
has_many :holodeck_programs,inverse_of: :starship
accepts_nested_attributes_for :holodeck_programs
end
#crew_member.rb
class CrewMember < ApplicationRecord
belongs_to :starship
accepts_nested_attributes_for :starship
has_many :holodeck_programs,through: :starship
end
#holodeck_program.rb
class HolodeckProgram < ApplicationRecord
belongs_to :starship
belongs_to :crew_member
end
#controller
def create
#Starship,CrewMember and HolodeckProgram are new via CrewMember.new
#crew_member = CrewMember.new(crew_member_params)
#crew_member.save
.
.
end
.
.
private
def crew_member_params
params.require(:crew_member).permit(:name,:division,:starship_id,
starship_attributes: [:name,:id,
holodeck_programs_attributes: [:title,:starship_id,:crew_member_id]])
end
Because there is no crew_member_id in holodeck_programs_attributes, validation error happen.
I can not use inverse_of: :crew_member because of through in crew_member.rb
How can I handle it?

Actually you can create any number of crew_members, holodeck_programs while creating a starship.
But you are trying to create many starships, holodeck_programs for a single crew_members, look into
def crew_member_params
params.require(:crew_member).permit(:name,:division,:starship_id,
starship_attributes: [:name,:id,
holodeck_programs_attributes: [:title,:starship_id,:crew_member_id]])
end
So you need to change the create to,
def create
#starship = Starship.new(star_ship_params)
#starship.save
end
and crew_member_params to
def star_ship_params
params.require(:star_ship).permit(<attributes of star_ship>, holodeck_programs_attributes: [<attributes of holodeck_programs>], crew_members_attributes: [<attributes of crew_members>])
end
And also make sure that you have done all changes in the view accordingly
Please look into: NestedAttributes for more information regarding nested attributes and its usage.

Related

Rails has_many STI with sub STI

I think it is more of a "Model Design" issue than a rails issue.
For clarity sake here is the business logic: I've Venues and I want to implement multiple APIs to get data about those venues. All this APIs have a lot in common, therefore I used STI.
# /app/models/venue.rb
class Venue < ApplicationRecord
has_one :google_api
has_one :other_api
has_many :apis
end
# /app/models/api.rb
class Api < ApplicationRecord
belongs_to :venue
end
# /app/models/google_api.rb
class GoogleApi < Api
def find_venue_reference
# ...
end
def synch_data
# ...
end
end
# /app/models/other_api.rb
class OtherApi < Api
def find_venue_reference
# ...
end
def synch_data
# ...
end
end
That part works, now what I'm trying to add is Photos to the venue. I will be fetching those photos from the API and I realise that every API might be different. I thought about using STI for that as well and I will end up with something like that
# /app/models/api_photo.rb
class ApiPhoto < ApplicationRecord
belongs_to :api
end
# /app/models/google_api_photo.rb
class GoogleApiPhoto < ApiPhoto
def url
"www.google.com/#{reference}"
end
end
# /app/models/other_api_photo.rb
class OtherApiPhoto < ApiPhoto
def url
self[url] || nil
end
end
My goal being to have this at the end
# /app/models/venue.rb
class Venue < ApplicationRecord
has_one :google_api
has_one :other_api
has_many :apis
has_many :photos :through => :apis
end
# /app/views/venues/show.html.erb
<%# ... %>
#venue.photos.each do |photo|
photo.url
end
<%# ... %>
And photo.url will give me the right formatting that is dependent of the api it is.
As I'm going deeper in the integration, something seems not right. If I had to Api the has_many :google_api_photo then every Api will have GoogleApiPhoto. What does not make sense to me.
Any idea how I should proceed from here?
I think I solved it.
By adding this to venue.rb
has_many :apis, :dependent => :destroy
has_many :photos, :through => :apis, :source => :api_photos
By calling venue.photos[0].url call the right Class based on the type field of the ApiPhoto

rails current_model in other model method

In book/show I want to see it's sales in each existing library without abusing the views. Can the logic be somehow be transported into the model? Current book/show.haml:
= #book.name
- #libraries.each do |library|
= library.sales.where(book_id: #book.id).map(&:quantity).sum
My idea is to add a method in library.rb like:
def current_book_sold_by_library
#book = Book.find(:id)
#sales.where(book_id: #book.id).map(&:quantity).sum
sales.map(&:quantity).sum
end
But playing with this did not help. My setup:
book.rb:
class Book < ActiveRecord::Base
end
library.rb
class Library < ActiveRecord::Base
has_many :books, through: :sales
end
sale.rb
class Sale < ActiveRecord::Base
belongs_to :book
belongs_to :library
end
books_controller.rb
class BooksController < ApplicationController
def show
#libraries = Library.all
#sales = #book.sales
end
end
You may add a method with a book as a parameter to Library model:
# view
- #libraries.each do |library|
= library.book_sold_by_library(#book)
# Library model
def book_sold_by_library(book)
sales.where(book_id: book.id).map(&:quantity).sum
end

Prevent from raising ActiveRecord::RecordInvalid or adding twice on has_many association

I want to change has_many association behaviour
considering this basic data model
class Skill < ActiveRecord::Base
has_many :users, through: :skills_users
has_many :skills_users
end
class User < ActiveRecord::Base
has_many :skills, through: :skills_users, validate: true
has_many :skills_users
end
class SkillsUser < ActiveRecord::Base
belongs_to :user
belongs_to :skill
validates :user, :skill, presence: true
end
For adding a new skill we can easily do that :
john = User.create(name: 'John Doe')
tidy = Skill.create(name: 'Tidy')
john.skills << tidy
but if you do this twice we obtain a duplicate skill for this user
An possibility to prevent that is to check before adding
john.skills << tidy unless john.skills.include?(tidy)
But this is quite mean...
We can as well change ActiveRecord::Associations::CollectionProxy#<< behaviour like
module InvalidModelIgnoredSilently
def <<(*records)
super(records.to_a.keep_if { |r| !!include?(r) })
end
end
ActiveRecord::Associations::CollectionProxy.send :prepend, InvalidModelIgnoredSilently
to force CollectionProxy to ignore transparently adding duplicate records.
But I'm not happy with that.
We can add a validation on extra validation on SkillsUser
class SkillsUser < ActiveRecord::Base
belongs_to :user
belongs_to :skill
validates :user, :skill, presence: true
validates :user, uniqueness: { scope: :skill }
end
but in this case adding twice will raise up ActiveRecord::RecordInvalid and again we have to check before adding
or make a uglier hack on CollectionProxy
module InvalidModelIgnoredSilently
def <<(*records)
super(valid_records(records))
end
private
def valid_records(records)
records.with_object([]).each do |record, _valid_records|
begin
proxy_association.dup.concat(record)
_valid_records << record
rescue ActiveRecord::RecordInvalid
end
end
end
end
ActiveRecord::Associations::CollectionProxy.send :prepend, InvalidModelIgnoredSilently
But I'm still not happy with that.
To me the ideal and maybe missing methods on CollectionProxy are :
john.skills.push(tidy)
=> false
and
john.skills.push!(tidy)
=> ActiveRecord::RecordInvalid
Any idea how I can do that nicely?
-- EDIT --
A way I found to avoid throwing Exception is throwing an Exception!
class User < ActiveRecord::Base
has_many :skills, through: :skills_users, before_add: :check_presence
has_many :skills_users
private
def check_presence(skill)
raise ActiveRecord::Rollback if skills.include?(skill)
end
end
Isn't based on validations, neither a generic solution, but can help...
Perhaps i'm not understanding the problem but here is what I'd do:
Add a constraint on the DB level to make sure the data is clean, no matter how things are implemented
Make sure that skill is not added multiple times (on the client)
Can you show me the migration that created your SkillsUser table.
the better if you show me the indexes of SkillsUser table that you have.
i usually use has_and_belongs_to_many instead of has_many - through.
try to add this migration
$ rails g migration add_id_to_skills_users id:primary_key
# change the has_many - through TO has_and_belongs_to_many
no need for validations if you have double index "skills_users".
hope it helps you.

Saving Rails associations after creating User

I'm new to Rails and ActiveRecord and need some help. Basically, I have 4 models: User, Property, PropertyAccount, and AccountInvitation. Users and Properties have a many to many relationship via PropertyAccounts. AccountInvitations have a user's email and a property_id.
What I want to happen is that after a user registers on my app, his user account is automatically associated with some pre-created Properties. What I don't know how to do is write the query to get the Property objects from the AccountInvitations and save them to the User object. Please see def assign_properties for my pseudo code. Any help is welcome, thanks so much!
class User < ActiveRecord::Base
has_many :property_accounts
has_many :properties, through: :property_accounts
after_create :assign_properties
# Check to see if user has any pre-assigned properties, and if so assign them
def assign_properties
account_invitations = AccountInvitations.where(email: self.email)
if account_invitations.any?
account_invitations.each do |i|
properties += Property.find(i.property_id)
end
self.properties = properties
self.save
end
end
end
class AccountInvitation < ActiveRecord::Base
belongs_to :property
validates :property_id, presence: true
validates :email, uniqueness: {scope: :property_id}
end
class Property < ActiveRecord::Base
has_many :account_invitations
has_many :property_accounts
has_many :users, through: :property_accounts
end
class PropertyAccount < ActiveRecord::Base
belongs_to :property
belongs_to :user
end
Thanks to #wangthony , I looked at the includes method on http://apidock.com/rails/ActiveRecord/QueryMethods/includes and tweaked one of their examples in order to get this to work. Here's the solution:
def assign_property
self.properties = Property.includes(:account_invitations).where('account_invitations.email = ?', self.email).references(:account_invitations)
self.save
end
I believe you can do this:
user.properties = Property.includes(:account_invitations).where(email: user.email)
user.save

Simpler way to update big decimal column in rails?

I am building a simple budgeting app, and have a line of code that feels convoluted and overly complex. For context:
class User < ActiveRecord::Base
has_one :month_budget
has_many :expenditures, as: :spendable
end
class MonthBudget < ActiveRecord::Base
belongs_to :user
has_many :expenditures, as: spendable
end
class Expenditure < ActiveRecord::Base
belongs_to :spendable, polymorphic: true
end
Within my Expenditure class, I have defined a class method, add_expenditure:
class Expenditure < ActiveRecord::Base
def self.add_expenditure(user, params) #params passed will be in [:expenditure][*keys], in which possible keys are [:amount] or [:location]
if user.month_budget
user.month_budget.expenditures.create(params)
new_amount = user.month_budget.current_amount += params[:amount].to_d
user.month_budget.update(current_amount: new_amount)
end
end
end
Is there a more efficient way to add a value to the initial month_budget.current_amount column, and then record this new number to the database?
Cheers in advance!
Maybe you could try increment! method (http://apidock.com/rails/v4.2.1/ActiveRecord/Persistence/increment%21).
However, I am not sure if it works well with big decimals.
class Expenditure < ActiveRecord::Base
def self.add_expenditure(user, params)
if user.month_budget
user.month_budget.expenditures.create(params)
user.month_budget.increment!(:current_amount, params[:amount].to_d)
end
end
end

Resources