I have a setup where User A rejects User B's offer. This triggers ActionCable to push a form created using button_to to User B informing him of this, and let's him click the message to delete it. But the form raises ActionController::InvalidAuthenticityToken
params {"_method"=>"delete", "authenticity_token"=>"4lqu8...", "id"=>"31"}
I do have another form which I replace a create form with an update form using ActionCable and it works fine. So, my attempt was to change the flow to when User A rejects the offer it triggers User B to request the message. And for that I moved the logic from a service object to
class AuctionOwnChannel < ApplicationCable::Channel
def subscribed
#...
end
def display(hash)
ActionCable.server.broadcast "auction_own_channel_#{params[:oPId]}_#{current_user.id}", hash
end
# In the code below `rack_inputs` is passed into `frustrated_replacement` and the subsequent methods to reach `render`
def rejected(data)
info = data["info"]
display BidServices::Process.frustrated_replacement(info["product_id"], info["message_id"], current_user)
end
end
module BidServices
class Process
class << self
def frustrated_replacement(product_id, message_id, user)
message = Message.find message_id
thing = {partial: "messages/rejected", locals: {message: message}}
replacements(thing, user)
end
def replacements(thing, user)
{ processed_bid: render(thing),
cart: render(partial: "bids/cart", locals: {current_user: user} )
}
end
def render(*args)
a_c = ApplicationController
a_c.per_form_csrf_tokens = true # Have tried with and without this
a_c.renderer.render(*args)
end
end
end
end
EDIT: Since writing this I did further research and went through some of the code for ActionView and I think I need to set the session
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user, :rack_inputs
def connect
self.rack_inputs = find_rack_inputs
self.current_user = find_verified_user
logger.add_tags 'ActionCable', current_user.id
end
protected
def find_rack_inputs
# I just tried adding `session` in the hash but it did not help
{input: env["rack.input"], http_host: env['HTTP_HOST'], session: env["rack.session"]}
end
# Attempt to replace whole env
# def find_rack_inputs
# env
# end
end
end
module BidServices
class Process
def render(*args)
rack_inputs = args.pop
controller = MessagesController
renderer = controller.renderer.new(rack_inputs)
renderer.render(*args)
end
# The changes when I was replacing the env
# def render(*args)
# rack_inputs = args.pop # This attempt rack_inputs was `env`
# MessagesController.renderer.new(rack_inputs).render(*args)
# end
end
end
end
Related
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
I would like to avoid duplicating the setup for multiple mailer previews. What is the best way to clean this up?
class MyMailerPreview < ActionMailer::Preview
def email1
setup
mailer.email1
end
def email2
setup
mailer.email2
end
def email3
setup
mailer.email3
end
end
Here are two possible solutions I found:
There is something called preview_interceptors that are used when generating mailer previews, you could add your own like this:
config/environments/development.rb
config.action_mailer.preview_interceptors = :my_setup
test/mailers/previews/my_setup.rb
class MySetup
def self.previewing_email(message)
message.subject = "New subject"
end
end
test/mailers/previews/user_mailer_preview.rb
class UserMailerPreview < ActionMailer::Preview
include ActionMailer::Previews
register_preview_interceptor :my_setup
def welcome_email
UserMailer.with(user: User.first).welcome_email
end
end
The message parameter is an instance of ActionMailer::Parameterized::MessageDelivery, I am not sure everything you can do with it, but you can set some attributes on the email itself.
I couldn't find much documentation on preview interceptors, but here is a link to how they are used in Rails.
# Previews can also be intercepted in a similar manner as deliveries can be by registering
# a preview interceptor that has a <tt>previewing_email</tt> method:
#
# class CssInlineStyler
# def self.previewing_email(message)
# # inline CSS styles
# end
# end
#
# config.action_mailer.preview_interceptors :css_inline_styler
#
# Note that interceptors need to be registered both with <tt>register_interceptor</tt>
# and <tt>register_preview_interceptor</tt> if they should operate on both sending and
# previewing emails.
I tried to include Rails before_action in the class, but it wouldn't hook the methods in the previewer, so the second option I found is to build your own before_action like this:
module MySetup
def before_action(*names)
UserMailer.instance_methods(false).each do |method|
alias_method "old_#{method}", method
define_method method do
names.each do |name|
send(name)
end
send("old_#{method}")
end
end
end
end
class UserMailerPreview < ActionMailer::Preview
extend MySetup
def welcome_email
UserMailer.with(user: User.first).welcome_email
end
before_action :setup
private
def setup
puts "Setting up"
end
end
Use an initialize method.
Just override the parent initialize method, call super and then run your setup:
class MyMailerPreview < ActionMailer::Preview
def initialize( params = {} )
super( params )
#email_address = "jules#verne.com"
end
def email1
mailer.email1( #email_address )
end
end
You can view the ActionMailer::Preview.new method here as a reference.
Based on my understanding of what you're asking maybe you could add it into one single method that takes the mailer method as a param
class MyMailerPreview < ActionMailer::Preview
def email_for(emailx) # (Pass the method(email1, etc) as an argument where you're calling it
setup
mailer.send(emailx.to_sym) # Call the method param as a method on the mailer
end
end
Would that work for you?
In my Ruby on Rails project, I have a user model that is set up by devise. Each user belongs to an account with an account_id.
In my application_controller.rb I have
def set_account
#account = current_user.account
end
This is working fine as in many places of my project I have before_action: set_account and it does assign #account correctly.
When a user signs in, I want the user to subscribe to message_notifications_channel_#{account_id} where account_id is the id of the account that the user belongs to.
This is how I set up connection.rb:
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
private
def find_verified_user
if verified_user = User.find_by(id: cookies.encrypted[:user_id])
verified_user
else
reject_unauthorized_connection
end
end
end
end
When I entered byebug here, User.find_by(id: cookies.encrypted[:user_id]) returned nil and cookies.encrypted[:user_id] is nil too.
This is the setup for message_notifications_channel.rb:
class MessageNotificationsChannel < ApplicationCable::Channel
def subscribed
current_user.appear
# stream_from "some_channel"
stream_from "message_notifications_channel_#{params[:account_id]}"
end
def unsubscribed
current_user.disappear
# Any cleanup needed when channel is unsubscribed
end
end
For message_notifications.coffee, this is the code:
App.message_notifications = App.cable.subscriptions.create {channel: "MessageNotificationsChannel", account_id: current_user.account_id},
connected: ->
# Called when the subscription is ready for use on the server
disconnected: ->
# Called when the subscription has been terminated by the server
received: (data) ->
# Called when there's incoming data on the websocket for this channel
if data['direction'] == 'incoming'
ding = new Audio('/assets/ding.wav');
ding.play();
$('#conversation-messages').append String(data['message']);
if data['direction'] == 'outgoing'
if $('#message_'+data['id']).length == 0
iphone_sms_sent_sound = new Audio('/assets/iphone_send_sms.mp3');
iphone_sms_sent_sound.play();
$('#conversation-messages').append String(data['message']);
else
$('#message_'+data['id']).replaceWith(data['message']);
I'm using the following to broadcast message in the after_create callback of Message.rb:
ActionCable.server.broadcast "message_notifications_channel_#{self.account_id}", {id: self.id, direction: self.direction, message: ApplicationController.render(partial:'inbox/message', locals: {message: self})}
This would not work and I got "An unauthorized connection attempt was rejected". I tried using App.message_notifications = App.cable.subscriptions.create {channel: "MessageNotificationsChannel", account_id: #account.id}
This would not work either.
Then I did App.message_notifications = App.cable.subscriptions.create {channel: "MessageNotificationsChannel", account_id: 3} and commented out current_user.appear and current_user.disappear in message_notifications_channel.rb and everything inside
module ApplicationCable
end
in connection.rb. Then packets will get broadcast and received and things will show up.
How do I get App.message_notifications = App.cable.subscriptions.create {channel: "MessageNotificationsChannel", account_id: }, to use the id of #account.id or current_user.account_id while keeping the methods for user verification in connection.rb?
Thanks!
So i'm working on a sort of custom-rolled history tracking for a RoR application. The part i'm hung up on is getting the logged in users information to tie to the record. I've figured out getting the user, its by a submodule which is attached to the ActionController::Base class. The problem is, I'm having trouble retrieving it from the submodule.
Here is my code:
module Trackable
# This is the submodule
module TrackableExtension
extend ActiveSupport::Concern
attr_accessor :user
included do
before_filter :get_user
end
def get_user
#user ||= current_user # if I log this, it is indeed a User object
end
end
# Automatically call track changes when
# a model is saved
extend ActiveSupport::Concern
included do
after_update :track_changes
after_destroy :track_destroy
after_create :track_create
has_many :lead_histories, :as => :historical
end
### ---------------------------------------------------------------
### Tracking Methods
def track_changes
self.changes.keys.each do |key|
next if %w(created_at updated_at id).include?(key)
history = LeadHistory.new
history.changed_column_name = key
history.previous_value = self.changes[key][0]
history.new_value = self.changes[key][1]
history.historical_type = self.class.to_s
history.historical_id = self.id
history.task_committed = change_task_committed(history)
history.lead = self.lead
# Here is where are trying to access that user.
# #user is nil, how can I fix that??
history.user = #user
history.save
end
end
In my models then its as simple as:
class Lead < ActiveRecord::Base
include Trackable
# other stuff
end
I got this to work by setting a Trackable module variable.
In my TrackableExtension::get_user method I do the following:
def get_user
::Trackable._user = current_user #current_user is the ActionController::Base method I have implemented
end
Then for the Trackable module I added:
class << self
def _user
#_user
end
def _user=(user)
#_user = user
end
end
Then, in any Trackable method I can do a Trackable::_user and it gets the value properly.
I have 3 tables
items (columns are: name , type)
history(columns are: date, username, item_id)
user(username, password)
When a user say "ABC" logs in and creates a new item, a history record gets created with the following after_create filter.
How to assign this username ‘ABC’ to the username field in history table through this filter.
class Item < ActiveRecord::Base
has_many :histories
after_create :update_history
def update_history
histories.create(:date=>Time.now, username=> ?)
end
end
My login method in session_controller
def login
if request.post?
user=User.authenticate(params[:username])
if user
session[:user_id] =user.id
redirect_to( :action=>'home')
flash[:message] = "Successfully logged in "
else
flash[:notice] = "Incorrect user/password combination"
redirect_to(:action=>"login")
end
end
end
I am not using any authentication plugin. I would appreciate if someone could tell me how to achieve this without using plugin(like userstamp etc.) if possible.
Rails 5
Declare a module
module Current
thread_mattr_accessor :user
end
Assign the current user
class ApplicationController < ActionController::Base
around_action :set_current_user
def set_current_user
Current.user = current_user
yield
ensure
# to address the thread variable leak issues in Puma/Thin webserver
Current.user = nil
end
end
Now you can refer to the current user as Current.user
Documentation about thread_mattr_accessor
Rails 3,4
It is not a common practice to access the current_user within a model. That being said, here is a solution:
class User < ActiveRecord::Base
def self.current
Thread.current[:current_user]
end
def self.current=(usr)
Thread.current[:current_user] = usr
end
end
Set the current_user attribute in a around_filter of ApplicationController.
class ApplicationController < ActionController::Base
around_filter :set_current_user
def set_current_user
User.current = User.find_by_id(session[:user_id])
yield
ensure
# to address the thread variable leak issues in Puma/Thin webserver
User.current = nil
end
end
Set the current_user after successful authentication:
def login
if User.current=User.authenticate(params[:username], params[:password])
session[:user_id] = User.current.id
flash[:message] = "Successfully logged in "
redirect_to( :action=>'home')
else
flash[:notice] = "Incorrect user/password combination"
redirect_to(:action=>"login")
end
end
Finally, refer to the current_user in update_history of Item.
class Item < ActiveRecord::Base
has_many :histories
after_create :update_history
def update_history
histories.create(:date=>Time.now, :username=> User.current.username)
end
end
The Controller should tell the model instance
Working with the database is the model's job. Handling web requests, including knowing the user for the current request, is the controller's job.
Therefore, if a model instance needs to know the current user, a controller should tell it.
def create
#item = Item.new
#item.current_user = current_user # or whatever your controller method is
...
end
This assumes that Item has an attr_accessor for current_user.
The Rails 5.2 approach for having global access to the user and other attributes is CurrentAttributes.
If the user creates an item, shouldn't the item have a belongs_to :user clause? This would allow you in your after_update to do
History.create :username => self.user.username
You could write an around_filter in ApplicationController
around_filter :apply_scope
def apply_scope
Document.where(:user_id => current_user.id).scoping do
yield
end
This can be done easily in few steps by implementing Thread.
Step 1:
class User < ApplicationRecord
def self.current
Thread.current[:user]
end
def self.current=(user)
Thread.current[:user] = user
end
end
Step 2:
class ApplicationController < ActionController::Base
before_filter :set_current_user
def set_current_user
User.current = current_user
end
end
Now you can easily get current user as User.current
The Thread trick isn't threadsafe, ironically.
My solution was to walk the stack backwards looking for a frame that responds to current_user. If none is found it returns nil. Example:
def find_current_user
(1..Kernel.caller.length).each do |n|
RubyVM::DebugInspector.open do |i|
current_user = eval "current_user rescue nil", i.frame_binding(n)
return current_user unless current_user.nil?
end
end
return nil
end
It could be made more robust by confirming the expected return type, and possibly by confirming owner of the frame is a type of controller...