Controller show action code is flawed, but error is silent - ruby-on-rails

Here is my User controller show action
def show
#public_groups = Group.public
#groups_member = #user.groups_as_member
#groups_as_owner = #user.groups_as_owner
#random_items = []
#assignments = []
unless #groups_member.nil?
until #random_items.count == 5 do
random_groups = #groups_member.sort_by{rand}.slice(0,5)
random_groups.each do |group|
assignments = Assignment.where(:group_id => group.id).limit(5).all
#assignments = Assignment.find_by_group_id(group.id)
y = Post.find_by_id(assignments.rand.post_id)
#random_items << y
end
end
end
end
I think it might be the way I am declaring the instance variable arrays #random_items and #assignments. I have no idea what the problem is though because my development and production servers don't give any compilation errors or anything.
When I comment out the big block of logic starting with the array declarations the site works.

I'd suggest you to perform a refactoring before you can find an error. Some principles before:
Any dataflow is about model layer responsibility
instance variables are use to share objects between ActionPack layers (controller and view)
use object's attributes instead of instance variables to easy to test
use associations and minimize Arel method and just find in controllers
With according to your code, it can be rewritten with:
# User model
def random_items
return unless groups_as_member
random_groups = groups_member.sort_by{rand}.slice(0,5)
random_groups.each do |group|
return if randorm_groups.length > 5
assignments = group.assignments.limit(5)
if y = Post.rand_by_post(assignments)
random_groups << y
end
end
return random_groups
end
# Post model
def self.rand_by_post(assignments)
find_by_id(assignments.rand.post_id)
end
Once you have the logic clear, you can find the bug and cover it with tests.

Related

Rails controller variable mess

I have a controller that I feel has too many instance variables.
The controller is pulling data from various places and it feels really sloppy.
I have watched some Sandi Metz talks, read books, and other research, and I want to have good practice but I just don't know what to do here.
This method is pulling all the data and sending it to my view and I am able to get it to work, I just know this isn't a good way to go about it and I am hoping someone can point me to some code samples, documentation, videos, or help me understand how to implement a better style.
I have searched on SO and Google but I mostly find people saying to send a hash or JSON to the view, and I want to know if that is ideal before I start on that.
The Client, Project, Person, Role controllers and models have really similar code and I am working on refactoring it to be more DRY.
For example the Client, Project, Person, and Role financial controllers have almost the exact same controller index code as this. :(
I would be happy to add more code if that would help!
This is the project_financials_controller#index
It's pretty much taking in the data from the view and pulling a bunch of data from the database and sending it to a view. I'm currently using only the index method because it was only supposed to be a 'view' but now we can add filters such as time, different clients, etc so I think I need to break it out somehow.
I do have a financial_reports_nav model that this is calling that I could maybe use more, Or even make a financial_reports_controller that pulls the data from the appropriate model and I wont even need the 4 different controllers...
I am totally open to any input/criticism!
def index
# CPPR = Client, Project, Person, Role
#financial_type = 'project'
#financial_params = params
# This pulls the timeframe from the view and figures out the dates requested. (eg. "Last Week")
#timeframe = Financial.time_frame(#financial_params[:timeframe], current_company.timezone, params[:start_date], params[:end_date])
# This grabs all the data required to recall this financial report view at a later time
#financial_nav = FinancialReportNav.set_financial_type(#current_user.id,#financial_type, #start_date, #end_date)
# Grab all active and inactive people for client
#people = Person.active.all
#deleted_people = Person.inactive.all
# This sends over all the info needed to generate the financial reports
#project_financial_populate = Financial.new(#financial_params, #financial_type).populate_project_financials(current_company.default_hourly_cost, current_company.billing_rate, #timeframe[:start_date],#timeframe[:end_date])
# This just pulls all the data from the database that the #project_financial_populate just populated (Can't we just use that??)
#financial_rows = ProjectFinancial.all.map { |p| [ p.project_id, p.billable_hours, p.revenue,p.real_rate, p.hourly_expense, p.labor_expense_total, p.salary_expense, p.gross_profit, p.profit_margin, p.missing_hourly_expense, p.missing_billable_rate ] }
# Using the same view for CPPR's
# Clients has an items count, so we just stuff everything into the first array slot
#items = [1]
# If these are not null then they show an option to change the financial filter type.
#filter_by_client = Client.find_by('id = ?', #financial_params[:filter_by_client])
#filter_by_project = Project.find_by('id = ?', #financial_params[:filter_by_project])
#filter_by_person = Person.find_by('id = ?', #financial_params[:filter_by_person])
#filter_by_role = PersonRole.find_by('id = ?', #financial_params[:filter_by_role])
# This pulls a list of CPPR's that have tracked time in the requested timeframe
#project_list = Financial.project_list(#timeframe[:start_date], #timeframe[:end_date])
#client_list = Financial.client_list(#timeframe[:start_date], #timeframe[:end_date])
#people_list = Financial.people_list(#timeframe[:start_date], #timeframe[:end_date])
end
I always tend to refactor code to be DRY whenever I noticed I have at least 3 instances of duplicate code, but I needed to future-proof the new code to be flexible enough for possible future changes; all of this considered however time permits.
Given your already current code and having told my preferences, this is what I would do:
Model Inheritance
Controller Inheritance
Shared template
Routes
config/routes.rb
resources :client_financial
resources :project_financial
resources :person_financial
resources :role_financial
Models
app/models/financial_record.rb
class FinancialRecord < ActiveRecord::Base # or ApplicationRecord if > Rails 5
self.abstract_class = true
# your shared "financials" model logic here
end
app/models/client_financial.rb
class ClientFinancial < FinancialRecord
# override "financials" methods here if necessary
# or, add new model specific methods / implementation
end
app/models/project_financial.rb
class ProjectFinancial < FinancialRecord
# override "financials" methods here if necessary
# or, add new model specific methods / implementation
end
app/models/person_financial.rb
class PersonFinancial < FinancialRecord
# override "financials" methods here if necessary
# or, add new model specific methods / implementation
end
app/models/role_financial.rb
class RoleFinancial < FinancialRecord
# override "financials" methods here if necessary
# or, add new model specific methods / implementation
end
Controllers
app/controllers/financial_controller.rb
class FinancialController < ApplicationController
before_action :set_instance_variables, only: :index
protected
def set_instance_variables
# strips the last "Controller" substring and change to underscore: i.e. ProjectFinancialsController becomes project_financials
#financial_type = controller_name[0..(-'Controller'.length - 1)].underscore
# get the corresponding Model class
model = #financial_type.camelcase.constantize
# get the correspond Financial Model class
financial_model = "#{#financial_type.camelcase}Financial".constantize
#financial_params = params
#timeframe = Financial.time_frame(#financial_params[:timeframe], current_company.timezone, params[:start_date], params[:end_date])
# I dont know where you set #start_date and #end_date
#financial_nav = FinancialReportNav.set_financial_type(#current_user.id,#financial_type, #start_date, #end_date)
# renamed (or you can set this instance variable name dynamically)
#records = model.active.all
# renamed (or you can set this instance variable name dynamically)
#deleted_records = model.inactive.all
#financial_populate = Financial.new(#financial_params, #financial_type).populate_project_financials(current_company.default_hourly_cost, current_company.billing_rate, #timeframe[:start_date],#timeframe[:end_date])
#financial_rows = financial_model.all.map { |p| [ p.project_id, p.billable_hours, p.revenue,p.real_rate, p.hourly_expense, p.labor_expense_total, p.salary_expense, p.gross_profit, p.profit_margin, p.missing_hourly_expense, p.missing_billable_rate ] }
#items = [1]
#filter_by_client = Client.find_by('id = ?', #financial_params[:filter_by_client])
#filter_by_project = Project.find_by('id = ?', #financial_params[:filter_by_project])
#filter_by_person = Person.find_by('id = ?', #financial_params[:filter_by_person])
#filter_by_role = PersonRole.find_by('id = ?', #financial_params[:filter_by_role])
#project_list = Financial.project_list(#timeframe[:start_date], #timeframe[:end_date])
#client_list = Financial.client_list(#timeframe[:start_date], #timeframe[:end_date])
#people_list = Financial.people_list(#timeframe[:start_date], #timeframe[:end_date])
end
end
app/controllers/client_financials_controller.rb
class ClientFinancialsController < FinancialController
def index
render template: 'financials/index'
end
end
app/controllers/project_financials_controller.rb
class ProjectFinancialsController < FinancialController
def index
render template: 'financials/index'
end
end
app/controllers/person_financials_controller.rb
class ProjectFinancialsController < FinancialController
def index
render template: 'financials/index'
end
end
app/controllers/role_financials_controller.rb
class ProjectFinancialsController < FinancialController
def index
render template: 'financials/index'
end
end
Views
app/views/financials/index.html.erb
<!-- YOUR SHARED "FINANCIALS" INDEX HTML HERE -->
P.S. This is just a simple refactor. Without knowing the fuller scope of the project, and future plans, I'll just do this one. Having said this, I would consider using "polymorpic" associations, and then just have one routes endpoint (i.e. resources :financials) and then just pass in a params filter like: params[:financial_type] which directly already map the financial_type polymorphic column name.

Spree error when using decorator with the original code

Need a little help over here :-)
I'm trying to extend the Order class using a decorator, but I get an error back, even when I use the exactly same code from source. For example:
order_decorator.rb (the method is exactly like the source, I'm just using a decorator)
Spree::Order.class_eval do
def update_from_params(params, permitted_params, request_env = {})
success = false
#updating_params = params
run_callbacks :updating_from_params do
attributes = #updating_params[:order] ? #updating_params[:order].permit(permitted_params).delete_if { |k,v| v.nil? } : {}
# Set existing card after setting permitted parameters because
# rails would slice parameters containg ruby objects, apparently
existing_card_id = #updating_params[:order] ? #updating_params[:order][:existing_card] : nil
if existing_card_id.present?
credit_card = CreditCard.find existing_card_id
if credit_card.user_id != self.user_id || credit_card.user_id.blank?
raise Core::GatewayError.new Spree.t(:invalid_credit_card)
end
credit_card.verification_value = params[:cvc_confirm] if params[:cvc_confirm].present?
attributes[:payments_attributes].first[:source] = credit_card
attributes[:payments_attributes].first[:payment_method_id] = credit_card.payment_method_id
attributes[:payments_attributes].first.delete :source_attributes
end
if attributes[:payments_attributes]
attributes[:payments_attributes].first[:request_env] = request_env
end
success = self.update_attributes(attributes)
set_shipments_cost if self.shipments.any?
end
#updating_params = nil
success
end
end
When I run this code, spree never finds #updating_params[:order][:existing_card], even when I select an existing card. Because of that, I can never complete the transaction using a pre-existent card and bogus gateway(gives me empty blanks errors instead).
I tried to bind the method in order_decorator.rb using pry and noticed that the [:existing_card] is actuality at #updating_params' level and not at #updating_params[:order]'s level.
When I delete the decorator, the original code just works fine.
Could somebody explain to me what is wrong with my code?
Thanks,
The method you want to redefine is not really the method of the Order class. It is the method that are mixed by Checkout module within the Order class.
You can see it here: https://github.com/spree/spree/blob/master/core/app/models/spree/order/checkout.rb
Try to do what you want this way:
Create file app/models/spree/order/checkout.rb with code
Spree::Order::Checkout.class_eval do
def self.included(klass)
super
klass.class_eval do
def update_from_params(params, permitted_params, request_env = {})
...
...
...
end
end
end
end

Rails - 'can't dump hash with default proc' during custom validation

I have 2 models. User and Want. A User has_many: Wants.
The Want model has a single property besides user_id, that's name.
I have written a custom validation in the Want model so that a user cannot submit to create 2 wants with the same name:
validate :existing_want
private
def existing_want
return unless errors.blank?
errors.add(:existing_want, "you already want that") if user.already_wants? name
end
The already_wants? method is in the User model:
def already_wants? want_name
does_want_already = false
self.wants.each { |w| does_want_already = true if w.name == want_name }
does_want_already
end
The validation specs pass in my model tests, but my feature tests fail when i try and submit a duplicate to the create action in the WantsController:
def create
#want = current_user.wants.build(params[:want])
if #want.save
flash[:success] = "success!"
redirect_to user_account_path current_user.username
else
flash[:validation] = #want.errors
redirect_to user_account_path current_user.username
end
end
The error I get: can't dump hash with default proc
No stack trace that leads to my code.
I have narrowed the issue down to this line:
self.wants.each { |w| does_want_already = true if w.name == want_name }
if I just return true regardless the error shows in my view as I would like.
I don't understand? What's wrong? and why is it so cryptic?
Thanks.
Without a stack trace (does it lead anywhere, or does it just not appear?) it is difficult to know what exactly is happening, but here's how you can reproduce this error in a clean environment:
# initialize a new hash using a block, so it has a default proc
h = Hash.new {|h,k| h[k] = k }
# attempt to serialize it:
Marshal.dump(h)
#=> TypeError: can't dump hash with default proc
Ruby can't serialize procs, so it wouldn't be able to properly reconstitute that serialized hash, hence the error.
If you're reasonably sure that line is the source of your trouble, try refactoring it to see if that solves the problem.
def already_wants? want_name
wants.any? {|want| want_name == want.name }
end
or
def already_wants? want_name
wants.where(name: want_name).count > 0
end

Non-redundant way to use a single controller to collect the exact same data from four models?

I just don't want to copy and paste. There has to be a better way.
I've got a single controller that uses a single model to collect some data into two arrays. Then I have a single view (index.html.erb) that uses those arrays in a graph. It's absurdly simple. This is the whole view. The arrays from the controller are, obviously, #buildStepArrays and #buildDates.
<% chart = GChart.line(:title=>"Build Times", :size=>"1000x300", :data=>#buildStepArrays, :colors=>#colors, :legend=>#buildDates) %>
<% chart.axis(:left) %>
<%= image_tag chart.to_url %>
Controller is here
def index
# These three arrays should be the same size
#buildStepArrays = []
#buildDates = []
#totalBuildTimes = []
#latestId = Env2.last().BuildId
#latestId = #latestId - 1
for buildNumber in (#latestId-4)..#latestId
#build = Env2.find_all_by_BuildId(buildNumber)
totalTime = 0
#currentBuildTimes = []
for step in #build
#currentBuildTimes << step.Minutes
totalTime += step.Minutes.to_i
end
#buildStepArrays << #currentBuildTimes.map { |e| e.nil? ? 0 : e }
#totalBuildTimes << totalTime
#buildDates << #build.last().Created
#colors = [["FF1300"], ["FF8C00"], ["FFFF00"], ["00CC00"], ["1240AB"]]
end
end
What I would like to do is collect the exact same data from four models (same table in four different databases), not just one, and then show four graphs on the view instead of one. I don't know enough about Rails to know how to do this correctly. My only idea right now is to literally copy and paste the code inside my controller four times and change the variable names around. That is totally awful. What am I "supposed" to do?
One way to achieve this is to create modules that hold code that is repeated, which is your case, is at least four times. What you would do is still create the controller files for each model...
class ModelAController < ApplicationController
include BasicActions
before_filter get_model
def get_model
#model = self.class.to_s.gsub("Controller").classify
end
end
You'll notice the 'get_model' method, which is important, since it sets the #model variable to hold the class you are dealing with. I didn't confirm the exact code to get just the coass name text, so you'll have to play around with that. You'll probably be using #model inside your module.
Here is a skeletal module:
module BasicActions
def some_method
...
end
You'll still need all of related views for each action that renders, but there are ways to limit that to just one set of views...
The most elegant solution would probably be to use a presenter. Presenters can help you either clean up logic from your views or clean up too many instance variables from your controller. The latter seems to be the case here.
Using a presenter would allow you to:
Optimize database queries, in your case doing a single query from all four databases.
Make testing your code much easier by allowing you to write tests directly to the actions in your presenters.
It's hard to say exactly how you would build your presenters without knowing more about your app, but I can give you some general guidelines with mockup code just to show you how it works.
Instead of simply defining your instance variables from the four different models, you would define a new action in your controller similar to this:
/app/controllers/charts_controller.rb
...
def show
#data = ChartPresenter.new(argument)
end
...
And then you would define the new presenter class in a new directory, with actions corresponding to the output you need in your views:
/app/presenters/chart_presenter.rb
class ChartPresenter
def initialize(data)
#data = data
end
def method_name
...
end
end
/app/views/chart.html.erb
...
<%= #data.method_name %>
...
References
RailsCasts #287 - Presenters from Scratch - only available for RailsCasts Pro subscribers
Simplifying your Ruby on Rails code: Presenter pattern, cells plugin - it's from 2009, but might be useful.
P.S.
Most tutorials will tell you to edit your config/application.rb to set config.autoload_paths to your presenters' directory. However, in Rails 3 this is no longer necessary since everything under /app/* is automatically added.
Try this
Controller
def index
#data = [Env1, Env2, Env3].map do |model|
data = Hash.new
data[:title] = "Build Times"
data[:buildStepArrays] = []
data[:buildDates] = []
data[:totalBuildTimes] = []
data[:latestId] = model.last().BuildId
data[:latestId] = data[:latestId] - 1
for buildNumber in (data[:latestId]-4)..data[:latestId]
data[:build] = model.find_all_by_BuildId(buildNumber)
totalTime = 0
data[:currentBuildTimes] = []
for step in data[:build]
data[:currentBuildTimes] << step.Minutes
totalTime += step.Minutes.to_i
end
data[:buildStepArrays] << data[:currentBuildTimes].map { |e| e.nil? ? 0 : e }
data[:totalBuildTimes] << totalTime
data[:buildDates] << data[:build].last().Created
data[:colors] = [["FF1300"], ["FF8C00"], ["FFFF00"], ["00CC00"], ["1240AB"]]
end
end
end
View
<% #data.each do |data| %>
<% chart = GChart.line(:title=>data[:title], :size=>"1000x300", :data=>data[:buildStepArrays], :colors=>data[:colors], :legend=>data[:buildDates]) %>
<% chart.axis(:left) %>
<%= image_tag chart.to_url %>
<% end %>
I just trowed everything into the Hash but you better just put there you need to render in the view.

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