Rails polymorphic association not available in models - ruby-on-rails

I am new to rails so please bear with me, I've search all day on this. Apologize if this is a beginner questions :)
I have a model with a polymorphic association defined:
class SocialLink < ActiveRecord::Base
belongs_to :social, polymorphic: true
end
And two models that should have one of this association
class Staff < ActiveRecord::Base
validates :name, presence: true
belongs_to :establishment
has_one :image, as: :imageable
has_one :social_link, as: :social
end
class Establishment < ActiveRecord::Base
validates :name, presence: true
has_one :location
has_one :social_link, as: :social
has_many :staff
accepts_nested_attributes_for :location
accepts_nested_attributes_for :staff
end
Edit: initial table creation migration for social links
class CreateSocialLinks < ActiveRecord::Migration
def change
create_table :social_links do |t|
t.string :facebook
t.string :twitter
t.string :yelp
t.string :google_plus
t.string :youtube
t.string :instagram
t.string :linkedin
t.timestamps null: false
end
end
end
The migration for this was created like so (Edit: please note the table existed at time of migration)
class AddSocialLinkReferenceToEstablishmentAndStaff < ActiveRecord::Migration
def change
add_reference :social_links, :social_links, polymorphic: true, index: true
end
end
class UpdateSocialLinkReference < ActiveRecord::Migration
def change
remove_reference :social_links, :social_links
add_reference :social_links, :social, polymorphic: true, index: true
end
end
Edit: after the above migration, the social_id and social_type are available
#<SocialLink id: nil, facebook: nil, twitter: nil, yelp: nil, google_plus: nil, youtube: nil, instagram: nil, linkedin: nil, created_at: nil, updated_at: nil, social_id: nil, social_type: nil>
However, for some reason the association isn't available on either of the models. I am having trouble seeing what I have done wrong, it looks like I've set it up the same as another polymorphic association on an image model that is working
This one is working
class Image < ActiveRecord::Base
belongs_to :imageable, polymorphic: true
end
Thanks or the help!

according to the rails guide on polymorphic associations your migration should look more like this:
class CreateSocialLinks < ActiveRecord::Migration
def change
create_table :social_links do |t|
t.string :name
t.references :social, polymorphic: true, index: true
t.timestamps null: false
end
end
end
otherwise the table will be misssing

The problem was that I was trying to access the association with the polymorphic name instead of just using the model name. Instead of doing staff.social I should have used staff.social_link. I got confused on this part.
Sorry for the confusion

Related

RAILS - CHANGE FIELD OF ANOTHER TABLE - BOOLEAN

Good afternoon. I'm new to rails and I'm using google translate to post in English here, so sorry if it's not very readable.
My question is, I have a User table, and a Setting table.
They are related (but I don't know if the relationship is correct), they can even confirm me, and I would like to know if:
when creating a user, I would like to automatically change the "email" and "push" fields of that user's settings table to true.
Would it be possible via a method that in the user model called: "setting_default"?
User model.
class User < ApplicationRecord
has_one :setting
before_save :setting_default
def setting_default
self.setting.update(:email, 'true')
self.setting.update(:push, 'true')
end
Setting Model
class Setting < ApplicationRecord
has_one :user
end
The Controller is normal, if you need it, I can put it in the post
My migration:
class CreateSettings < ActiveRecord::Migration[6.0]
def change
create_table :settings do |t|
t.boolean :email, default: true
t.boolean :push, default: true
t.timestamps
end
end
end
class AddSettingsToUser < ActiveRecord::Migration[6.0]
def change
add_reference :users, :setting, null: true, foreign_key: true
end
end
Google translate has worked well for you here.
First off you'll want to change your Setting model to belong to the User:
class Setting < ApplicationRecord
belongs_to :user
end
Your settings DB table is missing a user_id field to tie the setting back to the user. I'm not used to the add_reference technique so I just do things myself in the migrations. This would work:
class CreateSettings < ActiveRecord::Migration[6.0]
def change
create_table :settings do |t|
t.integer :user_id
t.boolean :email, default: true
t.boolean :push, default: true
t.timestamps
end
end
end
(Make note that your users DB table has a field setting_id that it does not need. I don't think it should be there. I would remove it. Unless it's a Rails 6 thing I'm not used to.)
Next it would probably be better to assign the values if the save succeeds (and not if it fails) so you'll want an after_save instead. And I'm simplifying your value assignment just in case you're having an issue there:
class User < ApplicationRecord
has_one :setting
after_save :setting_default
def setting_default
setting.email = true
setting.push = true
setting.save(validate: false)
end
private :setting_default
And to answer what seems to be your question, yes, what you're trying to do should be easily possible. This is a very common thing to do. It should work.
When you use one-to-one association you need to choose has_one in one and belongs_to in another model
Semantically user has one setting, but not setting has one user
So it's better to reverse them
To change your schema you need to write new migration
class ChangeOneToOneDirection < ActiveRecord::Migration[6.0]
def up
change_table :settings do |t|
t.belongs_to :user, foreign_key: true, null: false
end
User.where.not(setting_id: nil).find_each |user|
Setting.find(user.setting_id).update_columns(user_id: user.id)
end
change_table :users do |t|
t.remove :setting_id
end
end
def down
add_reference :users, :setting, null: true, foreign_key: true
Setting.find_each do |setting|
User.find(setting.user_id).update_columns(setting_id: setting.id)
end
change_table :settings do |t|
t.remove :user_id
end
end
end
After migration you can change User model
class User < ApplicationRecord
has_one :setting
after_commit :setting_default
private
def setting_default
setting&.update(email: true, push: true)
end
end
It's better to update associated model only if saves are in the database. And user can haven't setting. That's why after_commit and safe-navigator &

I want to be able to store values even if the related column is empty in rails

What we want to achieve
I'm doing the association in Rails, I'm associating the Schedule table with the PostItem table, and I'm associating the PostItem with belongs_to in the Schedule table, even if the PostItem_id associated with it in the Schedule table is empty. I would like to be able to save the file. In other words, I don't want to make the association every time; I always need the PostItem_id when I make the association within migration. Is there any way to allow saving even if the associated parent is empty?
Code
Migration File
class CreateSchedules < ActiveRecord::Migration[6.0]
def change
create_table :schedules do |t|
t.string :name
t.string :color
t.integer :start
t.integer :end
t.boolean :timed
t.boolean :long_time
t.integer :postItem_id # I want to save the file even if it is empty.
t.timestamps
end
end
end
Schedule Model
class Schedule < ApplicationRecord
belongs_to :post_item
validates :start, presence: true
validates :end, presence: true
# validates :postItem_id, allow_nill: true, allow_blank: true
end
postItem model
class PostItem < ApplicationRecord
belongs_to :post
has_many :schedules
end
After Rails 5 belongs_to associations are required by default. If you want it to be optional, you can add optional: true.
belongs_to :post_item, optional: true
You can also change this behaviour per model by doing:
self.belongs_to_required_by_default = false
https://guides.rubyonrails.org/upgrading_ruby_on_rails.html#active-record-belongs-to-required-by-default-option

Model associations callbacks

I would like to add a review section to my app. To be more specific, a user can leave a review for a shop and the shop can then reply to that review. But I'm not sure if the model associations and review table migrations I have are correct.
class User < ActiveRecord::Base
has_many :reviews
end
class Review < ActiveRecord::Base
belongs_to :user
end
class ReviewReply < ApplicationRecord
belongs_to :user, optional: true
belongs_to :review, optional: true
end
class Shop < ActiveRecord::Base
has_many :reviews
end
class CreateReviews < ActiveRecord::Migration[6.0]
def change
create_table :reviews do |t|
t.text :body
t.integer :rating
t.references :shop, null: false, foreign_key: true
t.references :user, null: false, foreign_key: true
t.timestamps
end
end
end
Architecture:
1. CALCULATING RATING
console:
rails g migration add_rating
migration:
def change
add_column :shops, :average_rating, :integer, default: 0, null: false
add_column :reviews, :rating, :integer, default: 0, null: false
end
user.rb
has_many :reviews
shop.rb
has_many :reviews
def update_rating
if reviews.any? && reviews.where.not(rating: nil).any?
update_column :average_rating, reviews.average(:rating).round(2).to_f
else
update_column :average_rating, 0
end
end
review.rb
belongs_to :user
belongs_to :shop
has_one :review_reply
after_save do
unless rating.nil? || rating.zero?
shop.update_rating
end
end
after_destroy do
shop.update_rating
end
review_reply.rb
belongs_to :review
2. REPLY TO A REVIEW
views/reviews/show.html.erb:
<% unless #review.review_reply.present? %>
<%= link_to "Write a Reply", new_review_reply_path(review_id: #review.id) %>
<% end %>
views/review_replies/form.html.erb
= f.input :review_id, input_html: {value: params[:review_id]}, as: :hidden
There are a couple of things things that I would change if it's a real application (= not just something you're toying around with):
I'd remove the optional: true from the two associations in ReviewReply since replies don't make sense (data-wise) if they don't have an author or aren't connected to a review.
I'd also set these two columns in review_replies to null: false.
You should think about adding some deletion cascades (either adding dependent: :some_action in models or using on_delete: :some_action on the database columns – I'd recommend the latter):
delete review replies when a review is deleted?
delete review replies when a user is deleted? (or just set it to NULL and then show "Deleted User" in the UI?)
delete reviews when a shop is deleted?
delete reviews when a user is deleted? (or just set it to NULL and then show "Deleted User" in the UI?)
In your migration polymorphic relation for reviewable is set up incorrectly - unique index on reviewable_type will prevent adding multiple records, better use
t.references :reviewable, polymorphic: true
in modern rails it adds non-unique index on both columns by default (you can be explicit with index: true, but in any way this is different from two separate indexes on each column alone)
Also most probably you want your unique index to include user id so that each user can review each reviewable once:
t.index [:reviewable_type, :reviewable_id, :user_id], unique: true, name: 'idx_unique_user_review'
Reason for including both shop and reviewable is not clear, but it depends on your application and goals. If the shop itself is the reviewable (as suggested by has_many :reviews, as: :reviewable) - then shop reference is useless. But in fact if you do not plan on extending reviews on something other than shops - it's easier to go with non-polymorphic reference for now.
In large app it's better to have common name prefixes for related things, so ReviewReply model name is better. belongs_to :review is most probably not optional, it's very strange to reply to nothing. Also it will most likely have belongs_to :shop (also not optional) not user, since the shop is the one replying.

Associating two models in a way both models' attributes are always pulled together in any view

I'm building a tech-specific pricegrabber-like web app, and I have a model that carries params that are common in all products. This model is called Product. Then I have one model for each type of product that I'm going to work with, for example, I'm now trying to build the first specific model, which is Videocard. So, the Product model always must have one Specific model, in this case Product-Videocard.
At this moment I'm stuck finding a way to make a product and a specific model always come tied together whenever I reach to them, be it in an index view, show view, form_for, a search, etc. But I can't picture in my head how a form will create an item and its specifications and insert a foreign key into another model with only one submit request.
Below are both models and the migrations for each:
class Product < ApplicationRecord
#belongs_to :productable, :polymorphic => true
has_one :videocard, dependent: :destroy
# Comment for this Stackoverflow question: the way I'm thinking I
# should have to make tons of has_one associations, for the other
# products. Is there a DRY way to do this?
has_many :prices, through: :stores
validates :platform, presence: { message: "should be specified." }
validates :name, presence: { message: "should be specified." }
validates_associated :videocard
end
class Videocard < ApplicationRecord
belongs_to :product
end
Migrations (shortened to make this question as clear as possible):
class CreateProducts < ActiveRecord::Migration[5.0]
def change
create_table :products do |t|
t.references :productable, polymorphic: true, index: true
t.string :name
t.string :image
t.string :partnum
t.string :manufacturer
t.string :platform #mobile, desktop, server, laptop
t.timestamps
end
end
end
class CreateVideocards < ActiveRecord::Migration[5.0]
def change
create_table :videocards do |t|
t.references :product, index: true
t.integer :memory
t.string :interface
# [...lots of attributes...]
t.integer :displayport
t.integer :minidisplayport
t.integer :tdp
t.timestamps
end
end
end
Also how can I make it so that Product only needs one has_one association, instead of using multiple ones. Remember that Videocard will have one type of specification, Memory will have other, and so on.

Rails: incrementing attribute from a model, upon creation of an instance from another model

Our Rails app works with the following models:
class User < ActiveRecord::Base
has_many :administrations, dependent: :destroy
has_many :calendars, through: :administrations
end
class Administration < ActiveRecord::Base
belongs_to :user
belongs_to :calendar
end
class Calendar < ActiveRecord::Base
has_many :administrations, dependent: :destroy
has_many :users, through: :administrations
end
And here are our migrations:
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :first_name
t.string :last_name
t.string :email
...
t.integer :total_calendar_count
t.integer :owned_calendar_count
t.timestamps null: false
end
end
end
class CreateAdministrations < ActiveRecord::Migration
def change
create_table :administrations do |t|
t.references :user, index: true, foreign_key: true
t.references :calendar, index: true, foreign_key: true
t.string :role
t.timestamps null: false
end
end
end
class CreateCalendars < ActiveRecord::Migration
def change
create_table :calendars do |t|
t.string :name
t.timestamps null: false
end
end
end
When a new #calendar is created, we need to increment :total_calendar_count and :owner_calendar_count by one in the User table.
We tried this in the CalendarsController:
class CalendarsController < ApplicationController
def create
#calendar = current_user.calendars.create(calendar_params)
current_user.total_calendar_count += 1
current_user.owned_calendar_count += 1
current_user.administrations.find_by(calendar_id: #calendar.id).update(role: 'Creator')
...
end
But it does not seem to update :total_calendar_count and :owner_calendar_count by one in the User table.
Are we missing a step here? Should we use an update action instead?
The actual problem in your code is that you don't then save the user.
So you update the counter... but this changes it on the local instance... and then after the controller action is done the change you made just disappears.
if you wanted to keep your code the way it is, you could do:
current_user.save
at the end.
but I'd advise you to look into the counter_cache, because it's the Rails way.
Also I'll point out that you haven't checked that the calendar successfully got created, before incrementing that counter... it's possible that it could fail a validation and not really have been created... you need to check for that first.
I have a best idea to solve your problems is as below....
Create a method that will call on the creating of calendar with the callbacks of model like as below...
Add the below inside the calendar model just after the validation and ORM relations
after_create :increment_counter
def increment_counter
calendar_user = self.user
calendar_user.update(:total_calendar_count += 1, :owned_calendar_count += 1 )
end
With the above code you don't need to do anything. It will increment the counter of calendar on every new entry of calendar.

Resources