How do you call a controller action from a model in rails - ruby-on-rails

I have a rails app that I am trying to use a model file that finds # mentions in a text field and then I want it to notify the #mentioned party, using after_create callback
class Post < ApplicationRecord
after_create :notifiy_users
def notifiy_users
mentioned_users.each do |user|
Notification.create!(recipient: user,
actor: self.user,
action: 'mentioned',
notifiable: self)
end
end
def mentions
#mentions ||= begin
regex = /#([\w]+)/
matches = body.scan(regex).flatten
end
end
def mentioned_users
#mentioned_users ||= User.where(username: mentions)
end
end
on local_env this works and the notification is created and saved, but when I push to production this acts like the after_create is never called and I get no return from the notify_users method.
Any help or recommendations for a better way to handle this would be greatly appreciated.

Calling controller actions from inside a model violates MVC. Model actions should only handle data-related logic, all the actions should stay within the controller.
Instead, you should be calling the callback from within your controller:
class PostController < ApplicationController
after_action :notify_users, only: [:create]
...
def notify_users
#post.mentioned_users.each do |user|
Notification.create!(recipient: user,
actor: #post.user,
action: 'mentioned',
notifiable: self)
end
end
end
And your model will look like this:
class Post < ApplicationRecord
def mentions
#mentions ||= begin
regex = /#([\w]+)/
matches = self.body.scan(regex).flatten
end
end
def mentioned_users
#mentioned_users ||= User.where(username: self.mentions)
end
end

Related

How to access current user in a mailer file before action

I have a before action in a user mailer file, which is supposed to stop mailers sending if a column on user is set to true or false. However current user is currently unavailable. I understand why, but was wondering if there was a way to do this.
I want to avoid adding the check_if_users_can_receive_mailers at the top of each mailer method.
before_action :check_if_users_can_receive_mailers
#methods that send mailers
private
def check_if_users_can_receive_mailers
current_user.send_mailers?
end
You have to make the current user available as a attribute or class variable. The most straight forward method is something like this:
class MailerBase < ActionMailer::Base
before_action :check_if_users_can_receive_mailers
attr_accessor :user
def initialize(user)
#user = user
end
private
def check_if_users_can_receive_mailers
user.send_mailers?
end
end
class SomeMailerClass < MailerBase
end
In Rails only your controller and views are request aware. Mailers and models and other classes in your application are not and they cannot get the current user since they can't access the session nor the method current_user which is a helper method mixed into your controller (and the view context).
If your mailers need to know about the current user the most logical approach is to pass that information into the mailer:
class UserMailer < ApplicationMailer
def intialize(user)
#user = user
end
end
However a mailer should only have one job - to send emails and it shouldn't be questioning if it should do the job or not. Determining if you should send an email to the user should be done elsewhere. You can place this logic in the controller or even better in a service object:
# app/notifiers/user_notifier.rb
class UserNotifier
def initialize(user, event:)
#user = user
#event = event
end
def notify
if #user.wants_email?
spam_user!
end
send_in_app_notification
end
def self.notify(user, event:)
new(user, event:)
end
private
def spam_user!
# ...
end
def send_in_app_notification
# ...
end
end
class ThingsController
def create
#thing = Thing.new
if #thing.save
UserNotifier.notify(current_user, event: :thing_created)
redirect_to #thing
else
render :new
end
end
end

How can I use variable request in a model?

I have a model called Session, and in it I need to save a column of user IP. However, the model can not see the variable "request".
class Session < ActiveRecord::Base
before_create :set_ip
[...]
def set_ip
self.ip = request.remote_ip
end
end
What is the most elegant way, without breaking the MVC pattern, to solve this problem?
Just pass it in from your controller:
class Session < ActiveRecord::Base
def initialize(ip)
#ip = ip
end
end
class SomeController
def new_session
Session.new(request.remote_ip)
Session.save
end
end
I used the initialize method here but you could just pass it into an existing method if that's more convenient for your use case.
In your controller
class YourController < ApplicationController
def your_action
Session.do_something(session_params) # do_something can be .new, .update, etc. depending on your action
end
private
def session_params
p = params.require(:session).permit(
:your_stuff,
...
)
p[:ip] = request.remote_ip
end
end
Then in your model/database, you can just define a column/field ip

How to get Devise's current_user in ActiveRecord callback in Rails?

I'm using Devise and Rails 3.2.16. I want to automatically insert who created a record and who updated a record. So I have something like this in models:
before_create :insert_created_by
before_update :insert_updated_by
private
def insert_created_by
self.created_by_id = current_user.id
end
def insert_updated_by
self.updated_by_id = current_user.id
end
Problem is that I get the error undefined local variable or method 'current_user' because current_user is not visible in a callback. How can I automatically insert who created and updated this record?
If there's an easy way to do it in Rails 4.x I'll make the migration.
Editing #HarsHarl's answer would probably have made more sense since this answer is very much similar.
With the Thread.current[:current_user] approach, you would have to make this call to set the User for every request. You've said that you don't like the idea of setting a variable for every single request that is only used so seldom; you could chose to use skip_before_filter to skip setting the User or instead of placing the before_filter in the ApplicationController set it in the controllers where you need the current_user.
A modular approach would be to move the setting of created_by_id and updated_by_id to a concern and include it in models you need to use.
Auditable module:
# app/models/concerns/auditable.rb
module Auditable
extend ActiveSupport::Concern
included do
# Assigns created_by_id and updated_by_id upon included Class initialization
after_initialize :add_created_by_and_updated_by
# Updates updated_by_id for the current instance
after_save :update_updated_by
end
private
def add_created_by_and_updated_by
self.created_by_id ||= User.current.id if User.current
self.updated_by_id ||= User.current.id if User.current
end
# Updates current instance's updated_by_id if current_user is not nil and is not destroyed.
def update_updated_by
self.updated_by_id = User.current.id if User.current and not destroyed?
end
end
User Model:
#app/models/user.rb
class User < ActiveRecord::Base
...
def self.current=(user)
Thread.current[:current_user] = user
end
def self.current
Thread.current[:current_user]
end
...
end
Application Controller:
#app/controllers/application_controller
class ApplicationController < ActionController::Base
...
before_filter :authenticate_user!, :set_current_user
private
def set_current_user
User.current = current_user
end
end
Example Usage: Include auditable module in one of the models:
# app/models/foo.rb
class Foo < ActiveRecord::Base
include Auditable
...
end
Including Auditable concern in Foo model will assign created_by_id and updated_by_id to Foo's instance upon initialization so you have these attributes to use right after initialization, and they are persisted into the foos table on an after_save callback.
another approach is this
class User
class << self
def current_user=(user)
Thread.current[:current_user] = user
end
def current_user
Thread.current[:current_user]
end
end
end
class ApplicationController
before_filter :set_current_user
def set_current_user
User.current_user = current_user
end
end
current_user is not accessible from within model files in Rails, only controllers, views and helpers. Although , through class variable you can achieve that but this is not good approach so for that you can create two methods inside his model. When create action call from controller then send current user and field name to that model ex:
Contoller code
def create
your code goes here and after save then write
#model_instance.insert_created_by(current_user)
end
and in model write this method
def self.insert_created_by(user)
update_attributes(created_by_id: user.id)
end
same for other methods
just create an attribute accessor in the model and initialize it when your record is being saved in controller as below
# app/models/foo.rb
class Foo < ActiveRecord::Base
attr_accessor :current_user
before_create :insert_created_by
before_update :insert_updated_by
private
def insert_created_by
self.created_by_id = current_user.id
end
def insert_updated_by
self.updated_by_id = current_user.id
end
end
# app/controllers/foos_controller.rb
class FoosController < ApplicationController
def create
#foo = Foo.new(....)
#foo.current_user = current_user
#foo.save
end
end

after_save callback to set the updated_by column to the current_user

I would like to use an after_save callback to set the updated_by column to the current_user. But the current_user isn't available in the model. How should I do this?
You need to handle it in the controller. First execute the save on the model, then if successful update the record field.
Example
class MyController < ActionController::Base
def index
if record.save
record.update_attribute :updated_by, current_user.id
end
end
end
Another alternative (I prefer this one) is to create a custom method in your model that wraps the logic. For example
class Record < ActiveRecord::Base
def save_by(user)
self.updated_by = user.id
self.save
end
end
class MyController < ActionController::Base
def index
...
record.save_by(current_user)
end
end
I have implemented this monkeypatch based on Simone Carletti's advice, as far as I could tell touch only does timestamps, not the users id. Is there anything wrong with this? This is designed to work with a devise current_user.
class ActiveRecord::Base
def save_with_user(user)
self.updated_by_user = user unless user.blank?
save
end
def update_attributes_with_user(attributes, user)
self.updated_by_user = user unless user.blank?
update_attributes(attributes)
end
end
And then the create and update methods call these like so:
#foo.save_with_user(current_user)
#foo.update_attributes_with_user(params[:foo], current_user)

Using the save method together with update_attributes. Is this common?

A user can sign up as an artist. All the user needs to do now, is provide his email.
In Artist controller, def create. Is it normal to have something like:
def create
#artist = current_user
respond_to do |format|
if #artist.update_attributes(params[:user]) # params[:user] contains email
#artist.is_artist = true
#artist.save
....
In my User model, I have:
attr_accessible :email
Which means, I can't simply do #artist.update_attributes(:is_artist => true). I would have to use the save method instead. Is this type of approach common? Or is there a better way?
You can define before_create method in your model:
class User < ActiveRecord::Base
...
before_create :fill_fields
def fill_fields
is_artist = true
end
end
I would do the following:
1st: I wound not set up an ArtistController if you do not have an Artist Model. rather I would add a non-restful method in your UserController, and push the implemention logic into the model ...
# config/routes.rb
resources :users do
member {post 'signup_as_artist'}
end
# UserController
def signup_as_artist
#user = User.find(params[:id])
#user.signup_as_artist
end
# User
def signup_as_artist
self.update_attribute :is_artist, true
end
Good luck

Resources