How to use the callback after_destroy (or something similar) in Rails 4? - ruby-on-rails

I'm creating an application that creates polls, each poll has many poll pages, and each poll page has many question clusters, what I want to do is that when a question cluster is deleted, search every question clusters from the same page that had a higher position, and diminish 1.
This is what I tried, but it doesn't even runs:
after_destroy :reassign_position
private
def reassign_position
question_clusters = QuestionCluster.where(poll_page_id: self.poll_page_id)
question_clusters.where("position > ?", self.position)
quest_cluster.each do |question_cluster|
question_cluster.position -= 1
end
end
How can I accomplish what I want?

You are not updating the question_cluster's attribute (position). Take a look:
def reassign_position
question_clusters = QuestionCluster.where(poll_page_id: self.poll_page_id)
question_clusters.where("position > ?", self.position)
quest_cluster.each do |question_cluster|
# actually update the question_cluster
question_cluster.update!(position: question_cluster.position - 1) # <========
end
end

Related

Rails Query a List for a CRON Job

I'm a complete novice with CRON jobs but I think I have that set up correctly.
Ultimately what I'm trying to do is send an email every day at 8:00 am to users (and a couple others) that have not logged in within the last 3 days, have not received the email, AND are marked as active OR temp as a status.
So from querying the db in console I know that I can do:
first = User.where(status: 'active').or(User.where(status: 'temp'))
second = first.where("last_login_at < ? ", Time.now-3.days)
third = second.where(notified: false)
That's not certainly clean but I was struggling with finding a contained query that grabbed all that data. Is there a cleaner way to do this query?
I believe I have my cron job set up correctly using a runner. I have whenever installed and in my schedule.rb I have:
every 1.day, at: '8:00 am' do
runner 'ReminderMailer.agent_mailer.deliver'
end
So under app > mailer I created ReminderMailer
class ReminderMailer < ApplicationMailer
helper ReminderHelper
def agent_reminder(user)
#user = user
mail(to: email_recipients(user), subject: 'This is your reminder')
end
def email_recipients(agent)
email_address = ''
email_addresses += agent.notification_emails + ',' if agent.notification_emails
email_addresses += agent.manager
email_address += agent.email
end
end
Where I'm actually struggling is where I should put my queries to send to the mailer, which is why I built a ReminderHelper.
module ReminderHelper
def applicable_agents(user)
agent = []
first = User.where(status: 'active').or(User.where(status: 'temp'))
second = first.where("last_login_at < ? ", Time.now-3.days)
third = second.where(notified: false)
agent << third
return agent
end
end
EDIT: So I know I could in theory do a chain of where queries. There's gotta be a better way right?
So what I need help on is: do I have the right structure in place? Is there a cleaner way to query this data in ActiveRecord for the CRON job? Is there a way to test this?
Try combining them together as if understand the conditions correct
Have not logged in within the last 3 days,
Have not received the email
Are marked as active OR temp as a status
User.where("last_login_at < ? ", 3.days.ago).
where(notified: false).
where(status: ['active', temp])
module ReminderHelper
def applicable_agents(user)
User.where("last_login_at < ? ", 3.days.ago).
where(notified: false).
where(status: ['active', temp])
end
end
You don't need to add/ assign them to array. Because this relation is already like an array. You can use .to_a if you need array. If you just want to iterate over them then users.each should work fine.
Update
class User
scope :not_notified, -> { where(notified: false) }
scope :active_or_temp, -> { where(status: ['active', 'temmp']) }
scope :last_login_in, -> (default_days = 3) { where("last_login_at < ?", default_days.days.ago) }
end
and then use
User.not_notified.active_or_temp.last_login_in(3)
Instead of Time.now-3.days it's better to use 3.days.ago because it keeps time zone also in consideration and avoids unnecessary troubles and failing test cases.
Additionally you can create small small scopes and combine them. More read on scopes https://guides.rubyonrails.org/active_record_querying.html

Get next/previous record in Collection by Date in Rails (and handle multiple dates on the same day)

I'm having trouble trying to isolate the next/previous record in a collection. I'm self-taught and relatively new to Rails/coding.
I have a Goal class which has many GoalTasks.
GoalTask has taskduedate. I want to be able to cycle next/previous on the goal_tasks, based on their taskduedate.
The issue is that a task due date is just when the task is due to be completed, but it can be set at any time and may not be in sequential order so that I don't know what else to order it by to correctly cycle through it.
I have created an array of goal_tasks to identify which one is currently being viewed (e.g. Task: 3/20), so I could use that to go to the next one, I think there might be a solution here, but it feels wrong to handle it in the view/controller?
I've tried the below solution from stackoverflow, but it doesn't handle the fact that I have multiple goal_tasks due on the same day, if I click next it just goes to the next day that goal_tasks are due. e.g. if I have three tasks due today and I'm on the first one and click next, it will just skip over the other two for today.
I then tried to add the >= (displayed below) to try and pull the next task (including those on the same day), and I've tried to ignore the current task by doing where created_at is not the same as the current goal_task and where.not, but I haven't managed to successfully get it to cycle the way I want it to, and I imagine there's a better solution.
GoalTasksController:
def show
#all_tasks_ordered_due_date_desc = #goal.goal_tasks.order('taskduedate ASC', 'id ASC')
end
show.html.erb:
Task: <%= #all_tasks_ordered_due_date_desc.find_index(#goal_task) +1 %> /
<%= #goal.goal_tasks.count%>
GoalTask.rb
scope :next_task, lambda {|taskduedate| where('taskduedate >= ?', taskduedate).order('id ASC') }
scope :last_task, lambda {|taskduedate| where('taskduedate <= ?', taskduedate).order('id DESC') }
def next_goal_task
goal.goal_tasks.next_task(self.taskduedate).first
end
Thanks
I used the method found here: Rails 5: ActiveRecord collection index_by
Which meant adding a default scope and changing GoalTask.rb to:
default_scope { order('taskduedate ASC') }
def next_goal_task
index = goal.goal_tasks.index self
goal.goal_tasks[index + 1]
end
def last_goal_task
index = goal.goal_tasks.index self
goal.goal_tasks[index -1]
end

Search by availability/dates Ruby On Rails

I've built a RoR app and implemented a simple booking system. The user is able to look for a space and can book it per day or per hour.
Everything works well, but I would now like to make the user able to look for a space depending on its availability.
I want to user to be able to select a start/end date and a start/end time and to show only spaces that don't have any booking included in this period.
I am using pg search at the moment to look for a space by category and location, but I have no idea how to implement a search by date and time, as it uses a different logic.
I've tried to do it by hand by creating an array of bookings for each space so I could compare it with the params, but it sounded very complicated and not clean (and I started being stuck anyway, as making it available for one hour or several hours or several days makes it even more complicated)
Is there a gem that could do this for me? If I have to do it by hand, what's the best way to begin?
Thanks a lot
Just create an instance method available? which tests there are no bookings that overlap the from to range. You can use none? on the relationship.
class Space
has_many :bookings
def available?(from, to)
bookings.where('start_booking <= ? AND end_booking >= ?', to, from).none?
end
end
Taking some inspiration from the answer of SteveTurczyn. The following might give you some inspiration.
class Space < ApplicationRecord
# attributes: id
has_many :bookings
def self.available(period)
bookings = Booking.overlap(period)
where.not(id: bookings.select(:space_id))
end
def available?(period)
if bookings.loaded?
bookings.none? { |booking| booking.overlap?(period) }
else
bookings.overlap(period).none?
end
end
end
class Booking < ApplicationRecord
# attributes: id, space_id, start, end
belongs_to :space
def self.overlap(period)
period = FormatConverters.to_period(period)
# lteq = less than or equal to, gteq = greater than or equal to
# Other methods available on attributes can be found here:
# https://www.rubydoc.info/gems/arel/Arel/Attributes/Attribute
where(arel_table[:start].lteq(period.end).and(arel_table[:end].gteq(period.start)))
end
def overlap?(period)
period = FormatConverters.to_period(period)
self.start <= period.end && self.end >= period.start
end
module FormatConverters
module_function
def to_period(obj)
return obj if obj.respond_to?(:start) && obj.respond_to?(:end)
obj..obj
end
end
end
With the above implemented you can query a single space if it is available during a period:
from = Time.new(2019, 10, 1, 9, 30)
to = Time.new(2019, 10, 5, 17, 30)
period = from..to
space.available?(period) # true/false
You can get all spaces available:
spaces = Space.available(period) # all available spaces during the period
Note that class methods will also be available on the scope chain:
spaces = Space.scope_01.scope_02.available(period)
I've also added the overlap scope and overlap? helper to simplify creating the above helpers.
Since in my version Booking has a start and end attribute (similar to Range) you can also provide it to any methods accepting a period.
booking_01.overlap?(booking_02) # true/false
To retrieve all bookings that that overlap this very moment:
bookings = Booking.overlap(Time.now) # all bookings overlapping the period
Hope this gave you some inspiration. If you'd like to know how the overlap checking works I have to forward you to this question.
Note: This answer assumes that the provided period is valid. A.k.a. start <= end. If you for some reason provide Time.new(2019, 10, 1)..Time.new(2019, 9, 23) the results are going to be skewed.

Summing time by looking at an associated column- an easier way?

I have a Project model that has_many: Tasks each Task has_many :users, through: :userAssociations so one person can work on a task but in the case of a meeting or any collaboration many users can work on the same task.
Tasks have a column: t.integer "time" which allows users to state in minutes the time the task took.
In the Show method of the Project, I'd like to be able to sum the total time that the project has taken.
If not for the fact that a Task can have many users through the userAssociations model, I think this would be very easy: #project.tasks.sum(:time) but that won't double count the time if multiple users were working on the task.
Since originally posting this question I was able to get it working with this helper:
def project_time_spent
#timeSum = 0;
#tasks.each do |tsk|
#userTask = UserTask.where(task_id: tsk.id);
if #userTask.count(:id) > 1
#timeSum += tsk.time * #userTask.count(:id)
else
#timeSum += tsk.time
end
end
return #timeSum
end
But this doesn't feel very "Railsy". Do any Rails gurus recommend a cleaner approach? Am I missing out on an easy method?
You can do it a little easier than that since a task has many users. I think you can also use inject here.
def project_time_spent
#tasks.inject { |sum, tsk| sum + (tsk.time * tsk.users.count) }
end
You can include an instance method in your Project model:
def time_spent
self.tasks.inject { |sum, tsk| sum + (tsk.time * tsk.users.count) }
end
I wouldn't bother checking for count > 1. The savings getting rid of the multiply are minuscule.

How can I prevent many sidekiq jobs from exceeding the API calls limit

I am working on an Ruby On Rails application. We have many sidekiq workers that can process multiple jobs at a time. Each job will make calls to the Shopify API, the calls limit set by Shopify is 2 calls per second. I want to synchronize that, so that only two jobs can call the API in a given second.
The way I'm doing that right now, is like this:
# frozen_string_literal: true
class Synchronizer
attr_reader :shop_id, :queue_name, :limit, :wait_time
def initialize(shop_id:, queue_name:, limit: nil, wait_time: 1)
#shop_id = shop_id
#queue_name = queue_name.to_s
#limit = limit
#wait_time = wait_time
end
# This method should be called for each api call
def synchronize_api_call
raise "a block is required." unless block_given?
get_api_call
time_to_wait = calculate_time_to_wait
sleep(time_to_wait) unless Rails.env.test? || time_to_wait.zero?
yield
ensure
return_api_call
end
def set_api_calls
redis.del(api_calls_list)
redis.rpush(api_calls_list, calls_list)
end
private
def get_api_call
logger.log_message(synchronizer: 'Waiting for api call', color: :yellow)
#api_call_timestamp = redis.brpop(api_calls_list)[1].to_i
logger.log_message(synchronizer: 'Got api call.', color: :yellow)
end
def return_api_call
redis_timestamp = redis.time[0]
redis.rpush(api_calls_list, redis_timestamp)
ensure
redis.ltrim(api_calls_list, 0, limit - 1)
end
def last_call_timestamp
#api_call_timestamp
end
def calculate_time_to_wait
current_time = redis.time[0]
time_passed = current_time - last_call_timestamp.to_i
time_to_wait = wait_time - time_passed
time_to_wait > 0 ? time_to_wait : 0
end
def reset_api_calls
redis.multi do |r|
r.del(api_calls_list)
end
end
def calls_list
redis_timestamp = redis.time[0]
limit.times.map do |i|
redis_timestamp
end
end
def api_calls_list
#api_calls_list ||= "api-calls:shop:#{shop_id}:list"
end
def redis
Thread.current[:redis] ||= Redis.new(db: $redis_db_number)
end
end
the way I use it is like this
synchronizer = Synchronizer.new(shop_id: shop_id, queue_name: 'shopify_queue', limit: 2, wait_time: 1)
# this is called once the process started, i.e. it's not called by the jobs themselves but by the App from where the process is kicked off.
syncrhonizer.set_api_calls # this will populate the api_calls_list with 2 timestamps, those timestamps will be used to know when the last api call has been sent.
then when a job wants to make a call
syncrhonizer.synchronize_api_call do
# make the call
end
The problem
The problem with this is that if for some reason a job fails to return to the api_calls_list the api_call it took, that will make that job and the other jobs stuck for ever, or until we notice that and we call set_api_calls again. That problem won't affect that particular shop only, but also the other shops as well, because the sidekiq workers are shared between all the shops using our app. It happen sometimes that we don't notice that until a user calls us, and we find that it was stuck for many hours while it should be finished in a few minutes.
The Question
I just realised lately that Redis is not the best tool for shared locking. So I am asking, Is there any other good tool for this job?? If not in the Ruby world, I'd like to learn from others as well. I'm interested in the techniques as well as the tools. So every bit helps.
You may want to restructure your code and create a micro-service to process the API calls, which will use a local locking mechanism and force your workers to wait on the socket. It comes with the added complexity of maintaining the micro-service. But if you're in a hurry then Ent-Rate-Limiting looks cool too.

Resources