I use inheritance in my model. An event has different types:
Event < activity
Event < training
Event < game
I want to set session data to every event type like
game.user_id = session[:user_id]
training.user_id = session[:user_id]
activity.user_id = session[:user_id]
I want to avoid writing #game.user_id = session[:user_id] , ..., ... in every create method in the controller of activity, game and training
Someone knows how to approach this best.
Thanks
Perhaps you are looking for a before_filter that resides in your ApplicationController? Then in each controller, you can set the before_filter to run on create actions.
ApplicationController
def set_user_ids
game.user_id = session[:user_id]
training.user_id = session[:user_id]
activity.user_id = session[:user_id]
end
...
end
OneController < ApplicationController
before_filter :set_user_ids, :only => [:create]
...
end
TwoController < ApplicationController
before_filter :set_user_ids, :only => [:create]
...
end
Don't use game.user_id, instead you can do this:
game = current_user.games.build(params[:game])
if game.save
# do something
else
# do something else
end
Repeat for your other controllers too!
The associations guide may be helpful too.
Generally you'll want to use the built-in scoping that Rails provides. Just to flesh out what #Radar already posted:
class ApplicationController < ActionController::Base
before_filter :find_current_user
private
def find_current_user
#current_user = User.find( session[:user_id] )
end
end
class EventsController < ApplicationController
def create
#event = #current_user.events.build( params[:event] )
#event.save!
end
end
This assumes that you have setup the associations in your model:
class User
has_many :events
end
class Event
belongs_to :user
end
This is also a rather handy mechanism if you need to restrict what a user can see or edit:
class EventsController < ApplicationController
def index
#events = #current_user.events # only fetch current users events
end
def update
#event = #current_user.events.find( params[:id] ) # can't update other user's events
#event.update_attributes!( params[:event] )
end
end
Related
I'm working on a rails app where I wrote a personalized route called "all_designs"; with the corresponding method on the controller and the view, before I add pundit to my project it was working fine.
Now I'm having this error:
Pundit::AuthorizationNotPerformedError in DesignsController#all_designs
I understand that I'm missing a policy for this action, but the way I'm trying is not working.
How can I add a policy for this method?
Controller:
class DesignsController < ApplicationController
before_action :set_design, only: [:show,:edit,:update,:destroy]
def index
#designs = policy_scope(Design.where(user: current_user, status: 'activo'))
#user = current_user
end
def all_designs
#designs = Design.where(user: current_user)
#user = current_user
end
...
end
Policy:
class DesignPolicy < ApplicationPolicy
class Scope < Scope
def resolve
scope.all
end
end
def create?
true
end
def show?
true
end
def destroy?
user == record.user
end
def update?
# If the user is the owner of the design
user == record.user
end
def all_designs?
true
end
end
I would consider separate controller and policy for this as what you're doing is really just a nested route (designs belonging to a singleton resource).
scope 'user', module: :users do
resources :designs, only: :index
end
module Users
class DesignsPolicy
class Scope < Scope
def resolve
#user.designs # make sure user has a `has_many :designs` assocation.
end
end
end
def index?
true
end
end
# Represents designs belonging to the current user
module Users
class DesignsController < ApplicationController
# GET /user/designs
def index
#designs = policy_scope
end
end
end
This lets you separate the logic of displaying the the current users designs from /designs which would display everything in a clean way.
Every method on the controller which needs to be authorized, needs to contains an explicit declaration like this:
def all_designs
#designs = Design.where(user: current_user)
#user = current_user
authorize #designs
end
The reason it wasn't working was: I missed the authorize line
I am referring to my own question Rails Nested Resources with Pundit Allowing Index and finally came up with a working solution but is there not any much better solution defining scope.where(?) or scope.select(?) in the property_policy? How to get all the properties that only belongs to one specific deal using the pundit resolve method?
What I finally did :
properties_controller.rb
class PropertiesController < ApplicationController
before_action :set_deal, except: [:index, :all]
before_action :set_property, only: [:show, :edit, :update, :destroy]
def all
#properties = Property.all
authorize #properties
end
def index
#deal = Deal.find(params[:deal_id])
#properties = policy_scope(Deal)
end
def set_deal
#deal = Deal.find(params[:deal_id])
# pundit ######
authorize #deal
###############
end
(...)
end
property_policy.rb
class PropertyPolicy < ApplicationPolicy
class Scope < Scope
def resolve
scope.all if user.admin?
end
def all?
user_is_admin?
end
def user_is_admin?
user.try(:admin?)
end
(...)
end
What I'd like better:
properties_controller.rb
def index
#deal = Deal.find(params[:deal_id])
#properties = policy_scope(Property) # => for # #properties = #deal.properties
authorize #deal
end
and in the property_policy.rb something like
def resolve
# scope.where(???) if user.admin? # only an admin user can see the #deal.properties
# or any other solution using scope
end
As a reminder 1 deal has many properties and 1 property belongs to one specific deal. My routes are nested deals/id/properties except for the full list of properties I have simple "/properties". Thanks a lot for helping.
** UPDATE **
I finally went for
properties_controller.rb
def index
#deal = Deal.find(params[:deal_id])
#properties = policy_scope(#deal.properties)
authorize #properties, :index?
end
and in property_policy.rb
class PropertyPolicy < ApplicationPolicy
class Scope < Scope
def resolve
user.admin? ? scope.all : scope.none
end
end
def index?
user_is_admin?
end
def user_is_admin?
user.try(:admin?)
end
end
Not sure if it is the proper way
What you want to do is pass a scope to the policy - not just a class.
#properties = policy_scope(#deal.policies)
class PropertiesPolicy
class Scope < Scope
def resolve
user.admin? ? scope.all : scope.none
end
end
end
Another problem with your controller is that authorize #deal will call DealsPolicy#index? which is not what you want.
To authorize an index action you want to call authorize with the model class (and not an instance):
def index
authorize Property # calls PropertiesPolicy#index?
#deal = Deal.find(params[:deal_id])
#properties = policy_scope(#deal.properties)
end
In that case you don't have to do anything special in your Scope#resolve method really. Just return scope since you can assume at that point that the user is an admin.
I have controller with action (welcome#index):
class WelcomeController < ApplicationController
def index
#card = current_user.cards.review_before(Date.today).first
#my_test_variable #this variable from another controller
end
end
and I have another controller:
class CardsController < ApplicationController
def review
if #card.check = true
#my_test_variable = 1
else
#my_test_variable = 2
end
redirect_to root_path #redirect to welcome#index
end
end
How can I put #my_test_variable value to action index controller Welcome to use it in view index?
I think what you're asking is how to make them available to the next request. Put them in the session:
class CardsController < ApplicationController
def review
if #card.check = true
session[:my_test_variable] = 1
else
session[:my_test_variable] = 2
end
redirect_to root_path #redirect to welcome#index
end
end
class WelcomeController < ApplicationController
def index
#card = current_user.cards.review_before(Date.today).first
session[:my_test_variable]
end
end
I will not question why you would want to do this but one solution is redirect to root_path with a parameter and then grab it in the other controller:
class CardsController < ApplicationController
def review
if #card.check == true
#my_test_variable = 1
else
#my_test_variable = 2
end
redirect_to root_path(my_test_variable: #my_test_variable)
end
end
class WelcomeController < ApplicationController
def index
#card = current_user.cards.review_before(Date.today).first
#my_test_variable = params[:my_test_variable] # will be a string
end
end
(and btw, you have a typo in the if statement. should be ==, not =)
The easy way would be making the variable global so that it can be assessed from any controller and later making it null.
class ModelController < ApplicationController
def index
$count = 125
end
end
class PaymentController < ApplicationController
def price
puts $count
end
end
The login in the cards controller should probably be in the model, then you don't have to worry about the approach you have taken.
It is highly uncommon to have 2 controller instances involved with processing one request.
I have a controller and every method of it starts with the following code:
#user = UserData.find_by_login(session[:cuser])
if #user == nil
redirect_to(:controller=> 'user_data', :action=> 'login')
return
end
I'm just wondering if it is possible to avoid code duplication in this case ?
Yes, use a before_filter
class YourController < ApplicationController
before_filter :check_user
def check_user
..
end
end
Absolutely.
class MyController < ApplicationController
before_filter :ensure_logged_in
# actions here.
def ensure_logged_in
#user = UserData.find_by_login(session[:cuser])
if #user == nil
redirect_to(:controller=> 'user_data', :action=> 'login')
end
end
end
You shouldn't need to worry about the 'return', as rails will bail out of the filter pipeline once the redirect happens.
To avoid duplication you just need to add before_filter in every controller where you want to check user authentication.
class SomeController < ApplicationController
before_filter :authenticate_user
end
then add your user authentication logic in application controller something like this,
class ApplicationController < ActionController::Base
private
def current_user
#current_user ||= UserData.find_by_login(session[:cuser]) if session[:cuser]
end
helper_method :current_user
def authenticate_user
redirect_to({:controller=> 'user_data', :action=> 'login'}, :alert => "Not authorized") if current_user.nil?
end
end
You can use current_user helper method in every controller to get current user.
Try to use before filter. This should be fine
Can I use an if statement in my controller or is this bad practice?
In both my create and destroy actions for TracksController, I want to do something like this:
if Product
#product = Product.find(params[:product_id])
#track = #product.tracks.create(params[:track])
eslif Release
#Release = Release.find(params[:release_id])
#track = #release.tracks.create(params[:track])
end
Is there a better way to do this?
I'd do it via a before_filter callback:
class TracksController < AC
before_filter :ensure_track, :only => [ :create, :destroy ]
private
def ensure_track
if Product
#product = Product.find(params[:product_id])
#track = #product.tracks.create(params[:track])
elsif Release
#release = Release.find(params[:release_id])
#track = #release.tracks.create(params[:track])
end
end
end
So with this setup it's ensured that you have a #track instance variable in your create and destroy methods, cause ensure_track gets invoked before those two methods.
I'm not sure though, if the logic you're applying makes sense... Why do you want to test if a constant named Product exists and if not if a constant named Release does? Maybe the question should be if either params[:product_id] or params[:release_id] is present!?
But that's a different question :)
UPDATE: See Rails Action Controller Guide for filters.
I'd go further and suggest a more DRY approach to the before_filter:
class TracksController < ApplicationController
before_filter :get_track_parent, only: [ :create, :destroy ]
def create
#track = #parent.tracks.create(params[:track])
...
redirect_to #parent
end
private
def get_track_parent
if params[:product_id].present?
#parent = Product.find(params[:product_id])
elsif params[:release_id].present?
#parent = Release.find(params[:release_id])
end
end
end
I used parent because we were given a context for the model relationships but I assume there's a better term to describe the commonality between release and product wrt tracks.