An application defines a pundit user according to its context of shop
def pundit_user
CurrentContext.new(current_user, #shop)
end
In practice, the following policy for Contact class
def initialize(user, contact)
#user = user
#contact = contact
end
def create?
user && (user.admin? || user.editor? || (user.id == contact.user_id))
end
does not work as the user attibutes cannot be accessed.
The following error is returned for the admin attribute of user, nor, upon removing the first two conditions, does not access the user's id.
NoMethodError: undefined method `id' for #<CurrentContext:0x0000000114537220
#user=#<User id: 830861402, email: "shopkeeper#mail.co", name_last: "string", name_first: "string", admin: false, editor: false [...] dconne: nil>,
#shop=#<Shop id: 1039309252, name: "[...]
An attempt to alter the initialization invoking the current context
def initialize(current_context, contact)
#user = user
#shop = shop
#contact = contact
end
fails where #shop is not recognized NameError: undefined local variable or method shop' for #<ContactPolicy:`
How can the #user and #shop values be accessed to establish a working policy?
You have to set up CurrentContext class so you can use it inside the policy classes:
class CurrentContext # <= FIXME: the name is not very descriptive
# maybe `AuthorizationContext`
# NOTE: create reader methods to
# get #user and #shop variables outside of this class.
attr_reader :user, :shop
# NOTE: to make a clear distinction here. `initialize` arguments
# just hold the values that you pass to `new` method.
def initialize(user_argument, shop_argument)
#user = user_argument
#shop = shop_argument
end
end
pundit_user method is what Pundit uses to initialize a policy class:
def pundit_user
CurrentContext.new(current_user, #shop)
end
# authorize(record)
# |
# `->Pundit.authorize(pundit_user, record, query, policy_class: policy_class, cache: policies)
# |
# `->Pundit.policy(pundit_user, record)
# |
# `->ContactPolicy.new(pundit_user, record)
Inside the policy class, just use CurrentContext as any other object:
# #contact or Contact-.
# |
# pundit_user--. |
# v v
def initialize(user, contact)
# NOTE: `user` and `contact` are just local variables
# actual names are irrelevant.
# NOTE: `user` is an instance of `CurrentContext`
#user = user.user # get the user
#shop = user.shop # get the shop
#contact = contact
end
To make it obvious what we should get in the initializer, just rename the argument:
def initialize(user_context, contact)
#user = user_context.user
#shop = user_context.shop
#contact = contact
end
Related
The problem in testing a create action is that the object is not already created. Thus a class policy initialisation could state:
def initialize(user, promotion)
#user = user
#promotion = promotion
end
with #shop = #promotion.shop_id added to the edit/update/delete policy actions.
However, the user may have a role within that shop that allows it to do some privileged actions, such as create ...
def shopmanager
#roleshopuser ||= Roleshopuser.where(['user_id = ? AND shop_id = ? AND role_id IN (?)', user.id, #shop, [1,2]]).first
end
note: this functions properly on edit/update/destroy actions, thus validating the testing fixtures
As the initialisation process indicates, the promotion requires a shop_id and is thus a submitted param to the create action. Therefore the controller action is defined as (with the puts confirming the presence of the parameter):
def create
puts params[:promotion][:shop_id]
#target_shop = params[:promotion][:shop_id]
#promotion = Promotion.new(promotion_params)
authorize #promotion
and the policy adapted accordingly
def create?
#shop = #target_shop
puts user.id
puts user.admin?
puts shopmanager.nil?
!user.nil? && (user.admin? || !shopmanager.nil?)
end
again the puts do indicate that there is proper handling of the attributes and method
The test however fails
test "should create promotion" do
#shop = shops(:three_4)
#shopusers.each do |user|
sign_in user
#shopusers.each do |user|
sign_in user
assert_difference('Promotion.count') do
post promotions_url, params: { promotion: { name: (#promotion.name + user.id.to_s), shop_id: #promotion.shop_id [...]
end
assert_redirected_to promotion_url(Promotion.last)
end
sign_out user
end
end
From the puts data, the policy runs on user.admin?, but the policy fails where true is returned for the next user fixture when running method shopmanager.nil? (admin is false). As the edit/update functions work with the same fixtures, this points to an error in the policy for create.
What is it?
There is a path through this as indicated in documentation.
For the above case, the initialization requires modification to handle additional elements
attr_reader :user, :promotion, :shop
def initialize(context, promotion)
#user = context.user
#shop = context.shop
#promotion = promotion
end
the Class-specific controller needs amending to pass on the context, adjusted for whether the object already exists or not.
class PromotionsController < ApplicationController
include Pundit
private
def pundit_user
if #promotion.blank?
shop = params[:promotion][:shop_id]
else
shop = #promotion.shop_id
end
CurrentContext.new(current_user, shop)
end
Then the policy can check if the user is a manager of the shop.
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
Just set up my first mailer on Rails 4. I have a welcome email sent to new users as soon as they sign up (create) for a new account using devise.
I also have devise set up so that if a current_user is not found, a guest user will be created. Unfortunately, this is interfering with the mailer. Every time a guest account is created, the mailer will send an email to a non-existing email.
I am having trouble figuring out how to exclude the guests from the mailer.
user.rb:
after_create :send_welcome_mail
def send_welcome_mail
UserMailer.welcome_email(self).deliver
end
mailers/user_mailer.rb:
class UserMailer < ActionMailer::Base
default from: "example#gmail.com"
def welcome_email(user)
#user = user
mail(:to => user.email, :subject => "Welcome!")
end
end
application_controller.rb (guest creation):
def current_or_guest_user
if current_user
if session[:guest_user_id] && session[:guest_user_id] != current_user.id
logging_in
guest_user(with_retry = false).try(:destroy)
session[:guest_user_id] = nil
end
current_user
else
guest_user
end
end
# find guest_user object associated with the current session,
# creating one as needed
def guest_user(with_retry = true)
# Cache the value the first time it's gotten.
#cached_guest_user ||= User.find(session[:guest_user_id] ||= create_guest_user.id)
rescue ActiveRecord::RecordNotFound # if session[:guest_user_id] invalid
session[:guest_user_id] = nil
guest_user if with_retry
end
private
# called (once) when the user logs in, insert any code your application needs
# to hand off from guest_user to current_user.
def logging_in
# For example:
# guest_comments = guest_user.comments.all
# guest_comments.each do |comment|
# comment.user_id = current_user.id
# comment.save!
# end
end
def create_guest_user
u = User.create(:name => "guest", :email => "guest_#{Time.now.to_i}#{rand(100)}#example.com")
u.save!(:validate => false)
session[:guest_user_id] = u.id
u
end
I'm sure this is easy, but I am still new to rails and am a bit confused on the best way to go about this. Let me know if you need any other code.
def welcome_email(user)
# The following line is unnecessary. Normally, you do
# something like this when you want to make a variable
# available to a view
##user = user
# You can make the if more explicit by writing
# if user.id == nil, but if will return false in
# Ruby if a value doesn't exist (i.e. is nil)
if user.id
mail(:to => user.email, :subject => "Welcome!")
end
end
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.
Say user submits a form (creates new item in his account).
Before it goes to database - I want to do this:
params[:user] => current_user.id
# make a note of who is the owner of the item
# without people seing this or being able to change it themselves
# and only after that to save it to database
What's the best way to do it?
All I see in controller is this:
def create
#item = Item.new(params[:item])
...
end
And I'm not sure how to change values under params[:item]
(current_user.id is Devise variable)
Tried to do this:
class Item < ActiveRecord::Base
before_save :set_user
protected
def set_user
self.user = current_user.id unless self.user
end
end
And got an error:
undefined local variable or method `current_user'
It should be as simple as:
def create
#item = Item.new(params[:item])
#item.user = current_user
#...
end
You're getting an undefined local variable or method current_user error as current_user is not available in the model context, only controller and views.