Avoid explicit reloading of association - ruby-on-rails

I have the following models:
class Model < ApplicationRecord
has_many :states
has_one :state, -> { order(created_at: :desc) }, class_name: 'State'
def update_state!(state)
states.create!(state: state)
end
end
class State < ApplicationRecord
belongs_to :model
end
If code is kept like this, then this is the behaviour:
m = Model.new(states: State.new(state: 'initial'))
m.save!
m.state.state # => "initial"
m.update_state!("final")
m.state.state # => "initial"
One workaround would be changing the Model#update_state! to:
def update_state!(state)
states.create!(state: state)
reload
end
Which kind of sucks.
Is there anyway to handle this without having to refresh the record? Something tells me I am missing something...
In this scenario, I might understand that Rails might not know how to relate :state with :states... However, I even tried making #state a plain method instead of an association, but I am still facing the same issue.
class Model < ApplicationRecord
has_many :states
def state
states.max_by(&:created_at)
end
def update_state!(state)
states.create!(state: state)
end
end
class State < ApplicationRecord
belongs_to :model
end

I would add a foreign key column to models as a "shortcut" to the latest record:
class AddStateToModels < ActiveRecord::Migration[6.0]
change_table :models do |t|
t.references :state
end
end
class Model < ApplicationRecord
has_many :states,
after_add: ->(state){ update_columns(state_id: state.id) }
belongs_to :state, class_name: 'State'
def update_state!(state)
states.create!(state: state)
end
end
This is a good read optimization if you are displaying a list of Model instances and there current state as it lets you do a very effective query:
#models = Model.eager_load(:current_state)
.all

Related

Rails - How to model object privacy settings and associations

I have Forms created by Users. Every form is only visible to the creator. I would like to grant permission to other users to see a specific form. One could say I want to whitelist other users for a specific form.
Here's what I tried by creating a third model called "SharedForm".
app/models/form.rb
Class Form < ApplicationRecord
belongs_to :user
...
end
app/models/user.rb
Class User < ApplicationRecord
has_many :forms
has_many :forms, through: :sharedforms
...
end
app/models/shared_form.rb
Class SharedForm < ApplicationRecord
belongs_to :user
belongs_to :form
...
end
migration
class CreateSharedForms < ActiveRecord::Migration[5.0]
def change
create_table :shared_forms do |t|
t.integer :form_id, index: true
t.integer :user_id, index: true
t.timestamps
end
add_foreign_key :shared_forms, :users, column: :user_id
add_foreign_key :shared_forms, :forms, column: :form_id
end
end
In order to present both user forms and forms shared with the user I defined the index as:
app/controllers/forms_controller.rb
Class FormsController < ApplicationController
def index
#forms = Form.where(user_id: current_user.id)
shared = SharedForm.where(user_id: current_user.id)
#sharedforms = Form.where(id: shared)
end
end
This doesn't work.
Is there a way to access the records I need by user.forms and user.sharedforms respectively?
You can't use the same name for two associations as the latter will overwrite the former:
class User < ApplicationRecord
has_many :forms
# this overwrites the previous line!
has_many :forms, through: :sharedforms
...
end
Instead you need to give each association a unique name:
class User < ApplicationRecord
has_many :forms
has_many :shared_forms
has_many :forms_shared_with_me, through: :shared_forms
end
Note that the through option for has_many should point to an association on the model!
This would let you use:
class FormsController < ApplicationController
def index
#forms = current_user.forms
#shared = current_user.forms_shared_with_me
end
end

Rails - relation between activeadmin model and custom model

First I would like to know if there is any possibilities to associate one of my model with the ActiveAdmin::Comment and the AdminUser models
this is my model
class AdminAction < ActiveRecord::Base
has_one :comment, :class_name => "ActiveAdmin::Comment", :foreign_key => "admin_action_id"
belongs_to :admin_user
end
thoses associations don't raise any errors, just returning `nil``
I have added a field in thoses two models :
add_column :admin_users, :admin_action_id, :integer
add_column :active_admin_comments, :admin_action_id, :integer
The goal here is to fetch the AdminUser and the Comment associate to my new model AdminAction
and when I do
a = AdminAction
a.admin_user
# and
a.comment
it works
any ideas ?
You need to have a admin_user_id in the admin_actions table to make this belongs_to association work.
class AdminAction < ActiveRecord::Base
belongs_to :admin_user
end
Also, the foreign_key param is unneeded because it will be inferred from the AdminAction class name.
class AdminAction < ActiveRecord::Base
has_one :comment, :class_name => "ActiveAdmin::Comment", :foreign_key => "admin_action_id"
end
Other than that, what you have should work as expected. If it is not, please provide more detail as to what you are seeing, or not seeing as the case may be.
I have this working, albiet with a User model rather than AdminUser. Here is my code:
Migrations
class CreateAdminAction < ActiveRecord::Migration
def change
create_table :admin_actions do |t|
t.references :user, index: true
t.timestamps
end
end
end
class AddFieldsForAdminAction < ActiveRecord::Migration
def change
add_column :active_admin_comments, :admin_action_id, :integer
end
end
AdminAction class
class AdminAction < ActiveRecord::Base
has_one :comment, class_name: 'ActiveAdmin::Comment'
belongs_to :user
end
Another thought: if you are looking to get the ActiveAdmin::Comment records for a single AdminUser, I think you can fetch them directly like this:
admin_comments = ActiveAdmin::Comment.find_for_resource_in_namespace(AdminUser.find(some_id), :admin)

Ruby on Rails Active Record Query Interface

I have the following:
models/like.rb:
class Like
belongs_to :post
end
models/post.rb:
class Post
has_many :likes, dependent: :destroy
def self.popular
Like.group(:post_id).count << ???
end
end
I would like to make a scope that returns the most popular posts: posts with more than 20 likes, but I don't know how to make the conditional.
You can use counter_cache to do this. You will have to create an extra column, but it is more performatic when SELECTing.
models/like.rb
class Like < ActiveRecord::Base
belongs_to :post, counter_cache: true
end
models/post.rb
class Post < ActiveRecord::Base
has_many :likes, dependent: :destroy
def self.popular
where('likes_count > 20').order('likes_count DESC')
end
end
Then create the migration:
class AddLikesToPosts < ActiveRecord::Migration
def change
add_column :posts, :likes_count, :integer, default: 0
end
end
And populate likes_count for your current Posts on rails console (only needed if you already have some created posts):
Post.find_each { |post| Post.reset_counters(post.id, :likes) }
After this, each time you create a new Like, the counter will be automatically incremented.

STI: user has multiple child class ids.( I want the user has only one of the child class id)

I'm trying to use STI because I want to use single sign-in page for a device. I want to assign either teacher_id or student_id to a user, but it turned out that all the user has both. How can I fix this problem? Below are the models and the migration.
class User < ActiveRecord::Base
...
DEFAULT_ROLE = 'Student'
after_create :set_role
attr_accessible ..., :role
has_one :role
...
private
def set_role
self.role ||= Role.find_by_name(DEFAULT_ROLE)
end
...
end
class Student < User
has_many :bookings
end
Class Teacher < User
has_many :bookings
end
class Role < ActiveRecord::Base
validates :name, :presence => true
belongs_to :user
end
Class Booking < ActiveRecord::Base
attr_accessible :student_id, :teacher_id
belongs_to :teacher, :class_name => 'Teacher'
belongs_to :student, :class_name => 'Student'
...
class CreateBookings < ActiveRecord::Migration
def change
create_table :bookings do |t|
t.integer :student_id
t.integer :teacher_id
t.date :booking_date
t.time :booking_time
t.timestamps
end
end
end
It looks like you need to separate the "role" part of User into a separate object and then allow users to have multiple roles. Sometimes these are called "profiles" as they really refer to a way of presenting the user.
You can then use the user model as a proxy for accessing these things where you'd test for the presence of the profile:
if (user.teacher)
# ...
else
flash[:notice] = "You must be a teacher to perform this operation."
end

Identifying the relationship with multiple one-to-one relationships between two models in rails

I have the following two models:
class Project < ActiveRecord::Base
has_one :start_date, :class_name => 'KeyDate', :dependent => :destroy
has_one :end_date, :class_name => 'KeyDate', :dependent => :destroy
and
class KeyDate < ActiveRecord::Base
belongs_to :project
Given a certain key date from the database related to a project:
#key_date = KeyDate.find(:first)
is there a way to introspect the relationship to check if the #key_date is related to the project as start_date or as end_date?
A nice way would be to use single table inheritance for the KeyDate class
class KeyDate < ActiveRecord::Base
belongs_to :project
end
class StartDate < KeyDate
end
class EndDate < KeyDate
end
class Project < ActiveRecord::Base
has_one :start_date, :dependent => :destroy
has_one :end_date, :dependent => :destroy
end
class CreateKeyDatesMigration < ActiveRecord::Migration
def up
create_table :key_dates do |t|
t.date :date
t.string :type #this is the magic column that activates single table inheritance
t.references :project
end
end
…
end
this lets you do
#key_date = KeyDate.find(:first)
#key_date.type # => "StartDate"
One clean way to do what you want is to create STI:
http://api.rubyonrails.org/classes/ActiveRecord/Base.html
See one example I gave here:
Rails devise add fields to registration form when having STI
Just thinking aloud...
class KeyDate < ActiveRecord::Base
belongs_to :project
def start_date?
project.start_date == self
end
def end_date?
project.start_date == self
end
date_type
[:start_date, :end_date].find {|sym| send("#{sym}?") }
end
end
To be honest I can't see why you'd ever need this. Surely you're always going to have a handle on a project anyway?

Resources