Ruby Cyclomatic and Perceived complexity - ruby-on-rails

I have this service object that parses an excel file and persists the data in it. Rubocop is flagging it as:
app/services/csv_upload.rb:17:3: C: Metrics/AbcSize: Assignment Branch Condition size for parse_spreadsheet is too high. [<14, 34, 11> 38.38/35]
def parse_spreadsheet ...
^^^^^^^^^^^^^^^^^^^^^
app/services/csv_upload.rb:17:3: C: Metrics/CyclomaticComplexity: Cyclomatic complexity for parse_spreadsheet is too high. [10/6]
def parse_spreadsheet ...
^^^^^^^^^^^^^^^^^^^^^
app/services/csv_upload.rb:17:3: C: Metrics/PerceivedComplexity: Perceived complexity for parse_spreadsheet is too high. [11/7]
How do i fix this? How should I refactor this code?
The parse_spreadsheet method is where it is being flagged on.
require "spreadsheet"
class CsvUpload
attr_accessor :review
def initialize(file)
Spreadsheet.client_encoding = "UTF-8"
tempfile = File.open(file.tempfile)
#spreadsheet = Spreadsheet.open tempfile
#exception_sheet = #spreadsheet.worksheet 3
#review = []
parse_spreadsheet
end
def parse_spreadsheet
#exception_sheet.each_with_index 4 do |row, index|
employee_no = row[1]
department = row[2]
date = row[3].to_datetime
time_in = row[4].to_datetime unless row[4].nil?
time_out = row[5].to_datetime unless row[5].nil?
if department == "Night"
next_row = #exception_sheet.to_a[index + 5]
next_date = next_row[3].to_datetime
next_time_in = next_row[4].to_datetime unless next_row[4].nil?
start_time = set_date_time(date, time_out) unless time_out.nil?
end_time = set_date_time(next_date, next_time_in) unless next_time_in.nil?
else
start_time = set_date_time(date, time_in) unless time_in.nil?
end_time = set_date_time(date, time_out) unless time_out.nil?
end
#employee = Employee.find_by(employee_no: employee_no)
next if #employee.nil?
#attendance_item = AttendanceItem.create(
start_time: start_time, end_time: end_time, employee_id: #employee.id
)
end
end
def to_review
#review
end
private
def set_date_time(date, time)
DateTime.new(
date.year,
date.month,
date.day,
time.hour,
time.min,
time.sec,
"+0800"
).in_time_zone
end
end

I'd start by having a separate method for each of your items in #exception_sheet:
def parse_spreadsheet
#exception_sheet.each_with_index 4 do |row, index|
handle_exception_row(row, index)
end
end
def handle_exception_row(row, index)
employee_no = row[1]
department = row[2]
date = row[3].to_datetime
time_in = row[4].to_datetime unless row[4].nil?
time_out = row[5].to_datetime unless row[5].nil?
if department == "Night"
next_row = #exception_sheet.to_a[index + 5]
next_date = next_row[3].to_datetime
next_time_in = next_row[4].to_datetime unless next_row[4].nil?
start_time = set_date_time(date, time_out) unless time_out.nil?
end_time = set_date_time(next_date, next_time_in) unless next_time_in.nil?
else
start_time = set_date_time(date, time_in) unless time_in.nil?
end_time = set_date_time(date, time_out) unless time_out.nil?
end
#employee = Employee.find_by(employee_no: employee_no)
next if #employee.nil?
#attendance_item = AttendanceItem.create(
start_time: start_time, end_time: end_time, employee_id: #employee.id
)
end
end
That fixes the parse_spreadsheet method but creates new errors with handle_exception_row. Then stripping that down:
def handle_exception_row(row, index)
employee_no = row[1]
department = row[2]
date = row[3].to_datetime
time_in = row[4].to_datetime unless row[4].nil?
time_out = row[5].to_datetime unless row[5].nil?
handle_attendance_creation(employee_no: employee_no, department: department, date: date, time_in: time_in, time_out: time_out)
end
def handle_attendance_creation(employee_no:, department:, date:, time_in:, time_out:)
if department == "Night"
next_row = #exception_sheet.to_a[index + 5]
next_date = next_row[3].to_datetime
next_time_in = next_row[4].to_datetime unless next_row[4].nil?
start_time = set_date_time(date, time_out) unless time_out.nil?
end_time = set_date_time(next_date, next_time_in) unless next_time_in.nil?
else
start_time = set_date_time(date, time_in) unless time_in.nil?
end_time = set_date_time(date, time_out) unless time_out.nil?
end
#employee = Employee.find_by(employee_no: employee_no)
next if #employee.nil?
#attendance_item = AttendanceItem.create(
start_time: start_time, end_time: end_time, employee_id: #employee.id
)
end
end
You'll probably still get an error on handle_attendance_creation, but hopefully you can see where this is going. Strip out further smaller methods that take the required arguments and you'll get there

Related

download leaderboard reports into a csv rails

I have a reports controller where I have leaderboard. I would like to download the leader board into a csv file
here is the leaderboard code
def leaderboard
if params.has_key?(:start_date) && params.has_key?(:end_date) && params.has_key?(:account)
if params[:unit] == 'all'
#start_date = current_user.created_at
#end_date = Time.now
else
#start_date = Time.at(params[:start_date].to_i/1000)
#end_date = Time.at(params[:end_date].to_i/1000)
end
if params[:account] == 'all'
user_accounts = current_user.accounts.collect{ |a| a.id }
ActsAsTenant.without_tenant do
#conversations = Conversation.where(account_id: user_accounts).updated_between(#start_date,#end_date)
#users = current_user.accounts.collect{|account| account.users}.flatten.uniq.to_a
unassigned = User.new(name: 'Un-assigned')
#users << unassigned
end
else
account = Account.find(params[:account])
ActsAsTenant.with_tenant(account) do
#conversations = account.conversations.updated_between(#start_date,#end_date)
#users = account.users.to_a
unassigned = User.new(name: 'Un-assigned')
#users << unassigned
end
end
else
render json: {error: 'Invalid parameters'}, status: 422
end
end
here is the image of the data I want to be able to download as csv
https://i.stack.imgur.com/VI6lo.png
here is the leaderboard.json.jbuider
json.users #users do |user|
json.(user,:id, :name)
json.closed_count #conversations.where(user: user).closed.count
json.open_count #conversations.where(user: user).open.count
json.replies #conversations.where(user: user).inject(0){|sum, conversation| sum + conversation.messages.where(updated_at: #start_date..#end_date).where(direction: 'OUT').count}
json.resolution_time formatted_duration(array_mean(#conversations.closed.where(user: user).closed.collect{ |c| c.resolution_time }))
json.first_response Conversation.first_response(#conversations.where(user: user))
end
json.total_closed #conversations.closed.count
json.total_open #conversations.open.count
json.total_replies #conversations.inject(0){|sum, conversation| sum + conversation.messages.where(updated_at: #start_date..#end_date).where(direction: 'OUT').count}
json.start_date #start_date.to_i * 1000
json.end_date #end_date.to_i * 1000
How do I go about downloading it into a csv?
From your code snippet I'm not sure what you're trying to "export", but in general it should look like:
csv_lines = []
#some_data.each do |record|
csv_lines << "#{record.field1};#{record.field2};#{record.field3}" #and so on
end
#export csv_lines
txt = csv_lines.join "\n"

Filter created_at with 3 functions by parsing DateTime rails

currently i am getting values from database with a query
created_ats = Snapshot.connection.select_all("SELECT created_at from snapshots WHERE snapshot_id >= '#{camera_id}_#{from_date}' AND snapshot_id <= '#{camera_id}_#{to_date}'")
This query is giving me all created_ats according to conditions.
i want to filter all these created_ats according to some inputs which users have been passed to database which are
days and times
"{"Monday":["3:0-7:0","15:0-17:30"],"Tuesday":[],"Wednesday":["11:0-16:0"],"Thursday":["5:0-10:0"],"Friday":["15:30-22:30"],"Saturday":[],"Sunday":[]}"
its just an example days and times are can be totally filled or partially.
there is a field named as INTERVAL. which is being considered as MINUTES.
Now what is the whole scenario, from query i am getting created_at which is general timestamps in ruby on rails.
i want to filter those created_at with the giving Days and timings provided along with everyday in info above. + adding X(interval) to that created at as well.
such as after refining between days and timings. we will have a list of created_ats so after that starting from first created_at will add x.minutes in that created at and will assure that (created_at+x.minutes) is also present in created_ats(which we got after days and hours refining), is YESS then save it else leave it.
what i have tried so far is this and its not working as i want it to work according to me it seems no error in that but still dont give me specific value
class SnapshotExtractor < ActiveRecord::Base
establish_connection "evercam_db_#{Rails.env}".to_sym
belongs_to :camera
require "rmega"
require "aws-sdk-v1"
require 'open-uri'
def self.connect_mega
storage = Rmega.login("#{ENV['MEGA_EMAIL']}", "#{ENV['MEGA_PASSWORD']}")
storage
end
def self.connect_bucket
access_key_id = "#{ENV['AWS_ACCESS_KEY']}"
secret_access_key = "#{ENV['AWS_SECRET_KEY']}"
s3 = AWS::S3.new(
access_key_id: access_key_id,
secret_access_key: secret_access_key,
)
bucket = s3.buckets["evercam-camera-assets"]
bucket
end
# def self.test
# snapshot_bucket = connect_bucket
# storage = connect_mega
# folder = storage.root.create_folder("dongi")
# s3_object = snapshot_bucket.objects["gpo-cam/snapshots/1452136326.jpg"]
# snap_url = s3_object.url_for(:get, {expires: 1.years.from_now, secure: true}).to_s
# File.open("formula.txt", 'w') { |file| file.write(snap_url) }
# open('image.jpg', 'wb') do |file|
# file << open(snap_url).read
# end
# folder.upload("image.jpg")
# end
def self.extract_snapshots
running = SnapshotExtractor.where(status: 1).any?
unless running
#snapshot_request = SnapshotExtractor.where(status: 0).first
#snapshot_request.update_attribute(:status, 1)
camera_id = #snapshot_request.camera_id
exid = Camera.find(camera_id).exid
mega_id = #snapshot_request.id
from_date = #snapshot_request.from_date.strftime("%Y%m%d")
to_date = #snapshot_request.to_date.strftime("%Y%m%d")
interval = #snapshot_request.interval
#days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
set_days = []
set_timings = []
index = 0
#days.each do |day|
if #snapshot_request.schedule[day].present?
set_days[index] = day
set_timings[index] = #snapshot_request.schedule[day]
index += 1
end
end
begin
created_ats = Snapshot.connection.select_all("SELECT created_at from snapshots WHERE snapshot_id >= '#{camera_id}_#{from_date}' AND snapshot_id <= '#{camera_id}_#{to_date}'")
created_at_spdays = refine_days(created_ats, set_days)
created_at_sptime = refine_times(created_at_spdays, set_timings, set_days)
created_at = refine_intervals(created_at_sptime, interval)
File.open("test.txt", 'w') { |file| file.write(created_at) }
storage = connect_mega
creatp = storage.root.create_folder("created_at")
creatp.upload("test.txt")
rescue => error
notify_airbrake(error)
end
begin
storage = connect_mega
snapshot_bucket = connect_bucket
new_folder = storage.root.create_folder("#{exid}")
folder = storage.nodes.find do |node|
node.type == :folder and node.name == "#{exid}"
end
folder.create_folder("#{mega_id}")
created_at.each do |snap|
snap_i = DateTime.parse(snap).to_i
s3_object = snapshot_bucket.objects["#{exid}/snapshots/#{snap_i}.jpg"]
if s3_object.exists?
snap_url = s3_object.url_for(:get, {expires: 1.years.from_now, secure: true}).to_s
File.open("formula_#{snap_i}.txt", 'w') { |file| file.write(snap_url) }
open('#{snap_i}.jpg', 'wb') do |file|
file << open(snap_url).read
end
folder.upload('#{snap_i}.jpg')
end
end
#snapshot_request.update_attribute(:status, 3)
rescue => error
error
end
end
# created_at
end
private
def self.refine_days(created_ats, days)
created_at = []
index = 0
created_ats.each do |single|
days.each do |day|
if day == Date.parse(single["created_at"]).strftime("%A")
created_at[index] = single["created_at"]
index += 1
end
end
end
created_at
end
def self.refine_times(created_ats, timings, days)
created_at = []
index = 0
day_index = 0
days_times = days.zip(timings.flatten)
one = 1
zero = 0
created_ats.each do |single|
days_times.each do |day_time|
if Date.parse(single).strftime("%A") == day_time[day_index]
start_time = DateTime.parse(day_time[one].split("-")[zero]).strftime("%H:%M")
end_time = DateTime.parse(day_time[one].split("-")[one]).strftime("%H:%M")
created_at_time = DateTime.parse(single).strftime("%H:%M")
if created_at_time >= start_time && created_at_time <= end_time
created_at[index] = single
index += 1
end
end
end
end
created_at
end
def self.refine_intervals(created_ats, interval)
created_at = [created_ats.first]
last_created_at = DateTime.parse(created_ats.last)
index = 1
index_for_dt = 0
length = created_ats.length
(1..length).each do |single|
if (DateTime.parse(created_at[index_for_dt]) + interval.minutes) <= last_created_at
temp = DateTime.parse(created_at[index_for_dt]) + interval.minutes
created_at[index] = temp.to_s
index_for_dt += 1
index += 1
end
end
created_at
end
end

custom validation gives blank field after validation which is not right

I have a custom validation in my model like this:
class Appointment < ActiveRecord::Base
#VIRTUAL ATTRIBUTES
attr_accessor :start_date, :start_time, :duration
#RELATIONSHIPS
belongs_to :task
#VALIDATIONS
before_validation :convert_to_datetime
before_validation :dur
validates :duration, presence: true
validate :is_date_nil
validate :time_collision_validation, if: :is_appointments_not_empty
validate :check_time
after_save :save_start_date
def is_appointments_not_empty
Appointment.all.present?
end
def check_time
start_at = Time.parse("#{#start_date} #{#start_time}")
if start_at < Time.now
errors.add(:start_date, "Cannot input past times")
end
end
def convert_to_datetime
unless #start_date.blank? && #start_time.blank?
self.start_at = Time.parse("#{#start_date} #{#start_time}")
end
end
def dur
if #start_date.present? && #start_time.present? && #duration.present?
self.end_at = Time.parse("#{#start_date} #{#start_time}") + (#duration.to_f*60*60)
end
end
def time_collision_validation
appointments = Appointment.all
if #start_date.present? && #start_time.present? && duration == 0.to_s
start_at = Time.parse("#{#start_date} #{#start_time}")
end_at = Time.parse("#{#start_date} #{#start_time}") + (#duration.to_f*60*60)
appointments.each do |a|
if start_at <= a.end_at - (2*60*60) && start_at >= a.start_at - (1*60*60)
errors.add(:start_time)
errors.add(:start_date, "An appointment already
exists at #{a.start_at.strftime("%I:%M%p")} of #{a.start_at.strftime("%d/%m/%Y")}
to #{a.end_at.strftime("%I:%M%p")} of #{a.end_at.strftime("%d/%m/%Y")}.
Please select a different date or time.")
break
end
end
elsif #start_date.present? && #start_time.present? && duration.present?
start_at = Time.parse("#{#start_date} #{#start_time}")
end_at = Time.parse("#{#start_date} #{#start_time}") + (#duration.to_f*60*60)
appointments.each do |a|
if start_at <= a.end_at - (2*60*60) && a.start_at <= end_at
errors.add(:start_time)
errors.add(:start_date, "An appointment already
exists at #{a.start_at.strftime("%I:%M%p")} of #{a.start_at.strftime("%d/%m/%Y")}
to #{a.end_at.strftime("%I:%M%p")} of #{a.end_at.strftime("%d/%m/%Y")}.
Please select a different date or time.")
break
end
end
end
end
def is_date_nil
if #start_date.blank? && #start_time.blank?
errors.add(:start_date, "Start date can't be blank")
errors.add(:start_time, "Start time can't be blank")
end
if #start_date.blank? && #start_time.present?
errors.add(:start_date, "Start date can't be blank")
end
if #start_time.blank? && #start_date.present?
errors.add(:start_time, "Start time can't be blank")
end
end
def start_date=(date)
#start_date = Date.strptime(date, "%d/%m/%Y") if date.present?
end
# def save_start_date
# #start_date = Date.strptime(#start_date, "%d/%m/%Y") if #start_date.present?
# end
# def save_start_date
# #start_date = Date.parse(#start_date).strftime("%d/%m/%Y")if #start_date.present?
# end
def start_time=(time)
#start_time = Time.parse(time).strftime("%H:%M:%S") if time.present?
end
def duration=(duration)
#duration = duration if duration.present?
end
# def start_date
# #start_date.strftime("%d/%m/%Y") if start_at.present? # || start_at.strftime("%d/%m/%Y") if start_at.present?
# end
def start_date
unless #start_date.blank?
#start_date.strftime("%d/%m/%Y")
end
# start_at.strftime("%d/%m/%Y") if start_at.present?
end
def start_time
#start_time || start_at.strftime("%I:%M%p") if start_at.present?
end
# def duration
# #duration || 9
# end
end
After this time_collision_validation executes, the value fields are blank which I don't want because I'm concerned with UX. ie: start_date and start_time fields are blank.
When I checked the value attribute in input in the HTML source code, the value contains a date string. I wonder why it does not show in the field.
Can somebody help me with this and explain what is going on please? >.<
validate :time_collision_validation
def time_collision_validation
appointments = Appointment.all
if self.start_date.present? && self.start_time.present? && duration.present?
start_at = Time.parse("#{self.start_date} #{self.start_time}")
end_at = Time.parse("#{self.start_date} #{self.start_time}") + (self.duration.to_f.hours)
appointments.each do |appointment|
if duration == 0.to_s
duration_ok = start_at >= appointment.start_at - (1.hours)
else
duration_ok = appointment.start_at <= end_at
end
if start_at <= appointment.end_at - (2.hours) && duration_ok
errors.add(:start_time)
errors.add(:start_date, "An appointment already
exists at #{appointment.start_at.strftime("%I:%M%p")} of #{appointment.start_at.strftime("%d/%m/%Y")}
to #{appointment.end_at.strftime("%I:%M%p")} of #{appointment.end_at.strftime("%d/%m/%Y")}.
Please select a different date or time.")
break
end
end
end
end
Notes:
you variously refer to duration and self.duration. For readability i would always use self.duration rather than duration or self.duration as it makes it clear to the reader that you are talking about a method/field of the current object rather than a local variable. I've change all instances of referencing methods/fields of the current object to self.methodname
you had a lot of repetition shared between the two if cases. I've refactored these to avoid repetition.
2*60*60 is like saying 2.hours and the latter is much more readable.
what are start_date and start_time - are they strings or date/time objects?
rather than loading every appointment in your entire database, and cycling through them, it would be much more efficient to just search for a single colliding other appointment. if you find one then you can add the errors. I was tempted to do this here but it's not clear exactly what's going on with your database.

Aggregate an array of Merchant store hours that overlap to produce a single range

I have a list of store hours from a merchant that could potentially overlap stored in AvailableHours model. The model has a start_time and an end_time stored as a TimeOfDay object (https://github.com/JackC/tod).
I want to iterate through the list and compare the store hours so that I can just display 1 store hour. For example, if a store could had hours of Mon: 9am-2pm and Mon: 10am-5pm, I would want to display Mon: 9am-5pm. A store could also have two times in a single day, i.e. Fri: 9-5pm, 7pm-10pm. I have to do this for each day of the week.
The issue I am running into is that my code is becoming hard to manage - I started introducing a lot of nested if/else for manual checking (what a nightmare!), but my question is this:
Is there a way to use ActiveRecord to produce a hash with merged time ranges?
def ranges_overlap?(shift, current_shift)
shift.include?(current_shift.beginning) || current_shift.include?(shift.beginning)
end
def merge_ranges(shift, current_shift)
new_open_time = [shift.beginning, current_shift.beginning].min
new_close_time = [shift.ending, current_shift.ending].max
Shift.new(new_open_time, new_close_time)
end
def get_aggregate_merchant_hours
merchant = self
day = nil
shifts_array = []
output_shift_array = []
merchant_shifts = merchant_shifts = merchant.available_hours.order(day: :asc, open_time: :desc).map do |ah|
merged_shift = nil
show_output = false
# if it's a new day
if day.blank? || day != ah.day
output_shift_array = shifts_array
shifts_array = []
show_output = true if !day.blank?
output_day = day
day = ah.day
end
next if ah.open_time.blank? || ah.close_time.blank?
open_time = ah.open_time
close_time = ah.close_time
current_shift = Shift.new(open_time, close_time)
if !shifts_array.blank?
#compare and merge
shifts_array.each do |shift|
merged_shift = merge_ranges(shift, current_shift) if ranges_overlap?(shift, current_shift)
end
end
#replace old shift with merged shift
if merged_shift
delete_shift = shifts_array.find(beginning: current_shift.beginning, ending: current_shift.ending).first
shifts_array.delete(delete_shift) if delete_shift
shifts_array.push(merged_shift)
else
shifts_array.push(current_shift)
end
if show_output
store_hours_string = ""
if output_shift_array.blank?
store_hours_string = "Closed"
else
output_shift_array.each do |shift|
shift.beginning.strftime("%I:%M%p")
shift.ending.strftime("%I:%M%p")
if store_hours_string.blank?
store_hours_string << "#{shift.beginning.strftime("%I:%M%p").downcase} - #{shift.ending.strftime("%I:%M%p").downcase}"
else
store_hours_string << ", #{shift.beginning.strftime("%I:%M%p").downcase} - #{shift.ending.strftime("%I:%M%p").downcase}"
end
end
end
"#{Date::DAYNAMES[output_day][0..2]}: #{store_hours_string}"
end
end
#output last shift
store_hours_string = ""
if output_shift_array.blank?
store_hours_string = "Closed"
else
output_shift_array.each do |shift|
shift.beginning.strftime("%I:%M%p")
shift.ending.strftime("%I:%M%p")
if store_hours_string.blank?
store_hours_string << "#{shift.beginning.strftime("%I:%M%p").downcase} - #{shift.ending.strftime("%I:%M%p").downcase}"
else
store_hours_string << ", #{shift.beginning.strftime("%I:%M%p").downcase} - #{shift.ending.strftime("%I:%M%p").downcase}"
end
end
end
merchant_shifts << "#{Date::DAYNAMES[day][0..2]}: #{store_hours_string}"
merchant_shifts.compact
end

Rails - Fetch results on the basis of number of params in query string

I am working on an events application where i want to filter events depending on the 3 parameters location or starts_at or ends_at in the query string. There can be any one, two or all the parameters in the query string. In i use if-else statement i need to make 6 cases which will make my code clumsy. Rather i am thinking to implement something this way:
class EventsController < ApplicationController
def index
unless params.empty?
unless params[:location].nil?
#events = Event.where("location = ?", params[:location])
end
unless params[:starts_at].nil?
unless #events.empty?
#events = #events.where("start_date = ?", params[:start_date])
else
#events = Event.where("Date(starts_at) = Date(?)", params[:starts_at])
end
end
unless params[:ends_at].nil?
unless #events.empty?
#events = #events.where("end_date = ?", params[:end_date])
else
#events = Event.where("Date(ends_at) = Date(?)", params[:ends_at])
end
end
end
end
end
But this code doesnt work since where query doen not work on an array. Can someone suggest me some solution for this..
You should be able to pass your params hash directly to where, and it will form the correct SQL based on the keys and values of that hash:
Event.where(params)
An example in the console:
1.9.3p194 :001 > puts Example.where(:location => 'here', :started_at => '2012-08-13').to_sql
SELECT "examples".* FROM "examples" WHERE "examples"."location" = 'here' AND "examples"."started_at" = '2012-08-13'
Try Following
def index
unless params.empty?
where_array, arr = [], []
if params[:location]
where_array << "location = ?"
arr << params[:location]
end
if params[:starts_at]
where_array << "start_date = ?"
arr << params[:starts_at]
end
if params[:ends_at]
where_array << "end_date = ?"
arr << params[:ends_at]
end
#events = arr.blank? ? [] : Event.where([where_array.join(" AND "), *arr])
end
end

Resources