ActiveModel::MissingAttributeError in Controller can't write unknown attribute - ruby-on-rails

For one of the views in my rails application, I have set up the controller as such. I want to get all students records from the db and append extra values to each student. This is giving me the error:
ActiveModel::MissingAttributeError in MemoMainTesterController#test_students
can't write unknown attribute current_target
class MemoMainTesterController < ApplicationController
def test_students
#all_students = Student.all
#all_students.each do |student|
current = current_target(student)
previous_test_info = last_pass(student)
student[:current_target] = current[0]
student[:current_level] = current[1]
student[:current_target_string] = "Level #{current[0]} - Target #{current[1]}"
student[:last_pass] = previous_test_info[0]
student[:attempts] = previous_test_info[1]
student[:last_pass_string] = previous_test_info[2]
end
end
.
.
.
end
It occurs specifically where student[:current_target] = current[0].
Am I not allowed to append extra values to this hash?
Is there a workaround for this?
EDIT: Although Student.all is a model instance, I want to turn it into a hash and append more key value pairs to it.

In your case, student is not a Hash but a Student model instance.
When you call student[:current_target] you are attempting to write Student's current_target attribute, which surely is not an actual attribute in the DB for students table. Hence the error.
To obtain a hash from your models containing the extra data, you may consider this refactor:
class MemoMainTesterController < ApplicationController
def test_students
#all_students = Student.all
#students_with_steroids = #all_students.map do |student|
current = current_target(student)
previous_test_info = last_pass(student)
student_attributes = student.attributes # <= this is a hash, that you store in student_attributes hash variable
student_attributes.merge(current_target: current[0],
current_level: current[1],
current_target_string: "Level #{current[0]} - Target #{current[1]}",
last_pass: previous_test_info[0],
attempts: previous_test_info[1],
last_pass_string: previous_test_info[2])
end
end

Related

rails mongoid undefined bson_type

I defined my own custom field type Price for my application. I defined all the required methods (demongoize, mongoize, ..) and it works perfectly fine retrieving the data from mongodb, sending it to front end and getting data back and store them in mongo db. My Price class stores the data as an array [value, currency] in the DB and also my front end expect the price to be delivered as a an array. All ok so far.
In my model I now added a Hash which reflects a tree structure containing objects which have a price. I was re-using the price class for these prices when creating the tree structure.
But now I get an undefined bs_type error when I try to save this hash field to the database. Looks like rails/mongoid doesnt find a method to transform the price in the Hash into a suitable mongoid format.
I tried to define as_json, to_json, to_s, ... everything on my Price model but I always get the same message: undefined bson_type for Price whenever I want to save my tree field.
(If I store my values as [value,currency] in the hash - not using Price, obviously all works fine)
Any idea?
class Price
def initialize(value = 0, currency = '')
#value,#currency = value, currency
end
def mongoize
[#value,#currency.to_s]
end
class << self
def demongoize(object)
if object
cur = object[1] || ''
Price.new(object[0], cur)
else
Price.new()
end
end
def mongoize(object)
case object
when Price then object.mongoize
when Array then Price.new(object[0], object[1]).mongoize
else object
end
end
def evolve(object)
case object
when Price then object.mongoize
else object
end
end
end
I have another model:
class Financial
include Mongoid::Document
field: quote, type: Price
field: tree, type: Hash
end
In my application code setting the quote and saving it works.
Here is the problem:
revenue = Price.new(10,'USD')
record = Financial.new()
record.quote = revenue
record.save # All works so far
record.tree = { data: [10,'USD']}
record.save # this works too
record.tree = { data: revenue}
record.save # here I get the undefined bson_type for Price error.

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.

wrong number of arguments (given 0, expected 4)

I am getting this error for this set up. My thought is that the file cannot properly access the csv. That I am attempting to import. I've got to import from one csv to create another csv using the model date. What do I put in the controller and views to show the new csv / manipulated data? Basically how can I pass one csv file in a model for manipulation (orders.csv) and out into another csv file (redemption.csv) the code in the model is just telling model to calculate the existing numbers in orders.csv a certain way for export without this argument error?
The controller (I don't really know what to do here)
class OrdersController < ApplicationController
def index
orders = Order.new
end
def redemptions
orders = Order.new
end
end
The View (not confident about this either)
<h1>Chocolates</h1>
puts "#{order.purchased_chocolate_count}"
<%= link_to "CSV", orders_redemptions_path, :format => :csv %>
Model
require 'csv'
# Define an Order class to make it easier to store / calculate chocolate tallies
class Order < ActiveRecord::Base
module ChocolateTypes
MILK = 'milk'
DARK = 'dark'
WHITE = 'white'
SUGARFREE = 'sugar free'
end
BonusChocolateTypes = {
ChocolateTypes::MILK => [ChocolateTypes::MILK, ChocolateTypes::SUGARFREE],
ChocolateTypes::DARK => [ChocolateTypes::DARK],
ChocolateTypes::WHITE => [ChocolateTypes::WHITE, ChocolateTypes::SUGARFREE],
ChocolateTypes::SUGARFREE => [ChocolateTypes::SUGARFREE, ChocolateTypes::DARK]
}
# Ruby has this wacky thing called attr_reader that defines the available
# operations that can be performed on class member variables from outside:
attr_reader :order_value
attr_reader :chocolate_price
attr_reader :required_wrapper_count
attr_reader :order_chocolate_type
attr_reader :chocolate_counts
def initialize(order_value, chocolate_price, required_wrapper_count, order_chocolate_type)
#order_value = order_value
#chocolate_price = chocolate_price
#required_wrapper_count = required_wrapper_count
#order_chocolate_type = order_chocolate_type
# Initialize a new hash to store the chocolate counts by chocolate type.
# Set the default value for each chocolate type to 0
#chocolate_counts = Hash.new(0);
process
end
# Return the number of chocolates purchased
def purchased_chocolate_count
# In Ruby, division of two integer values returns an integer value,
# so you don't have to floor the result explicitly
order_value / chocolate_price
end
# Return the number of chocolate bonuses to award (which can include
# multiple different chocolate types; see BonusChocolateTypes above)
def bonus_chocolate_count
(purchased_chocolate_count / required_wrapper_count).to_i
end
# Process the order:
# 1. Add chocolate counts to the totals hash for the specified order type
# 2. Add the bonus chocolate types awarded for this order
def process
chocolate_counts[order_chocolate_type] += purchased_chocolate_count
bonus_chocolate_count.times do |i|
BonusChocolateTypes[order_chocolate_type].each do |bonus_chocolate_type|
chocolate_counts[bonus_chocolate_type] += 1
end
end
end
# Output the chocolate counts (including bonuses) for the order as an array
# of strings suitable for piping to an output CSV
def csv_data
ChocolateTypes.constants.map do |output_chocolate_type|
# Get the display string (lowercase)
chocolate_key = ChocolateTypes.const_get(output_chocolate_type)
chocolate_count = chocolate_counts[chocolate_key].to_i
"#{chocolate_key} #{chocolate_count}"
end
end
end
# Create a file handle to the output file
CSV.open("redemptions.csv", "wb") do |redemption_csv|
# Read in the input file and store it as an array of lines
input_lines = CSV.read("orders.csv")
# Remove the first line from the input file (it just contains the CSV headers)
input_lines.shift()
input_lines.each do |input_line|
order_value, chocolate_price, required_wrapper_count, chocolate_type = input_line
# Correct the input values to the correct types
order_value = order_value.to_f
chocolate_price = chocolate_price.to_f
required_wrapper_count = required_wrapper_count.to_i
# Sanitize the chocolate type from the input line so that it doesn't
# include any quotes or leading / trailing whitespace
chocolate_type = chocolate_type.gsub(/[']/, '').strip
order = Order.new(order_value, chocolate_price, required_wrapper_count, chocolate_type)
order.process()
puts order.purchased_chocolate_count
# Append the order to the output file as a new CSV line
output_csv << order.csv_data
end
end
In Your initialize method you are not provide default value to argument.
def initialize(order_value, chocolate_price, required_wrapper_count, order_chocolate_type)
When you are trying to run orders = Order.new it is expecting four argument and you haven't provide it.
One more issue. Your local variable name should be order not orders for proper naming convention.
To assign default values properly, you can look here.

Passing from a Controller to a Model

Pretty new to RoR. Wonder if anyone can help me with this issue.
I got a gem called "business_time" which calculates the business days between two dates. I have set up a method in the model which does all the calculations.
I have a field called "credit" which should hold the number of business days. Here's what I have:
MODEL
def self.calculate(from_date,to_date)
days = 0
date_1 = Date.parse(from_date)
date 2 = Date.parse(to_date)
days = date_1.business_days_until(date2)
days
end
CONTROLLER
def new
#vacation = current_user.vacations.build
#vacations = Vacation.calculate(:from_date, :to_date)
end
I got an error referencing something about a string.
Furthermore, how do I go about storing the data from the method into the field called "credit"?
Thanks guys.
I think there is no need for an extra method, since all attributes (from_date, end_date and credit) are stored in the same model.
I would just set from_date and end_date in the initializer and calculate credit with a callback before validation:
# in the model
before_validation :calculate_credit
private
def calculate_credit
if from_date && to_date
# `+ 1` because the user takes off both days (`from_date` and `to_date`),
# but `business_days_until` doesn't count the `from_day`.
self.credit = from_date.business_days_until(to_date) + 1
end
end
# in the controller
def new
#vacation = current_user.vacations.build
end
def create
#vacation = current_user.vacations.build(vacation_params)
if #vacation.save
# #vacation.credit would return the calculated credit at this point
else
# ...
end
end
private
def vacation_params
params.require(:vacation).permit(:from_date, :to_date)
end
What you need here is pass String objects instead of Symbol objects.
So instead of #vacations = Vacation.calculate(:from_date, :to_date), you probably need to pass params[:from_date] and params[:to_date] which should be strings like 20/01/2016, etc...
Your code should be
#vacations = Vacation.calculate(params[:from_date], params[:to_date])

Creating new class to put into an Array in Ruby

I am coming from a C# background and trying to learn Ruby and Ruby on Rails. I have the following Car class - note the build_xml method I need in order to build XML in that syntax and then pass to a WebService
class Car
##array = Array.new
#this will allow us to get list of all instances of cars created if needed
def self.all_instances
##array
end
def initialize(id, model_number, engine_size, no_doors)
# Instance variables
#id = id
#model_number = model_number
#engine_size = engine_size
#no_doors = no_doors
##array << self
end
def build_car_xml
car = { 'abc:Id'=> #id, 'abc:ModelNo' => #model_number, 'abc:ES' => #engine_size, 'abc:ND' => #no_doors}
cars = {'abc:Car' => [car] }
end
end
In another class then I was using this as below:
car1 = Car.new('1', 18, 3.0, 4)
request = car1.build_car_xml
This works as expected and the request is formatted how I need and the webservice returns the results. I now want to expand this however so I can pass in an array of cars and produce the request XML - however I am struggling to get this part working.
So far I have been trying the following (for now I am ok with just the Id changing as it is the only parameter required to be unique):
car_array = []
(1..10).each do |i|
car_array << Car.new(i.to_s, 18, 3.0, 4)
end
Am I correct in saying that I would need to define a new build_car_xml method on my Car class that can take an array of cars and then build the xml so my request call would be something like:
request = Car.build_car_xml(car_array)
What i am unsure of is 1) - is this the correct way of doing things in Ruby and 2) how to construct the method so that it is Building the XML in the correct format in the way it was when I call it on the single object - i.e - I need the namespaces added before the actual value.
def build_car_xml(car_array)
#here is where I am unsure how to contruct this method
end
Possible solution ('abc:Car' is a wrong name, should be Cars if you want it to hold an array):
class Car
...
def self.build_cars_xml(cars)
{ 'abc:Car' => cars.map(&:build_car_xml) }
end
def build_car_xml
{ 'abc:Id'=> #id, 'abc:ModelNo' => #model_number, 'abc:ES' => #engine_size, 'abc:ND' => #no_doors }
end
end
cars =
(1..10).map do |i|
Car.new(i.to_s, 18, 3.0, 4)
end
Car.build_cars_xml(cars)
It doesn't meet your requirements as instance build_car_xml doesn't generate Car namespace, but for me it's some inconsistency. Your XML is actually a collection, even if it has just one element, instance method should not be responsible for collection. Car.build_cars_xml([Car.new(...)] looks more logical to me.

Resources