How to refactor complex method in Rails model with Rspec? - ruby-on-rails

I have the following complex method. I'm trying to find and implement possible improvements. Right now I moved last if statement to Access class.
def add_access(access)
if access.instance_of?(Access)
up = UserAccess.find(:first, :conditions => ['user_id = ? AND access_id = ?', self.id, access.id])
if !up && company
users = company.users.map{|u| u.id unless u.blank?}.compact
num_p = UserAccess.count(:conditions => ['user_id IN (?) AND access_id = ?', users, access.id])
if num_p < access.limit
UserAccess.create(:user => self, :access => access)
else
return "You have exceeded the maximum number of alotted permissions"
end
end
end
end
I would like to add also specs before refactoring. I added first one. How should looks like others?
describe "#add_permission" do
before do
#permission = create(:permission)
#user = create(:user)
end
it "allow create UserPermission" do
expect {
#user.add_permission(#permission)
}.to change {
UserPermission.count
}.by(1)
end
end

Here is how I would do it.
Make the check on the Access more like an initial assertion, and raise an error if that happens.
Make a new method to check for an existing user access - that seems reusable, and more readable.
Then, the company limit is more like a validation to me, move this to the UserAccess class as a custom validation.
class User
has_many :accesses, :class_name=>'UserAccess'
def add_access(access)
raise "Can only add a Access: #{access.inspect}" unless access.instance_of?(Access)
if has_access?(access)
logger.debug("User #{self.inspect} already has the access #{access}")
return false
end
accesses.create(:access => access)
end
def has_access?(access)
accesses.find(:first, :conditions => {:access_id=> access.id})
end
end
class UserAccess
validate :below_company_limit
def below_company_limit
return true unless company
company_user_ids = company.users.map{|u| u.id unless u.blank?}.compact
access_count = UserAccess.count(:conditions => ['user_id IN (?) AND access_id = ?', company_user_ids, access.id])
access_count < access.limit
end
end

Do you have unit and or integration tests for this class?
I would write some first before refactoring.
Assuming you have tests, the first goal might be shortening the length of this method.
Here are some improvements to make:
Move the UserAccess.find call to the UserAccess model and make it a named scope.
Likewise, move the count method as well.
Retest after each change and keep extracting until it's clean. Everyone has a different opinion of clean, but you know it when you see it.

Other thought, not related to moving the code but still cleaner :
users = company.users.map{|u| u.id unless u.blank?}.compact
num_p = UserAccess.count(:conditions => ['user_id IN (?) AND access_id = ?', users, access.id])
Can become :
num_p = UserAccess.where(user_id: company.users, access_id: access.id).count

Related

How to add exception handling to my before_action

I have a before_action method like this:
def current_user
#current_user ||= User.find(:id => session[:id])
end
And I call a method like this:
def get_food user
food = Food.find(:id => user.id)
end
This is fine, but I want to add exception handling.
When the user is nil I want to use #current_user:
def get_food user
food = Food.find(if user is nil i want to use #current_user.id)
end
Of course, I can write it like this:
def get_food user
if user.nil?
food = Food.find(#current_user.id)
else
food = Food.find(user.id)
end
Or, is this the best way?
def get_food user
food = Food.find(user == nil? #current_user.id : user.id)
end
I'm curious is there a better way than adding a simple if statement inside the param?
The shortest one lines I can think of are something like this:
Food.find((user || current_user).id)
Food.find(user.try(:id) || current_user.id)
Food.find(user ? user.id : current_user.id)
Not sure if this is really an impovement in readability. I would prefer something like this:
def get_food(user)
user ||= current_user
Food.find(user.id)
end
You can use ternary operator to make it one line:
user ? Food.find(user.id) : Food.find(#current_user.id)
How about arrays
food = Food.where(id: [#current_user.try(:id),user.id]).first
You can try this:
food = Food.find(user.nil? ? #current_user.id : user.id)
What about default parameters?
def get_food(user = #current_user)
food = Food.find(user.id)
end
It will work if you call it without the parameter
something.get_food # notice the method is called with no params
If you want it working also if you pass nil, you should also add:
def get_food(user = #current_user)
food = Food.find((user || #current_user).id)
end
However is strange that foods and users have the same ids...
Maybe the correct query is:
food = Food.find_by_user_id((user || #current_user).id)
or, if users have more than just one food:
foods = Food.where(user: (user || #current_user)) # rails 4, :user => (user || #current_user) for rails 3
Food.find(user.id rescue #current_user.id)

selecting certain users in rails

I'm writing a zombie survival app, and I'm trying to select all my users marked "alive" where :alive is a boolean.
I was writing a private method in my users controller but can't get the ruby right, does anyone have a pointer?
def get_alive
#holder = (User.map {|user| user})
#user = #holder.each {|i| if i.alive #user << i}
end
thanks
Use a scope to find all alive users.
class User < ActiveRecord::Base
scope :alive, where(:alive => true)
# ... the rest of your model ...
end
Then you can do this:
#alive_users = User.alive
You could just select those users directly if User is active record:
User.where(:alive => true)
Or filter for just those users:
User.all.filter(&:alive)
you need to give a bit more details what "holder" is supposed to be... and why you are comparing against 'i'
otherwise:
User.where(:alive => true)
it's a good idea to wrap this in a scope as in Sean Hill's answer
You can even use this syntax in where query
User.where(alive: true)
Or use select over array of object. But select is slow
User.all.select{ |user| user.alive == true}

Rails 3 multiple parameter filtering using scopes

Trying to do a basic filter in rails 3 using the url params. I'd like to have a white list of params that can be filtered by, and return all the items that match. I've set up some scopes (with many more to come):
# in the model:
scope :budget_min, lambda {|min| where("budget > ?", min)}
scope :budget_max, lambda {|max| where("budget < ?", max)}
...but what's the best way to use some, none, or all of these scopes based on the present params[]? I've gotten this far, but it doesn't extend to multiple options. Looking for a sort of "chain if present" type operation.
#jobs = Job.all
#jobs = Job.budget_min(params[:budget_min]) if params[:budget_min]
I think you are close. Something like this won't extend to multiple options?
query = Job.scoped
query = query.budget_min(params[:budget_min]) if params[:budget_min]
query = query.budget_max(params[:budget_max]) if params[:budget_max]
#jobs = query.all
Generally, I'd prefer hand-made solutions but, for this kind of problem, a code base could become a mess very quickly. So I would go for a gem like meta_search.
One way would be to put your conditionals into the scopes:
scope :budget_max, lambda { |max| where("budget < ?", max) unless max.nil? }
That would still become rather cumbersome since you'd end up with:
Job.budget_min(params[:budget_min]).budget_max(params[:budget_max]) ...
A slightly different approach would be using something like the following inside your model (based on code from here:
class << self
def search(q)
whitelisted_params = {
:budget_max => "budget > ?",
:budget_min => "budget < ?"
}
whitelisted_params.keys.inject(scoped) do |combined_scope, param|
if q[param].nil?
combined_scope
else
combined_scope.where(whitelisted_params[param], q[param])
end
end
end
end
You can then use that method as follows and it should use the whitelisted filters if they're present in params:
MyModel.search(params)

Rails 3 displaying tasks from partials

My Tasks belongs to different models but are always assigned to a company and/or a user. I am trying to narrow what gets displayed by grouping them by there due_at date without doing to many queries.
Have a application helper
def current_tasks
if user_signed_in? && !current_company.blank?
#tasks = Task.where("assigned_company = ? OR assigned_to = ?", current_company, current_user)
#current_tasks = #tasks
else
#current_tasks = nil
end
end
Then in my Main view I have
<%= render :partial => "common/tasks_show", :locals => { :tasks => current_tasks }%>
My problem is that in my task class I have what you see below. I have the same as a scope just named due_today. when I try current_tasks.due_today it works if I try current_tasks.select_due_today I get a undefined method "select_due_tomorrow" for #<ActiveRecord::Relation:0x66a7ee8>
def select_due_today
self.to_a.select{|task|task.due_at < Time.now.midnight || !task.due_at.blank?}
end
If you want to call current_tasks.select_due_today then it'll have to be a class method, something like this (translating your Ruby into SQL):
def self.select_due_today
select( 'due_at < ? OR due_at IS NOT NULL', Time.now.midnight )
end
Or, you could have pretty much the same thing as a scope - but put it in a lambda so that Time.now.midnight is called when you call the scope, not when you define it.
[edited to switch IS NULL to IS NOT NULL - this mirrors the Ruby in the question, but makes no sense because it will negate the left of the ORs meaning]

How do I pass a var from one model's method to another?

Here is my one model..
CardSignup.rb
def credit_status_on_create
Organization.find(self.organization_id).update_credits
end
And here's my other model. As you can see what I wrote here is an incorrect way to pass the var
def update_credits
#organization = Organization.find(params[:id])
credit_count = #organization.card_signups.select { |c| c.credit_status == true}.count
end
If it can't be done by (params[:id]), what can it be done by?
Thanks!
Ideally the data accessible to the controller should be passed as parameter to model methods. So I advise you to see if it is possible to rewrite your code. But here are two possible solutions to your problem. I prefer the later approach as it is generic.
Approach 1: Declare a virtual attribute
class CardSignup
attr_accessor call_context
def call_context
#call_context || {}
end
end
In your controller code:
def create
cs = CardSignup.new(...)
cs.call_context = params
if cs.save
# success
else
# error
end
end
In your CardSignup model:
def credit_status_on_create
Organization.find(self.organization_id).update_credits(call_context)
end
Update the Organization model. Note the change to your count logic.
def update_credits
#organization = Organization.find(call_context[:id])
credit_count = #organization.card_signups.count(:conditions =>
{:credit_status => true})
end
Approach 2: Declare a thread local variable accessible to all models
Your controller code:
def create
Thread.local[:call_context] = params
cs = CardSignup.new(...)
if cs.save
# success
else
# error
end
end
Update the Organization model. Note the change to your count logic.
def update_credits
#organization = Organization.find((Thread.local[:call_context] ||{})[:id])
credit_count = #organization.card_signups.count(:conditions =>
{:credit_status => true})
end
Use an attr_accessor.
E.g.,
class << self
#myvar = "something for all instances of model"
attr_accessor :myvar
end
#myothervar = "something for initialized instances"
attr_accessor :myothervar
then you can access them as ModelName.myvar and ModelName.new.myvar respectively.
You don't say whether you're using Rails 2 or 3 but let's assume Rails 2 for this purpose (Rails 3 provides the a new DSL for constructing queries).
You could consider creating a named scope for in your Organization model as follows:
named_scope :update_credits,
lambda { |id| { :include => :card_signup, :conditions => [ "id = ? AND card_signups.credit_status = TRUE", id ] } }
And then use it as follows:
def credit_status_on_create
Organization.update_credits(self.organization_id)
end
Admittedly I don't quite understand the role of the counter in your logic but I'm sure you could craft that back into this suggestion if you adopt it.

Resources