Devise controller for models with polymorphic relationships - ruby-on-rails

tl;dr:
the Devise::RegistrationsController doesn't instantiate associated polymorphic models, what's the best way to solve that?
setup:
I have an address model which I am associating with different entities (e.g. a person, a business) and thus am using a polymorphic relationship:
class Person < ActiveRecord::Base
has_one :address, :as => :addressable
accepts_nested_attributes_for :address # to make fields_for work
end
class Address < ActiveRecord::Base
belongs_to :addressable, :polymorphic => true
end
I have used the Person model as the Devise model but run into issues where the Address object is not instantiated or saved when signing up for a new account.
The Person controller, which to my knowledge is replaced by the Devise RegistrationController (routing: new_person_registration GET /person/sign_up(.:format) devise/registrations#new) had code to instantiate the respective objects which used to do the trick before I started using Devise:
class PeopleController < ApplicationController
[...]
def create
#person = Person.new(params[:person])
#person.address = Address.new(params[:address])
[...some checking...]
#person.save
#person.address.save
end
def new
#person = Person.new
#person.address = Address.new
end
end
Now it seems that the Devise::RegistrationsController doesn't create the Address object.
One suggested resolution was to use callbacks to initialize the objects, thus my person model has:
before_create :instantiate_all
before_save :save_all
def save_all
self.address.save
self.save
end
def instantiate_all
if self.address.nil?
self.address = Address.new
end
end
With that I still get
NoMethodError (undefined method `address' for nil:NilClass):
app/models/person.rb:25:in `save_all'
I was looking into overriding the RegistrationController but it seems that by calling super I will get the whole block of the respective parent method executed and thus won't be able to insert my desired action.
Lastly, overriding the controller, copying its code and adding my code just seems too wrong to be the only way to get this done.
I'd appreciate your guys' thoughts on that.

Related

Run model method after user creates a object in controller

I'm looking for help with a problem I'm facing. I have a referral program and i would like to run a method when the user who was referred creates a Post. In my user.rb file I have:
def complete_referral!
update(referral_completed_at: Time.zone.now)
end
And my PostsController has:
def create
#post = current_user.posts.build(post_params)
#post.save
end
Some ideas I've tried was trying to run the complete_referral! method on post.rb like so:
after_create :complete_referral!
but that just raised an undefined method complete_referral!' for #<Post:0x00007ff65b0514d8> error.
Any ideas would be appreciated.
You could also use an association callback:
class Post < ApplicationRecord
belongs_to :referrer, class_name: 'User', optional: true
end
class User < ApplicationRecord
has_many :referred_posts,
class_name: 'Post',
foreign_key: 'referrer_id',
after_add: :complete_referral!
end
The callback is then called when you create a post from the association:
#post = User.find(some_param).referred_posts.new(post_params)
The reason I would use this instead of a regular callback is that it does not create a hard dependency from Post to User that you have to address in all your tests when you just want to create a post record.
You need to define the callback in the Post class, e.g.
class Post < ApplicationRecord
after_create :complete_referral!
# ...
private
def complete_referral!
user.complete_referral!
end
end
The other answers use callbacks, and I love callbacks in my models, if the callbacks are necessary to keep my data in a correct state. In this case I would prefer to do this in the controller, because only if a post is saved through the controller (by a user), do we want to call complete_referral! on the user, not when we create a post in the tests or in the console (or on import etc.etc.)
So in that case in the PostsController we write
def create
#post = current_user.posts.build(post_params)
if #post.save
current_user.complete_referral!
end
end

How can I create all has_one relationships automatically?

I have the following models:
class Post < ApplicationRecord
has_one: :basic_metric
has_one: :complex_metric
end
class BasicMetric < ApplicationRecord
belongs_to :post
end
class ComplexMetric < ApplicationRecord
belongs_to :post
end
Once a post is created, both basic_metric and complex_metric are nil:
p = Post.first
p.basic_metric # => nil
p.complex_metric # => nil
And because of how my app is going to work, the BasicMetricsController and ComplexMetricsController only have the update method. So I would like to know if there is a way to create them as soon as a post is created.
One very common way of accomplishing this is using ActiveRecord callbacks
class Post
after_create :create_metrics
private
def create_metrics
# methods created by has_one, suggested in the comments
create_basic_metric(additional_attrs)
create_complex_metric(additional_attrs)
end
end
Another option you have is to overwrite the method created by has_one, i.e.:
class Post
has_one: :basic_metric
has_one: :complex_metric
def basic_metric
super || create_basic_metric(additional_attrs)
end
def complex_metric
super || create_complex_metric(additional_attrs)
end
end
This way they won't be created automatically with any new post, but created on demand when the method is called.
Can you try this one,
post = Post.first
post.build_basic_metric
post.build_complex_metric
This will help you to build/create the has_one association object if record not saved by default use post.save at the end.
If you need this in modal you can use like this,
class Post
after_create :build_association_object
private
def create_metrics
self.build_basic_metric
self.build_complex_metric
# save # (optional)
end
end

Rails Devise - How to add more data to current_user

Suppose I have a User model
user.rb
class User < ActiveRecord::Base
...
end
with attributes like: name, username, access
access is an enum that tells me if the user is "staff" or "customer"
To get the name and username of the logged in user, I can do:
current_user.name
current_user.username
And suppose I have a Staff model
staff.rb
class Staff < ActiveRecord::Base
belongs_to :user
end
with attributes like: salary, phone_number
And I also have a Customer model
customer.rb
class Customer < ActiveRecord::Base
belongs_to :user
end
with attributes like: address, phone_number
I want to be able to call this on my staff's controller:
current_user.staff.salary
And this on my customer's controller:
current_user.customer.address
WHAT I TRIED SO FAR
I overwrote sessions_controller.rb
def create
super
model_name = current_user.access.capitalize.constantize
spec = model_name.where(user_id: current_user.id).take
session[:spec] = spec
end
So I'm able to access it via session[:spec], but not via current_user. Any ideas?
Well to begin with, your User model should reference the staff or customer, even if they are to stay blank
class User
has_one :staff
has_one :address
Just by doing this, you should be able to use current_user.customer.address. However...
I suggest you add some convenient methods in ApplicationController or a module that you include
def staff_signed_in?
#staff_signed_in ||= (user_signed_in? and current_user.access == :staff)
end
def current_staff
#current_staff ||= (current_user.staff if staff_logged_in?)
end
# same for customer
# Note that I use instance variables so any database queries are executed only once !
Then you can simply call
<% if customer_signed_in? %>
<h2>Logged in as customer</h2>
<p>Address : <%= current_customer.address %>
<% end %>
EDIT : about your concerns concerning database hits
You gave the example of current_user.customer.cart.products
This is indeed quite a nested association. My suggestion above already reduces it by one level (ie current_customer == current_user.customer). Then you have to go through carts to reach products... it isn't so bad in my opinion.
If you need to call that often (current_customercustomer.cart) you can override the current_customer for a given controller and eager load the resources you know you will use use.
def UserShopController < ApplicationController
# Let's assume current_customer is defined in ApplicationController like I showed above
# UserShopController always uses the customer cart, so let's load it right at the beginning
...
private
# Override with eager loading
def current_customer
#current_customer ||= (current_user.customer.includes(:cart) if customer_logged_in?)
end
add has_one :customer to your user.rb
Your user model should be like below to accessing related model.
class User < ActiveRecord::Base
has_one :customer
end

Child not being created from Parent model?

I have a checkbox that if checked allows my child resource called Engineer to be created. I'm trying to create it through my model since that's where I can call the after_save method.
Here is my code:
models/user.rb
class User < ActiveRecord::Base
has_many :armies
has_many :engineers
end
models/army.rb
class Army < ActiveRecord::Base
has_many :engineers
attr_reader :siege
after_save :if_siege
private
def if_siege
if self.siege
Engineer.create!( :user_id => current_user.id, :army_id => self.id )
end
end
end
models/engineer.rb
class Engineer < ActiveRecord::Base
belongs_to :user
belongs_to :army
end
controllers/armies_controller.rb
def new
#army = Army.new
end
def create
#army = current_user.armies.build(params[:army])
if #army.save
redirect_to new_army_path
else
render :new
end
end
end
This gives me an error though for my if_siege method:
undefined local variable or method `current_user'
How can I fix this or is there another way to do this? Not sure if this should go in the controller or model but I only can wrap my head around putting this in the model.
Thanks.
Add belongs_to :user to the Army model
In Army#if_siege, update Engineer.create! as follows
Engineer.create!( :user_id => self.user.id, :army_id => self.id )
First, the current_user object won't exist within the context of the Model layer unless your authentication is doing something to make it available. This is usually a non Threadsafe approach though. Maybe for you this isn't the issue.
Current User Instantiation
Having said that, one way (perhaps not the ideal way) to address this is by creating an attr_accessor in the model on the object called Army. Then set the current_user to this in the Army new action in the controller where the current_user instance is available.
# in the Army model
attr_accessor :the_user
# in the Army Controller
#army = Army.new(:the_user => current_user.id)
You will also have to add a hidden field to store this value in your view to carry this through to the create action.
Just an observation, but I'm fairly sure in the "if_seige" method the self calls are redundant. self should already be scoped to the Army object in that method.

Rails, using build to create a nested object, but it is not being saved

My app has three models:
Thread (has_many :thread_apps)
ThreadApp (belongs_to :thread, has_many :forms, :as => :appable)
Form (belongs_to :app)
ThreadApp Fields: thread_id, form_id, appable_id, appable_type
What I want to be able to do is when creating a form, ensure that a ThreadApp record is also created to make the association:
Here is what I have:
class FormsController < ApplicationController
def create
#thread = Thread.find(params[:thread_id])
#thread_app = #thread.thread_apps.new
#form = #thread_app.forms.build(params[:form].merge(:user_id => current_user.id))
#form.save
....
This is saving the form nicely, but the thread_app associated is not being made? Any ideas why?
Thank you
callings model.save does not save associations unless you tell it to
you can set autosave
class Form < ActiveRecord::Base
belongs_to :thread_app , :autosave => true
end
or call save on it in the controller
#thread_app.save
or you can take it out of the controller entirely
and do this with a callback
class Form < ActiveRecord::Base
before_create :create_thread_app
def create_thread_app
self.thread_app ||= ThreadApp.create(...)
end
end
or after_create, _before_validation_on_create, or any other call back would work
--UPDATE--
this might make a difference using create inse=tead of new, and appables that you specified as 'as'
class FormsController < ApplicationController
def create
#thread = Thread.find(params[:thread_id])
#thread_app = #thread.thread_apps.create
#form = #thread_app.appables.build(params[:form].merge(:user_id => current_user.id))
#form.save
....
Instead of:
#thread_app = #thread.thread_apps.new
You should have:
#thread_app = #thread.thread_apps.create

Resources