I have a simple helper in my rails app that sets a css class based on the state of an object, like:
<li class='<%= car_color(car.state) %>'><%= car.model %></li>
in the helper it's basically:
module CarHelper
def car_color(state)
if state == 'in service'
'car car-in-service'
elsif state == 'in garage'
'car car-in-garage'
else
'car'
end
end
end
And it works fine for my usecase. However, how there's a new requirement that a User with a role of customer should not see the color coding that this helper creates.
My first thought was to do a check in the controller to see if the user should be able to see the color coding:
class CarsController < ApplicationController
before_action :is_customer?
# bunch of restful stuff
private
def is_customer?
#customer if current_user.roles.include? 'customer'
end
end
And then in my helper, I can just add a line:
def car_color(color)
return 'car' if #customer
end
This meets the requirements, but it smells to me. Now my helper has a dependency on #customer which is passed simply because it is an instance variable. An alternative would be to explicitly pass in a user.role to car_color or to wrap all of the calls to car_color in conditionals based on the user.role, which seems even worse.
Is there a way to help prepare this code for even more conditionals based on different user roles? My thought is to do something like:
module CarHelper
def car_color(args)
set_color_for_role(args[:user_role]) if args[:user_role]
set_color_for_state(args[:car_state]) if args[:car_state]
end
private
def set_color_for_role(user_role)
# stuff
end
def set_color_for_state(car_state)
# stuff
end
end
I don't use rails helpers very often since I mostly work in angular and am wondering if I'm missing a cleaner OOP approach.
I don't see any issue with checking the current user's roles in the helper method.
You could move the checking behaviour to the user model though which would make things cleaner (you may of course want to generalise this for multiple roles):
class User < ActiveRecord::Base
def is_customer?
self.roles.include?('customer')
end
end
Then in your helper you can just check if current_user.is_customer?
def car_color(state)
if current_user.is_customer?
'car'
else
if state == 'in service'
'car car-in-service'
elsif state == 'in garage'
'car car-in-garage'
else
'car'
end
end
I find it useful sometimes to build up an array of the classes too which is often cleaner (I've thrown in a case too):
def car_color(state)
car_classes = ['car']
unless current_user.is_customer?
car_classes << case state
when 'in service' then 'car-in-service'
when 'in garage' then 'car-in-garage'
end
end
car_classes.join(" ")
end
Use the draper https://github.com/drapergem/draper gem and move this logic to decorator
Related
According to the JSON API specification, we should use a filter query parmeter to filter our records in a controller. What the filter parameter actually is isn't really specified, but since it should be able to contain multiple criteria for searching, the obvious thing to do would be to use a hash.
The problem is, it seems like I'm repeating myself quite often in controller actions for different types of records.
Here's what things look like for just a filter that includes a list of ids (to get multiple specific records).
def index
if params[:filter] and params[:filter][:id]
ids = params[:filter][:id].split(",").map(&:to_i)
videos = Video.find(ids)
else
videos = Video.all
end
render json: videos
end
For nested property checks, I guess I could use fetch or andand but it still doesn't look dry enough and I'm still doing the same thing across different controllers.
Is there a way I could make this look better and not repeat myself that much?
Rather than using concerns to just include the same code in multiple places, this seems like a good use for a service object.
class CollectionFilter
def initialize(filters={})
#filters = filters
end
def results
model_class.find(ids)
end
def ids
return [] unless #filters[:id]
#filters[:id].split(",").map(&:to_i)
end
def model_class
raise NotImplementedError
end
end
You could write a generic CollectionFilter as above, then subclass to add functionality for specific use cases.
class VideoFilter < CollectionFilter
def results
super.where(name: name)
end
def name
#filters[:name]
end
def model_class
Video
end
end
You would use this in your controller as below;
def index
videos = VideoFilter.new(params[:filter]).results
render json: videos
end
Here is my take on this, somewhat adapted from Justin Weiss' method:
# app/models/concerns/filterable.rb
module Filterable
extend ActiveSupport::Concern
class_methods do
def filter(params)
return self.all unless params.key? :filter
params[:filter].inject(self) do |query, (attribute, value)|
query.where(attribute.to_sym => value) if value.present?
end
end
end
end
# app/models/user.rb
class User < ActiveRecord::Base
include Filterable
end
# app/controllers/users_controller.rb
class UsersController < ApplicationController
# GET /users
# GET /users?filter[attribute]=value
def index
#users = User.filter(filter_params)
end
private
# Define which attributes can this model be filtered by
def filter_params
params.permit(filter: :username)
end
end
You would then filter the results by issuing a GET /users?filter[username]=joe. This works with no filters (returns User.all) or filters that have no value (they are simply skipped) also.
The filter is there to comply with JSON-API. By having a model concern you keep your code DRY and only include it in whatever models you want to filter. I've also used strong params to enforce some kind protection against "the scary internet".
Of course you can customize this concern and make it support arrays as values for filters.
you can use Rails Concerns to drying up ...
##================add common in app/models/concerns/common.rb
module Common
extend ActiveSupport::Concern
# included do
##add common scopes /validations
# end
##NOTE:add any instance method outside this module
module ClassMethods
def Find_using_filters (params)
Rails.logger.info "calling class method in concern=======#{params}=="
##Do whatever you want with params now
#you can even use switch case in case there are multiple models
end
end
end
##======================include the concern in model
include Common
##=======================in your controller,call it directly
Image.Find_using_filters params
I have a Rails 3.2 app. It is a publishing app where we kick off several Sidekiq jobs in response to changes in content. I was calling this from the controller but there's now getting to be multiple points of entry and are now duplicating logic in multiple controllers. The proper place for this to be is in a callback in the model. However, accessing current_user is frowned upon in the model but for things like logging changes or app events, it is critical.
So I have two questions (1) Is there something I'm missing regarding the argument about accessing current_user when you want to be logging changes across complex model structures? and (2) Is the proposed solution here an effective one with last update over 2 years ago in terms of thread-safety? I use a three Unicorn processes on Heroku. https://stackoverflow.com/a/2513456/152825
Edit 1
Thinking through this, wondering if I should just do something like this in my application.rb
class ArcCurrentUser
#current_user_id
def self.id
return #current_user_id
end
def self.id=id_val
#current_user_id=id_val
end
end
and then in my current_user method in application_controller, just update ArcCurrentUser.id to #current_user.id? I will only be using it for this logging functionality.
You're correct in that you can't access current_user from a model.
As for the answer you linked, I'm not entirely sure but I think it's not fully thread-safe. From the same question, I like this answer https://stackoverflow.com/a/12713768/4035338 more.
Say we have a controller with this action
...
def update
#my_object = MyModel.find(params[:id])
#my_object.current_user = current_user
#my_object.assign_attributes params[:my_model]
#my_object.save
end
...
and this model
class MyModel < ActiveRecord::Base
attr_accessor :current_user
before_save :log_who_did_it
private
def log_who_did_it
return unless current_user.present?
puts "It was #{current_user}!"
end
end
Or my favourite
...
def update
#my_object = MyModel.find(params[:id])
#my_object.update_and_log_user(params[:my_model], current_user)
end
...
class MyModel < ActiveRecord::Base
...
def update_and_log_user(params, user)
update_attributes(params)
puts "It was #{user}!" if user.present?
end
end
Ok, so my main issue is I have implemented Mailboxer into our project to handle messaging and I am trying to write tests for it. However, I keep stumbling over and over again. I have attempted several different stub/mocks but have made no progress.
We have a conversations_controller.rb that relies on before_filters for setting all the instance variables necessary for doing each action. Then in the controller actions, the instance variables are referenced directly to do any sort of action or to return specific data.
Here is an example of our index action which returns all conversations in the "box" that is specified in the before_filter, of the mailbox also specified in another before_filter:
class ConversationsController < ::ApplicationController
before_filter :get_user_mailbox, only: [:index, :new_message, :show_message, :mark_as_read, :mark_as_unread, :create_message, :reply_message, :update, :destroy_message, :untrash]
before_filter :get_box
def index
if #box.eql? "inbox"
#conversations = #mailbox.inbox
elsif #box.eql? "sentbox"
#conversations = #mailbox.sentbox
else
#conversations = #mailbox.trash
end
end
And before filters:
private
def get_user_mailbox
#user = User.where(:user_name => user.user_name.downcase).where(:email => user.email.downcase).first_or_create
#mailbox = #user.mailbox if #user
end
def get_box
if params[:box].blank? or !["inbox","sentbox","trash"].include?params[:box]
params[:box] = 'inbox'
end
#box = params[:box]
end
So I guess I have 2 questions in one. First, how to I get my tests to generate the correct data #mailbox, #user, and #box that is needed for the index action. Next, how do I pass the fake parameter to set #box to different "inbox/sentbox/trash". I have tried controller.index({box: "inbox"}) but always get "wrong arguments 1 for 0" messages.
I have tried the following in various different ways, but always get nil:class errors which means that my instance variables are definitely not being set properly.
describe "GET 'index' returns correct mailbox box" do
before :each do
#user = User.where(:user_name => 'test').where(:email => 'test#test.com').first_or_create
#mailbox = #user.mailbox
end
it "#index returns inbox when box = 'inbox'" do
mock_model User
User.stub_chain(:where, :where).and_return(#user)
controller.index.should == #mailbox.inbox
end
end
Filters and callbacks are hard to test and debug. Avoid them when possible.
In this case I don't think your before_filter is necessary, thus no need to test it. The better home for the methods is model.
Check my refacoring:
class User < ActiveRecord::Base
delegate :inbox, :sentbox, :trash, to: :mailbox
end
class ConversationsController < ::ApplicationController
def index
#conversations = current_user.send get_box
end
private
def get_box
# your code
end
end
That's all. Should be enough.
You can then test regularly.
First of all, read the oficial documentation for rails testing: using data for testing and passing parameters to controlers is explained there.
To generate data for your tests you can:
fill your test database with some mailbox and users using rails fixtures or use something like factory girl
use mock objects to fake data. I personally use the mocha gem but there are others
I tend to use a combination of both, prefering mock objects when possible and falling back to factory girl when mocking needs too much code.
I am trying my hand on rails (4). I have done some Sinatra earlier.
I have a signup form, in which user can fill out his organization name, address and his own name, password etc. I have two tables - Users and Organizations, those table get populated with Signup data. So, I have two active records model users and organizations. My controllers looks as follows:
class SignupsController < ApplicationController
# GET /signup
def new
# show html form
end
# POST /signup
def create
#signup = Registration.register(signup_params)
respond_to do |format|
if #signup.save
format.html { redirect_to #signup, notice: 'Signup was successfully created.' }
else
format.html { render action: 'new' }
end
end
end
private
# Never trust parameters from the scary internet, only allow the white list through.
def signup_params
params[:signup]
end
end
I do not have any Registration model (table in db). What I am looking for such Registration model (should I call it model?) where I can have something like:
class Registration
def self.register params
o = Organization.new params
u = User.new o.id, params
self.send_welcome_email params[:email]
end
def send_welcome_email email
#send email here
end
end
1) So where should I keep this Registration class?
2) Is this a correct approach for such situation? If not, then what is the best way to do it?
3) Having such class will effect running any unit tests?
4) I see there is file, signup_helper.rb what is the use of that in SignupsController
You can do include ActiveModel::Model in your model, and it will behave as a normal Model. You will be able to do validations, callbacks. Anything other than persisting to a database.
Have a look at this for more info.
class Registration
include ActiveModel::Model
def self.register params
o = Organization.new params
u = User.new o.id, params
self.send_welcome_email params[:email]
end
def send_welcome_email email
#send email here
end
end
To answer your questions:
1) I would move the Registration class to a Signup module (as it relates to the signup use case) in app/models:
# apps/models/signup/registration.rb
module Signup
class Registration
...
end
end
2) I like your approach. It's something I would do. It is not the "Rails way" but in my opinion it is superior.
3) No. Having a PORO (plain old ruby object) is currently fine for any unit tests and you can easily test it.
4) Helpers are an (in my opinion) old and obsolete way to share functionality between views (and controllers). Don't use them unless there is absolutely no other way (maybe some Library demands it ... ). It is alway better to use POROs like you did.
You don't have to include ActiveModel::Model unless you need it's functionality. E.g. validations. You can include individual pieces of its functionality in this case. E.g
include ActiveModel::Validations
I am trying to understand a bit better the capabilities of CanCan when it comes to authorization. Imagine this controller action:
def update
if can? :action, Model or can? :resolve, Model or can? :authorize, AnotherModel
# My Code here
respond_with #model
else
raise CanCan::AccessDenied.new(nil, :update, Model)
end
end
I got to this point while trying to find a solution to the above using authorize!. As far as I can see (also looking at the signature) authorize! only accepts one permission (action) and one subject, with an optional message, like this:
def authorize!(action, subject, *args)
# code
end
Is there a way which I may be overlooking to instruct authorize to check for multiple actions? Putting two authorize one after the other will act as an AND condition between permissions, what I would like is it to work like an OR condition, basically similar to the custom code above (which has the problem of raising the AuthorizationNotPerformed in CanCan, avoidable with skip_authorize_resource which is not something I would really like to do).
You can create an custom action and create as many or-conditions as you like.
can :my_update_action, Project do |project|
can?(:read, ModelX) || can?(:read, ModelY) || ...
end
In the end I added this rather nice solution to the ability class:
def multi_authorize!(*actions, message_hash)
message = nil
if message_hash.kind_of?(Hash) && message_hash.has_key?(:message)
message = message_hash[:message]
end
auth = false
actions.each do |act|
auth = auth || can?(act[0], act[1])
end
if !auth
message ||= unauthorized_message(actions[0][0], actions[0][1])
raise CanCan::AccessDenied.new(message, actions[0][0], actions[0][1])
end
end
Included an helper for the Controllers:
module CanCanAddition
def multi_authorize!(*args)
#_authorized = true
current_ability.multi_authorize!(*args)
end
end
if defined? ActionController::Base
ActionController::Base.class_eval do
include ApplicationHelper::CanCanAddition
end
end
Which I call like this:
def create
multi_authorize! [:create, Model1], [:authorize, Model2], :message => "You are not authorized to perform this action!"
# other code...
end
WARNING: Due to the code in the ability class, you must provide a message or the last pair of authorization will not be passed in the *args. I'll take some time to overcome this but the idea of the solution I think fits nice with.