rails add restrictions to association based on enum value - ruby-on-rails

I have a Media model which has an enum
enum asset_type: { broadcast: 1, video: 2, audio: 3 }
and another model called Post and has an association with media
has_many :medias, dependent: :destroy
accepts_nested_attributes_for :medias
I want to add restrictions that per Post that would one media for asset type audio but I still want mutliple broadcast and video.
how can I achieve that?

I'm not sure if defining a has_one relationship really makes sense given the enum.
If you just need to enforce this on record creation or updates, consider handling it with callbacks.
An example on Post:
class Post
has_many :medias, dependent: :destroy
accepts_nested_attributes_for :medias
before_save :ensure_only_one_audio_media
...
private
def ensure_only_one_audio_media
errors.add(:medias, 'already has an audio media attached') if medias.where(asset_type: 3).size > 1
end
end
But depending on how you save medias to posts, you might need to validate this on Media instead.
If you're not locked into the current schema, and really want to do this with a relationship, one of the clearest ways would be to separate Audio to be its own Object.
class Post
has_many :medias, dependent: :destroy
has_one :audio
accepts_nested_attributes_for :medias, :audio
I think this could be a good example of "designing your UX to meet your requirements". Instead of creating some safety catch at the model and dB level, just ensure your users don't have the opportunity to add more audio media to a post that already has one. In essence, I'm suggesting you can do this at the controller and view level without changing any of your existing code.

Related

Please explain the has_many, through: source: Rails Association

I've found a bunch of articles, stackoverflow answers and rails documentation about 'source:', but none of it explains this association in a way I can understand it. I need the most simplified explanation of this way of associating, if possible.
My example is this:
Album:
has_many :reviews, :dependent => :destroy
has_many :reviewers, through: :reviews, source: :user
belongs_to :user
Review:
belongs_to :album, optional: true
belongs_to :user
User:
has_many :reviews
has_many :reviewed_albums, through: :reviews, source: :album
has_many :albums
The rest of the code does not mention "reviewers" or "reviewed_albums", so that is the part I understand the least.
Are those names completely irrelevant?
TL;DR
source is standing for the table that this association is referring to, since we give it a different name than just .users because we already have the belongs_to :user association.
Long explanation
I think it's easiest with this little picture which is basically the database schema for the models you posted above.
We have albums, that belong to users, meaning that a user is basically someone who creates an album. We also have reviews and they belong to albums, meaning an album can be reviewed. And a review is made by a user so that's why a review belongs to a user.
Now associations in rails is a way to create methods that can be called on a database record to find its associated record.
We could get a user from an album or all the reviews a user made for example.
album = Album.find(1)
album.user # => returns the creator of the album
user = User.first
user.reviews # => returns all the reviews a user made
Now there is even more connections between those models than the ones mentioned above.
Let's look at album first:
# album.rb
has_many :reviews, :dependent => :destroy
belongs_to :user
has_many :reviewers, through: :reviews, source: :user
An album belongs to one user who created it. It has many reviews. And, if we we follow the line from albums to reviews and then further along to the users table, we see that we can also access the users that gave the reviews.
So we would want to do something like
album.reviews.users
Meaning: give me all the users that left a review for this album. Now this line of code wouldn't work - because album.reviews returns an array (an ActiveRecord::Relation object to be exact) and we cannot just call .users on this.
But we can have another association
has_many :reviewers, through: :reviews, source: :user
And here we're calling it reviewers to not get confused with the method/association .user that refers to the creator. Normally, Rails would refer the database table name from the name of the association. Since we're giving a different name here, we have to explicitly give the name of the DB table we're referring to and that is the users table.
So this is what this line is about - we create another association, we don't want the direct line (see image) between album and user, we want the users that left a review on this album, so we go through the reviews table and then we have to give the name (source) of the table so Rails knows in which table to look.
And this will finally allow us to write code like this:
album = Album.first
album.user # => creator of the album
album.reviewers # => all users that have left a review for this album
Hope that helps! Let me know if you have any more questions.
Maybe you can explain the other association with source in the users model in the comments.

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.

Persist array from before_save callback for ActiveModel::Dirty like change tracking on has_many through relationship

Firstly, apologies for the snappy question title! It does however sum up what I am trying to do.
I've been using ActiveModel::Dirty successfully to create a kind of audit trail on various model attributes within my app (like on Product below).
I've now have a fairly pressing request to be able to track the changes (additions & deletions in this case) on an associated has_many through relationship.
The models in question are:
class Product < ActiveRecord::Base
has_many :products_territories, :dependent => :destroy
has_many :territories, :through => :products_territories
end
class Territory < ActiveRecord::Base
has_many :products_territories
has_many :products, :through => :products_territories
end
class ProductsTerritory < ActiveRecord::Base
belongs_to :territory
belongs_to :product
end
I've failed with using ActiveModel::Dirty, it doesn't seem possible, so am trying my own thing which on the surface is quite simple; grab an array of a product's products_territories before_save and then again after_save and then perform comparisons on the two arrays to identify the additions and deletions. What I can't get my head around is the best way to persist the array of products_territories from the before save so it's then available to my after_save callback. I'm pretty certain ## class variables aren't the way to go and i'm also not so sure about session variables. I'm wondering whether something like Redis or Memchached is what I should be looking at?
Can anyone that's had to do something similar to this give me any pointers or direct me to some further reading please?
Thanks in advance.

Is it possible to have multiple has_many associations with one specific model in Rails?

I have two models with the following associations:
organization.rb
class Organization < ActiveRecord::Base
has_one :user, as: :identifiable
has_many :speakers
#has_many :cast_items
end
speaker.rb
class Speaker < ActiveRecord::Base
has_one :user, as: :identifiable
#has_many :cast_items
end
As you can see, I've commented out an association with the CastItem model.
I want a Speaker to add multiple CastItems. Also, an Organization must be able to add multiple CastItems. When an Organization adds a CastItem, it does not necessarily belongs to a Speaker who is associated with an Organization. In other words an organization must be able to add a CastItem to itself or to a Speaker who is associated with him.
Will it be completely valid to put the has_many :cast_items in both models, or are there more practical design options?
Yes, you can do that. Remember to add organization_id and speaker_id to your cast_items model.
You can check out this link, http://guides.rubyonrails.org/association_basics.html , some useful information regarding many to many and one to many associations.
Personally, in your case, I will use has_many :through
You can definitely do that. I can't think of any reason that would be bad and it's often necessary.
You may want to look up the 'delegate' method for when you're creating CastItems, and have them always created by Organizations.
Also, make sure that if you have a :speaker_id on your CastItem that it can accept nil or false.

How to associate descendant models to a parent

I have the following models:
Account
has_many :libraries
Library
has_many :topics
belongs_to :account
Topic
has_many :functions
belongs_to :library
Function
has_one :example
belongs_to :topic
Example
belongs_to :function
I would like to be able to able to do things such as:
some_account.libraries
some_account.topics
some_account.functions
some_account.examples
In addition, I would like to be able to assign an account to a descendant, i.e
some_example.account = some_account
some_function.account = some_account
some_topic.account = some_account
some_library.account = some_account
To give some context:
I am letting a user (Account) create each Library, Topic, Function, Example. record separately. Then a user is free to change how the records are associated: Change the topic of a Function, move a Topic to a different Library, add an example to a function, and so on.
To my understanding no matter what record is created, I would need to assign it to a user (account) so that I can have a list of each Model records that a user has created, as well as prevent other users from seeing stuff that doesn't belong to them
Although I might be overcomplicating, I really don't know :(
Thanks in advance.
Just put
belongs_to :account
on each entity a user can make... and add a foreign key, and
Account
has_many :libraries
has_many :topics
has_many :functions
has_many :examples
(Note: I use the hobo_fields gem to make migrations easier)
That way.. if they change which functions are in which topics etc.. you can't loose who created it.
If you want to make sure users cannot add their topics to someone else's library just put validation on the record to prevent it.

Resources