Run model method after user creates a object in controller - ruby-on-rails

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

Related

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

Ruby on Rails - nested associations - creating new records

I'm learning Rails (4.2 installed) and working on a social network simulation application.
I have setup an one to many relation between Users and Posts and now I'm trying to add also Comments to posts. After multiple tries and following the documentation on rubyonrails.org I ended up with the following setup:
User Model
has_many :posts, dependent: :destroy
has_many :comments, through: :posts
Post Model
belongs_to :user
has_many :comments
Comment Model
belongs_to :user
The comment is initiated from the Post show page, so the
Post Controller has:
def show
#comment = Comment.new
end
Now the question is: in Comments Controller , what is the correct way to create a new record.
I tried the below and many others, but without success.
def create
#comment = current_user.posts.comment.new(comment_params)
#comment.save
redirect_to users_path
end
(current_user is from Devise)
Also, afterwards, how can I select the post corresponding to a comment?
Thank you
You'll want to create a relation on Post, letting each Comment know "which Post it relates to." In your case, you'll likely want to create a foreign key on Comment for post_id, then each Comment will belong_to a specific Post. So, you'd add belongs_to :post on your Comment model.
So, your models become:
class User < ActiveRecord::Base
has_many :posts, dependent: :destroy
has_many :comments, through: :posts
end
class Post < ActiveRecord::Base
belongs_to :user
has_many :comments
end
class Comments < ActiveRecord::Base
belongs_to :user
belongs_to :post
end
Then, to create a Comment, you would want to do one of two things in your controller:
Load the Post corresponding to the Comment you are creating via the URI as a parameter.
Pass the Post ID along in the form in which calls the create method on Comment.
I personally prefer loading the Post from a parameter in the URI, as you'll have less checking to do as far as authorization for can the Comment in question be added to the Post - e.g. think of someone hacking the form and changing the ID for the Post that the form originally sets.
Then, your create method in the CommentsController would look like this:
def create
#post = Post.find(post_id_param)
# You'll want authorization logic here for
# "can the current_user create a Comment
# for this Post", and perhaps "is there a current_user?"
#comment = #post.comments.build(comment_params)
if #comment.save
redirect_to posts_path(#post)
else
# Display errors and return to the comment
#errors = #comment.errors
render :new
end
end
private
def post_id_param
params.require(:post_id)
end
def comment_params
params.require(:comment).permit(...) # permit acceptable Comment params here
end
Change your Comment model into:
class Comment < ActiveRecord::Base
belongs_to :post
belongs_to :user, through: :post
end
Pass the post_id from view template:
<%= hidden_field_tag 'post_id', #post.id %>
Then change your create action:
def create
#post = Post.find(params[:post_id])
#comment = #post.comments.build(comment_params)
#comment.user = current_user
redirect_to post_path(#post)
end

Devise controller for models with polymorphic relationships

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.

accessing associations within before_add callback in Rails 3

In Rails 3.2 I have been looking for a way to traverse the associations of an object within the before_add callback.
So basically my use case is:
class User < ActiveRecord::Base
has_and_belongs_to_many :meetings
end
class Meeting < ActiveRecord::Base
has_and_belongs_to_many :users
has_many :comments, :before_add => :set_owner_id
end
class Comment < ActiveRecord::Base
belongs_to :meeting
end
def set_owner_id(child)
child.owner_id = <<<THE USER ID for #user >>>
end
and I am creating a comment within the context of a user:
#user.meetings.first.comments.create
How do I traverse the associations from within the before_add callback to discover the id of #user? I want to set this at model level. I have been looking at proxy_association, but I may be missing something. Any ideas?
You should probably create the comment in the context of the meeting, no? Either way, you should handle this in the controller since you'll have no access to #user in your model.
#comment = Meeting.find(id).comments.create(owner_id: #user, ... )
But if you insist on your way, do this:
#comment = #user.meetings.first.comments.create(owner_id: #user.id)

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.

Resources