How do I prevent my last nested attribute from being destroyed? - ruby-on-rails

I have a customer model that has_many phones like so;
class Customer < ActiveRecord::Base
has_many :phones, as: :phoneable, dependent: :destroy
accepts_nested_attributes_for :phones, allow_destroy: true
end
class Phone < ActiveRecord::Base
belongs_to :phoneable, polymorphic: true
end
I want to make sure that the customer always has at least one nested phone model. At the moment I'm managing this in the browser but I want to provide a backup on the server (I've encountered a couple of customer records without phones and don't quite know how they got like that so I need a backstop).
I figure this must be achievable in the model but I'm not quite sure how to do it. All of my attempts so far have failed. I figure a before_destroy callback in the phone model is what I want but I don't know how to write this to prevent the destruction of the model. I also need it to allow the destruction of the model if the parent model has been destroyed. Any suggestion?

You can do it like this:
before_destory :check_phone_count
def check_phone_count
errors.add :base, "At least one phone must be present" unless phonable.phones.count > 1
end

Related

Rails model associations - has_one or single table inheritance?

I'm having trouble deciding between Single Table Inheritance and a simple has_one relationship for my two models.
Background: I'm creating a betting website with a "Wager" model. Users may create a wager, at which point it is displayed to all users who may accept the wager if they choose. The wager model has an enum with three statuses: created, accepted, and finished.
Now, I want to add the feature of a "Favorite Wager". The point of this is to make it more convenient for users to create a wager, if they have ones they commonly create. One click instead of ten.
FavoriteWagers exist only as a saved blueprint. They are simply the details of a wager -- when the User wants to create a Wager, they may view FavoriteWagers and click "create", which will take all the fields of the FavoriteWager and create a Wager with them. So the difference is that FavoriteWagers acts as only as a storage for Wager, and also includes a name specified by the user.
I read up on STI, and it seems that a lot of examples have multiple subclassing - eg. Car, Motorcycle, Boat for a "Vehicle" class. Whereas I won't have multiple subclasses, just one (FavoriteWager < Wager). People have also said to defer STI until I can have more classes. I can't see myself subclassing the Wagers class again anytime soon, so that's why I'm hesitant to do STI.
On the other hand, has_one doesn't seem to capture the relationship correctly. Here is an example:
Class User < ApplicationRecord
has_many :favorite_wagers, dependent: :destroy
has_many :wagers, dependent: destroy
end
Class FavoriteWager < ApplicationRecord
has_one :wager
belongs_to: user, index: true, foreign_key: true
end
Class Wager < ApplicationRecord
belongs_to :favorite_wager, optional: true
belongs_to :user
end
I've also thought about just copying the fields directly, but that's not very DRY. Adding an enum with a "draft" option seems too little, because I might need to add more fields in the future (eg. time to auto-create), at which point it starts to evolve into something different. Thoughts on how to approach this?
Why not just do a join table like:
Class User < ApplicationRecord
has_many :favorite_wagers, dependent: :destroy
has_many :wagers, through: :favorite_wagers
end
Class FavoriteWager < ApplicationRecord
belongs_to :wager, index: true, foreign_key: true
belongs_to :user, index: true, foreign_key: true
end
Class Wager < ApplicationRecord
has_one :favorite_wager, dependent: destroy
has_one :user, through: :favorite_wager
end
Your FavoriteWager would have the following fields:
|user_id|wager_id|name|
That way you can access it like:
some_user.favorite_wagers
=> [#<FavoriteWager:0x00007f9adb0fa2f8...
some_user.favorite_wagers.first.name
=> 'some name'
some_user.wagers.first.amount
=> '$10'
some_user.wagers.first.favorite_wager.name
=> 'some name'
which returns an array of favorite wagers. If you only want to have ONE favorite wager per user you can tweak it to limit that. But this gives you the ability to have wagers and users tied together as favorites with a name attribute. I don't quite understand your use case of 'a live wager never has a favorite' but that doesn't matter, you can tweak this to suit your needs.

rails association limit record based on an attribute

As you can see in the schema below, a user can create courses and submit a time and a video (like youtube) for it, via course_completion.
What I would like to do is to limit to 1 a course completion for a user, a given course and based one the attribute "pov" (point of view)
For instance for the course "high altitude race" a user can only have one course_completion with pov=true and/or one with pov=false
That mean when creating course completion I have to check if it already exist or not, and when updating I have to check it also and destroy the previous record (or update it).
I don't know if I'm clear enough on what I want to do, it may be because I have no idea how to do it properly with rails 4 (unless using tons of lines of codes avec useless checks).
I was thinking of putting everything in only one course_completion (normal_time, pov_time, normal_video, pov_video) but I don't really like the idea :/
Can someone help me on this ?
Thanks for any help !
Here are my classes:
class CourseCompletion < ActiveRecord::Base
belongs_to :course
belongs_to :user
belongs_to :video_info
# attribute pov
# attribute time
end
class Course < ActiveRecord::Base
belongs_to :user
end
class User < ActiveRecord::Base
has_many :courses
has_many :course_completions
end
You could use validates uniqueness with scoping Rails - Validations .
class CourseCompletion < ActiveRecord::Base
belongs_to :course
belongs_to :user
belongs_to :video_info
validates :course, uniqueness: { scope: :pov, message: "only one course per pov" }
# attribute pov
# attribute time
end

Rails AR - validating that parents of join table have same parent

I have the following classes:
class Account
has_many :payment_methods
has_many :orders
class PaymentMethod
belongs_to :account, inverse_of: :payment_methods
has_many :payments
class Order
belongs_to :account, inverse_of: :orders
has_many :payments
class Payment
belongs_to :order, inverse_of: :payments
belongs_to :payment_method, inverse_of: :payments
I have an order form that creates a payment as a child. That payment also has an associated payment_method. The idea is that placing an order will build the order and payment objects, the order will call payment.process (below), the payment will call a charge method on the payment_method object, and if all that passes, the payment and order are saved. Still with me?
Ok, so I need to validate at the payment level that both order and payment_method have the same parent (account), otherwise it needs to fail validation and not try to process. I have this method in the payment class:
def process
charge = payment_method.charge amount
self.is_paid = true
self.transaction_id = charge.id
rescue Exception => e
# handle exception
end
And this validation:
def payment_method_belongs_to_account
if payment_method and payment_method.account != order.account
# add error
end
end
I'm running into a few issues.
I am calling payment_method.charge before the payment is validated.
I can solve this by explicitly calling self.validate inside the payment.process method before calling payment_method.charge, but that leads to #2 below.
If I attempt to call validate on the payment object, I get an exception because payment.order is nil. The order hasn't been saved yet, so the reference inside the child object (payment) hasn't been set up.
It seems like solving for #2 is the easiest solution. I'm not sure why the reference from the child (payment) to the parent (order) isn't getting set up. Any suggestions?
I figured out the answer to my own question. I'll leave this here in case it can help someone else.
Adding this to my Order class fixed the problem:
has_many :payments, inverse_of: :order
It seems like that should happen automatically, but obviously not.

Creating new object with relations without the related id

I have a rails app with the following models:
class Product < ActiveRecord::Base
has_many :stores, through: :product_store
attr_accessible :name, :global_uuid
end
class ProductStore < ActiveRecord::Base
attr_accessible :deleted, :product_id, :store_id, :global_uuid
belongs_to :product
belongs_to :store
end
Since this model is for a REST API of a mobile app, I create the objets remotely, on the devices, and then sync with this model. As this happens, it may happen that I have to create a ProductStore before having an id set for Product. I know I could batch the API requests and find some workaround, but I've settled to have a global_uuid attribute that gets created in the mobile app and synced.
What I'd like to know is how can I make this code in my controller:
def create
#product_store = ProductStore.new(params[:product_store])
...
end
be aware that it will be receiving a product_global_uuid parameter instead of a product_id parameter and have it properly populate the model.
I figure I can override ProductStore#new but I'm not sure if there's any ramification when doing that.
Overriding .new is a dangerous business, you don't want to get involved in doing that. I would just go with:
class ProductStore < ActiveRecord::Base
attr_accessible :product_global_uuid
attr_accessor :product_global_uuid
belongs_to :product
before_validation :attach_product_using_global_uuid, on: :create
private
def attach_product_using_global_uuid
self.product = Product.find_by_global_uuid! #product_global_uuid
end
end
Having these kinds of artificial attr_accessors that are only used in model creation is kind of messy, and you want to avoid passing in anything that isn't a direct attribute of the model you are creating where possible. But as you say there are various considerations to balance, and it's not the worst thing in the world.

How do I create/maintain a valid reference to a particular object in an ActiveRecord association?

Using ActiveRecord, I have an object, Client, that zero or more Users (i.e. via a has_many association). Client also has a 'primary_contact' attribute that can be manually set, but always has to point to one of the associated users. I.e. primary_contact can only be blank if there are no associated users.
What's the best way to implement Client such that:
a) The first time a user is added to a client, primary_contact is set to point to that user?
b) The primary_contact is always guaranteed to be in the users association, unless all of the users are deleted? (This has two parts: when setting a new primary_contact or removing a user from the association)
In other words, how can I designate and reassign the title of "primary contact" to one of a given client's users? I've tinkered around with numerous filters and validations, but I just can't get it right. Any help would be appreciated.
UPDATE: Though I'm sure there are a myriad of solutions, I ended up having User inform Client when it is being deleted and then using a before_save call in Client to validate (and set, if necessary) its primary_contact. This call is triggered by User just before it is deleted. This doesn't catch all of the edge cases when updating associations, but it's good enough for what I need.
My solution is to do everything in the join model. I think this works correctly on the client transitions to or from zero associations, always guaranteeing a primary contact is designated if there is any existing association. I'd be interested to hear anyone's feedback.
I'm new here, so cannot comment on François below. I can only edit my own entry. His solution presumes user to client is one to many, whereas my solution presumes many to many. I was thinking the user model represented an "agent" or "rep" perhaps, and would surely manage multiple clients. The question is ambiguous in this regard.
class User < ActiveRecord::Base
has_many :user_clients, :dependent => true
has_many :clients, :through => :user_client
end
class UserClient < ActiveRecord::Base
belongs_to :user
belongs_to :client
# user_client join table contains :primary column
after_create :init_primary
before_destroy :preserve_primary
def init_primary
# first association for a client is always primary
if self.client.user_clients.length == 1
self.primary = true
self.save
end
end
def preserve_primary
if self.primary
#unless this is the last association, make soemone else primary
unless self.client.user_clients.length == 1
# there's gotta be a more concise way...
if self.client.user_clients[0].equal? self
self.client.user_clients[1].primary = true
else
self.client.user_clients[0].primary = true
end
end
end
end
end
class Client < ActiveRecord::Base
has_many :user_clients, :dependent => true
has_many :users, :through => :user_client
end
Though I'm sure there are a myriad of solutions, I ended up having User inform Client when it is being deleted and then using a before_save call in Client to validate (and set, if necessary) its primary_contact. This call is triggered by User just before it is deleted. This doesn't catch all of the edge cases when updating associations, but it's good enough for what I need.
I would do this using a boolean attribute on users. #has_one can be used to find the first model that has this boolean set to true.
class Client < AR::B
has_many :users, :dependent => :destroy
has_one :primary_contact, :class_name => "User",
:conditions => {:primary_contact => true},
:dependent => :destroy
end
class User < AR::B
belongs_to :client
after_save :ensure_only_primary
before_create :ensure_at_least_one_primary
after_destroy :select_another_primary
private
# We always want one primary contact, so find another one when I'm being
# deleted
def select_another_primary
return unless primary_contact?
u = self.client.users.first
u.update_attribute(:primary_contact, true) if u
end
def ensure_at_least_one_primary
return if self.client.users.count(:primary_contact).nonzero?
self.primary_contact = true
end
# We want only 1 primary contact, so if I am the primary contact, all other
# ones have to be secondary
def ensure_only_primary
return unless primary_contact?
self.client.users.update_all(["primary_contact = ?", false], ["id <> ?", self.id])
end
end

Resources