Is it possible to define two methods in a Rails model that require different initialization? - ruby-on-rails

Hi I'm attempting to create a model in Rails that can perform two calculations. This is my code:
class Calculator
def initialize(nair, cppy, interest_rate, payment, periods)
#nair = nair.to_f / 100
#cppy = cppy.to_f
#interest_rate = interest_rate
#payment = payment
#periods = periods
end
def effective
Refinance::Annuities.effective_interest_rate(#nair, #cppy)
end
def principal
Refinance::Annuities.principal(#interest_rate, #payment, #periods)
end
end
I have two forms that reside in different views that take input from the user including 'nair' and 'cppy' on one and 'interest_rate', 'payment' and 'periods' on the other.
The problem I've run into is that to use this model all five arguments need to be available.
Do I need to have separate models for each calculation?
I'm a complete beginning sorry if there is a really obvious answer.
Thanks!

There's probably a dozen different ways you could solve this, but one possible approach would be to use default arguments in your initialize method.
class Calculator
def initialize(nair=0, cppy=0, interest_rate=0, payment=0, periods=0)
#nair = nair.to_f / 100
#cppy = cppy.to_f
#interest_rate = interest_rate
#payment = payment
#periods = periods
end
def effective
Refinance::Annuities.effective_interest_rate(#nair, #cppy)
end
def principal
Refinance::Annuities.principal(#interest_rate, #payment, #periods)
end
end
Another possible solution is to make them class methods and not deal with instances or state:
class Calculator
def self.effective(nair, cppy)
nair = nair.to_f / 100
cppy = cppy.to_f
Refinance::Annuities.effective_interest_rate(nair, cppy)
end
def self.principal(interest_rate, payment, periods)
Refinance::Annuities.principal(interest_rate, payment, periods)
end
end
Calculator.effective(x, y)
Calculator.principal(x, y, z)

Related

How to connect with different tables depending on parameter

I have model Board and BoardController where i can find all boards of my project.
All records have filled "board_layout" column with enum.
Now i have show method in BoardController and i want to load there different data from different table depending on board_layout column value.
I could do it like this:
def show
#board = Board.find(params[:id])
if #board.board_layout == 1
#tasks = Car.where(board_id: #board.id)
elsif #board.board_layout == 2
#tasks = Truck.where(board_id: #board.id)
end
end
But it's not elegant + it is not DRY (i need to use some sort of if statement anytime when i want to do something with those 2 tables).
So i have tried to create Concern and create case statement inside, now it looks like:
def show
#board = Board.find(params[:id])
#tasks = get_class_name(#board.board_layout).where(board_id: #board.id)
end
# inside my concern
def get_class_name(scope)
case scope
when 1
Car
when 2
Truck
end
end
My Question:
Is there better way to do it? Is my solution safe and clear?
What is the best solution to resolve problem like this?
I would appreciate any help.
maybe you can abstract that out into a class, so you can define multiple layout and their correspondent classes, like:
class LayoutClassGetter
CLASS_BY_LAYOUT = { '1' => Car, '2' => Truck }.freeze
def initialize(layout_number)
#layout_number = layout_number
end
def layout_class
CLASS_BY_LAYOUT[#layout_number]
end
def self.layout_class(layout_number)
new(layout_number).layout
end
end
And then use it:
def show
#board = Board.find(params[:id])
#tasks = layout_class(#board.board_layout).where(board_id: #board.id)
end
def layout_class(scope)
LayoutClassGetter.layout_class(scope)
end

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.

Automatically Scheduling Conference

I'm trying to create a Ruby on Rails site that manages conferences. It should fill in time slots without any gaps in between. I've got it to the point where it fill in the the slots. But in most instances it leaves some time slots empty. I'm not able to find the flow in my logic.
app/services/conference_service.rb
class ConferenceService
def initialize(conference, temp_file)
self.first_track = conference.tracks.first
self.second_track = conference.tracks.last
self.file = temp_file
self.talks = []
end
def call
create_talks
set_track(1, 'Lunch')
set_track(2, 'Lunch')
set_track(1, 'Networking Event')
# set_track(2, 'Networking Event')
set_second_track_evening
end
private
def create_talks
file.read.split(/\n/).each do |line|
next if line.blank?
title = line.split(/\d|lightning/).first
length = line.scan(/\d+/).first
length = length.nil? ? 5 : length.to_i
talks << Talk.create(title: title, length: length)
end
end
attr_accessor :first_track, :second_track, :file, :talks
def set_track(track_number, track_portion)
track = track_number == 1 ? first_track : second_track
time = track_portion == 'Lunch' ? Time.zone.now.change(hour: 9) : Time.zone.now.change(hour: 13)
minutes = track_portion == 'Lunch' ? 180 : 240
talks.shuffle!
local_talks = []
n = 0
while local_talks.map(&:length).inject(0, &:+) < minutes
local_talks << talks[n]
n += 1
end
if local_talks.map(&:length).inject(0, &:+) == minutes
local_talks.each do |talk|
talk.start_time = time
track.talks << talk
time = time.advance(minutes: talk.length)
end
track.talks << Talk.create(title: track_portion, start_time: time, length: 60)
track.save
(0..local_talks.count - 1).each do |i|
talks.delete_at(i)
end
else
set_track(track_number, track_portion)
end
end
def set_second_track_evening
time = Time.zone.now.change(hour: 13)
talks.each do |talk|
talk.start_time = time
time = time.advance(minutes: talk.length)
end
second_track.talks << talks
second_track.talks << Talk.create(title: 'Networking Event', start_time: time.change(hour: 17), length: 60)
end
end
app/controllers/conference_controller.rb
def create
#conference = Conference.new(conference_params)
build_tracks
conference_service = ConferenceService.new(#conference, input_file)
conference_service.call
respond_to do |format|
if #conference.save
format.html { redirect_to #conference, notice: 'Conference was successfully created.' }
format.json { render :show, status: :created, location: #conference }
else
format.html { render :new }
format.json { render json: #conference.errors, status: :unprocessable_entity }
end
end
end
def input_file
params['conference']['input_file']
end
input file
Writing Fast Tests Against Enterprise Rails 60min
Overdoing it in Python 45min
Lua for the Masses 30min
Ruby Errors from Mismatched Gem Versions 45min
Common Ruby Errors 45min
Rails for Python Developers lightning
Communicating Over Distance 60min
Accounting-Driven Development 45min
Woah 30min
Sit Down and Write 30min
Pair Programming vs Noise 45min
Rails Magic 60min
Ruby on Rails: Why We Should Move On 60min
Clojure Ate Scala (on my project) 45min
Programming in the Boondocks of Seattle 30min
Ruby vs. Clojure for Back-End Development 30min
Ruby on Rails Legacy App Maintenance 60min
A World Without HackerNews 30min
User Interface CSS in Rails Apps 30min
error when calling set_track(2, 'Networking Event')
undefined method `length' for nil:NilClass #line 42
Recommend you do a few things before worrying about the algorithm:
Separate concerns / Single Responsibility. The code that parses the file should be independent from the code that runs the business logic, which should be independent from the code that saves to your database. Separating these things may seem unnecessary for simple logic (and may be), but is necessary as your app complexity grows.
Write tests. As you refactor your code, you're going to want to ensure it still works. Bonus: Writing code that you can test forces you to create interfaces that you can understand, which can make the code easier to understand!
Come up with a design first. Reading this code I have no idea what the intention of the sections are. One of my favorite ways to do this is to use Class, Responsibilities, Collaborators post cards (see https://en.wikipedia.org/wiki/Class-responsibility-collaboration_card and http://agilemodeling.com/artifacts/crcModel.htm).
It seems like you could break this code down into:
Parse input file into generic 'Talk' objects that have a length (in minutes) and a name. I would not have these be DB backed. If it's the same concept as an ActiveRecord model, we often name this a TalkDouble (or similar). I'd also recommend just using CSV here rather than your own custom (and hard to parse) format.
Schedule talk objects into tracks. It seems like you're trying to randomize the talks across two tracks, with some built-in lunch breaks (?). Whatever the desired behavior, this also doesn't need to use anything but plain old ruby objects. I've found it best to have the logic be stateless/idempotent and return a new object each time it's run as the result.
For example:
class TalkScheduler
def schedule(talks, number_of_tracks: 2)
# Logic goes here, returns an array of `Tracks`
# each with a set of talks.
tracks = build_tracks(number_of_tracks)
talks.each do |talk|
tracks.sample.add_talk(talk)
end
tracks
end
def build_tracks(number)
(0..number).times.map do { Track.new }
end
end
However, if you're looking for an algorithm that chooses "best fit" of available talks into open spaces, you're essentially trying to solve the Knapsack problem (https://en.wikipedia.org/wiki/Knapsack_problem). It may not become combinatorially hard due to the limited number of talk lengths (e.g. only 30, 45 and 60) but realize that you're slipping into challenging territory.
I'd also question the value to anyone of the ability to create conference with a random order of talks vs. just being able to organize them by hand.
In any case, you could handle solving the problem of determining a (random?) selection of talks in a given time-space with something like the following:
class Schedule
SLOT_LENGTH = 15
attr_accessor :start, :length, :talks
def initialize(start:, length:)
#start = start
#length = length
#slots = length / SLOT_LENGTH
#talks = []
end
def add_talk(talk)
talks.push(talk)
end
def slots_remaining
slots - talks.map(&:length).sum / SLOT_LENGTH
end
def can_fit?(talk)
talk.length / SLOT_LENGTH <= slots_remaining
end
end
class TalkScheduler
def schedule(talks, schedules)
unscheduled_talks = talks.dup.shuffle # Always dup, even if you don't shuffle
schedules.each do |schedule|
while(talks.any?)
index = unscheduled_talks.index{|t| schedule.can_fit?(t) }
break unless index
talk = unscheduled_talks.delete_at(index)
schedule.add_talk(talk)
end
end
end
end
I'd think a bit more about to model lunches, networking breaks, etc. before deciding to model them as talks or as something else, but using this type of pattern (simple ruby objects that store data being manipulated by NounVerber classes that contain the complex business logic) has been very helpful to me for simplifying handling complex workflows like what you're doing here.
Good luck!

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])

Rails more idiomatic way of adding up values

I am working on a survey app, and an Organisation has 0 or more SurveyGroups which have 0 or more Members who take the survey
For a SurveyGroup I need to know how many surveys still need to be completed so I have this method:
SurveyGroup#surveys_outstanding
def surveys_outstanding
respondents_count - surveys_completed
end
But I also need to know how many surveys are outstanding at an organisational level, so I have a method like below, but is there a more idiomatic way to do this with Array#inject or Array#reduce or similar?
Organisation#surveys_pending
def surveys_pending
result = 0
survey_groups.each do |survey_group|
result += survey_group.surveys_outstanding
end
result
end
Try this:
def surveys_pending
#surveys_pending ||= survey_groups.map(&:surveys_outstanding).sum
end
I'm using memoization in case it is slow to calculate
def surveys_pending
survey_groups.inject(0) do |result, survey_group|
result + survey_group.surveys_outstanding
end
end

Resources