Using Whenever with Rails 4 - Modify attributes from a model - ruby-on-rails

I got a problem. I use whenever to run daily a method in a model class (of course). This method iterates over a collection of "holidays" instances and then notifies the existence of this holiday to a client (another model).
Here comes the problem; the holidays instances have an attribute called "notified". I use this to know if that holidays was notified to the clients (I don't want to notify a holiday twice). In order to do that, i need to access to the attributes of the holiday instance to chango the value (boolean) of the attribute "notified". But I can not do that (i get no errors, but the attribute doesn't get updated -always false-)
Any hints?
holidays.rb
class Holiday < ActiveRecord::Base
belongs_to :user
def self.notify
#holidays = Holiday.all
#today = DateTime.now.to_date
#holidays.each do |holiday|
if holiday.notified == false && (holiday.fecha - #today).to_i < 15
#clients = holiday.user.clients
#clients.each do |client|
ReminderMailer.new_holiday_reminder(holiday, client).deliver
end
holiday.notified = true <== I need to include something like this
end
end
end
end
And the scheduler.rb
every :day do
runner "Holiday.notify", :environment => 'development'
end
THANKS!!

Use update_attribute. Also, You don't have to use instance variables in your notify method
class Holiday < ActiveRecord::Base
belongs_to :user
def self.notify
holidays = Holiday.all
today = DateTime.now.to_date
holidays.each do |holiday|
if holiday.notified == false && (holiday.fecha - today).to_i < 15
clients = holiday.user.clients
clients.each do |client|
ReminderMailer.new_holiday_reminder(holiday, client).deliver
end
holiday.update_attribute(:notified, true) #or holiday.update_attributes(:notified => true)
end
end
end
end

Related

How to update a column using active record concurrently?

Here're my models - User, UserDevice, Device.
The tables 've the following columns:
users - id, name, email, phone, etc.
devices - id, imei, model etc.
user_devices - id, user_id, device_id etc.
Now I added a new column to devices trk, and want to update it to 1 for a huge number of users.
Here're how my models look.
class User < ActiveRecord::Base
has_many :user_devices
has_one :some_model
# ....
end
class Device < ActiveRecord::Base
has_many :user, through: :user_devices
attr_accessor :device_user_phone
# ....
# some callbacks which depend on on trk & device_user_phone
end
class UserDevice < ActiveRecord::Base
belongs_to :device
belongs_to :user
# ....
end
The list of user ids are provided in csv file like this:
1234
1235
1236
....
This's what I tried so far, but it updates one after the other and is taking a lot of time. Also I can't use update_all, as I want the call_backs to be triggered.
def update_user_trk(user_id)
begin
user = User.find_by(id: user_id)
user_devices = user.user_devices
user_devices.each do |user_device|
user_device.device.update(trk: 1, device_user_col: user.some_model.col) if user_device.device.trk.nil?
end
rescue StandardError => e
Rails.logger.error("ERROR_FOR_USER_#{user_id}::#{e}")
end
end
def read_and_update(file_path)
start_time = Time.now.to_i
unless File.file? file_path
Rails.logger.error("File not found")
end
CSV.foreach(file_path, :headers => false) do |row|
update_user_trk(row[0])
end
end_time = Time.now.to_i
return end_time-start_time
end
Since this updates one row after another in a sequence, it's taking a lot of time and I was wondering if this can be done concurrently to speed it up.
Ruby version : ruby-2.2.3
rails version : Rails 4.2.10
This is at least slightly quicker, this way you're not creating and committing a new transaction on every update.
user = User.find_by(id: user_id)
user.user_devices.each do |user_device|
if user_device.device.trk.nil?
user_device.device.trk = 1
user_device.device_user_col = user.some_model.col
end
end
user.save

after_save callback of act_as_nested_set model lead to SystemStackError

I am using Rails 5.1.6
I have a model called Taxon using acts_as_nested_set. I have 4 levels of Taxons, the last level sub_category has an attribute holding names of all parents, I want to update the sub_category attribute every time any of its parents name is changed, when using after_save callback it runs into SystemStackError as each after save callback is run for each child leading to infinite loop. Any idea how to overcome this issue?
class Taxon
acts_as_nested_set dependent: :destroy
def update_tree_name
if shop_sub_category?
update(display_tree_name: beautiful_name)
else
related_sub_categories = tree_list.select{ |taxon| taxon.kind == "sub_category" }
related_sub_categories.each do |t|
t.update(display_tree_name: t.beautiful_name)
end
end
end
def beautiful_name
"#{parent.parent.parent.name} -> #{parent.parent.name} -> #{parent.name}-> #{name}"
end
I have a solution that will work for you but I do not think it is an elegant one but here you go and then you can fine tune it:
In your model:
class Taxon < ActiveRecord::Base
cattr_accessor :skip_callbacks
after_save :update_tree_name, :unless => :skip_callbacks
end
def update_tree_name
if shop_sub_category?
update(display_tree_name: beautiful_name)
else
related_sub_categories = tree_list.select{ |taxon| taxon.kind == "sub_category" }
Taxon.skip_callbacks = true # disable the after_save callback so that you do not end up in infinite loop (stackoverflow)
related_sub_categories.each do |t|
t.update(display_tree_name: t.beautiful_name)
end
Taxon.skip_callbacks = false # enable callbacks again when you finish
end
end

How to check if associated model has entries in Rails 5?

I have a model RegularOpeningHour(dayOfWeek: integer) that is associated to a model OpeningTime(opens: time, closes: time). RegularOpeningHour has an 1:n relation to OpeningTime, so that a specific day can have many opening times.
(I know that I simply could have one entry with 'opens' and 'closes' included in RegularOpeningHour but for other reasons I need this splitting)
Now I want a open?-Method, that returns whether the business is opened or not. I tried the following in my model file regular_opening_hour.rb:
def open?
RegularOpeningHour.where(dayOfWeek: Time.zone.now.wday).any? { |opening_hour| opening_hour.opening_times.where('? BETWEEN opens AND closes', Time.zone.now).any? }
end
Unforutnately, that doesn't work. Any ideas to solve this?
How about this:
def open?
joins(:opening_times)
.where(dayOfWeek: Time.current.wday)
.where("opens <= :time AND closes >= :time", time: Time.current)
.any?
end
EDIT: Missing ':' in the join
You could create some scopes to make selecting open OpeningTimes and open RegularOpeningHours less clunky. This makes creating the given selection much easier.
class OpeningTime < ApplicationRecord
# ...
belongs_to :regular_opening_hour
def self.open
time = Time.current
where(arel_table[:opens].lteq(time).and(arel_table[:closes].gteq(time)))
end
# ...
end
class RegularOpeningHour < ApplicationRecord
# ...
has_many :opening_times
def self.open
where(
dayOfWeek: Time.current.wday,
id: OpeningTime.select(:regular_opening_hour_id).open,
)
end
# ...
end
def open?
RegularOpeningHour.open.any?
end
Since you have has_many association of RegularOpeningHour to OpeningTime you can use join query like below.:
RegularOpeningHour.joins(:opening_times).where(dayOfWeek: Time.zone.now.wday).where('? BETWEEN opening_times.opens AND opening_times.closes', Time.zone.now).any?

Setting Time Zone Per Record

Any idea how I can use the time zone generated from a location to create a new record with that respective zipcode?
I have created a service object to help pull the zipcode information.
I am able to use this info in the terminal to set the zip code but it doesn't work when I try a before_save or before_create hook.
class ServiceObject
include ActiveModel::Model
def self.get_timezone_name(location)
z = GoogleTimeZone.fetch(location.latitude, location.longitude)
ActiveSupport::TimeZone.find_tzinfo(z.time_zone_id)
end
end
class Location < ActiveRecord::Base
has_many :events
#name - String
#start_time - DateTime
#end_time - DateTime
end
class Event < ActiveRecord::Base
belongs_to :location
geocoded_by :full_address
before_create :zoned
private
def zoned
Time.zone = ServiceObject.get_time_zone(self.location)
end
end
I also tried to use the date time attribute gem to set an event's time zone. Again this works in the console but not with a call back. Records are not created via a browser but rather in the console.
Here is a blog post I wrote on timezones with rails: http://jessehouse.com/blog/2013/11/15/working-with-timezones-and-ruby-on-rails/
There are two different ways to accomplish what you want:
Time.use_zone block when saving data
use in_time_zone when displaying data
I recommend you save the timezone on your location, update it if the long/lat changes; it looks like your Event and Location example above is flipped? Event should have a start and end, not location?
class Location
has_many :events
geocoded_by :full_address
before_save :set_time_zone
private
def set_time_zone
if new_record? || latitude_changed? || longitude_changed?
self.time_zone = ServiceObject.get_time_zone(self)
end
end
end
class Event
belongs_to :location
end
then in the console or controller code
location = Location.last
Time.use_zone(location.time_zone) do
location.events << Event.new({ ... })
end

How to update instance variable in Rails model?

In my Rails app I have users who can have many payments.
class User < ActiveRecord::Base
has_many :invoices
has_many :payments
def year_ranges
...
end
def quarter_ranges
...
end
def month_ranges
...
end
def revenue_between(range, kind)
payments.sum_within_range(range, kind)
end
end
class Invoice < ActiveRecord::Base
belongs_to :user
has_many :items
has_many :payments
...
end
class Payment < ActiveRecord::Base
belongs_to :user
belongs_to :invoice
def net_amount
invoice.subtotal * percent_of_invoice_total / 100
end
def taxable_amount
invoice.total_tax * percent_of_invoice_total / 100
end
def gross_amount
invoice.total * percent_of_invoice_total / 100
end
def self.chart_data(ranges, unit)
ranges.map do |r| {
:range => range_label(r, unit),
:gross_revenue => sum_within_range(r, :gross),
:taxable_revenue => sum_within_range(r, :taxable),
:net_revenue => sum_within_range(r, :net) }
end
end
def self.sum_within_range(range, kind)
#sum ||= includes(:invoice => :items)
#sum.select { |x| range.cover? x.date }.sum(&:"#{kind}_amount")
end
end
In my dashboard view I am listing the total payments for the ranges depending on the GET parameter that the user picked. The user can pick either years, quarters, or months.
class DashboardController < ApplicationController
def show
if %w[year quarter month].include?(params[:by])
#unit = params[:by]
else
#unit = 'year'
end
#ranges = #user.send("#{#unit}_ranges")
#paginated_ranges = #ranges.paginate(:page => params[:page], :per_page => 10)
#title = "All your payments"
end
end
The use of the instance variable (#sum) greatly reduced the number of SQL queries here because the database won't get hit for the same queries over and over again.
The problem is, however, that when a user creates, deletes or changes one of his payments, this is not reflected in the #sum instance variable. So how can I reset it? Or is there a better solution to this?
Thanks for any help.
This is incidental to your question, but don't use #select with a block.
What you're doing is selecting all payments, and then filtering the relation as an array. Use Arel to overcome this :
scope :within_range, ->(range){ where date: range }
This will build an SQL BETWEEN statement. Using #sum on the resulting relation will build an SQL SUM() statement, which is probably more efficient than loading all the records.
Instead of storing the association as an instance variable of the Class Payment, store it as an instance variable of a user (I know it sounds confusing, I have tried to explain below)
class User < ActiveRecord::Base
has_many :payments
def revenue_between(range)
#payments_with_invoices ||= payments.includes(:invoice => :items).all
# #payments_with_invoices is an array now so cannot use Payment's class method on it
#payments_with_invoices.select { |x| range.cover? x.date }.sum(&:total)
end
end
When you defined #sum in a class method (class methods are denoted by self.) it became an instance variable of Class Payment. That means you can potentially access it as Payment.sum. So this has nothing to do with a particular user and his/her payments. #sum is now an attribute of the class Payment and Rails would cache it the same way it caches the method definitions of a class.
Once #sum is initialized, it will stay the same, as you noticed, even after user creates new payment or if a different user logs in for that matter! It will change when the app is restarted.
However, if you define #payments_with_invoiceslike I show above, it becomes an attribute of a particular instance of User or in other words instance level instance variable. That means you can potentially access it as some_user.payments_with_invoices. Since an app can have many users these are not persisted in Rails memory across requests. So whenever the user instance changes its attributes are loaded again.
So if the user creates more payments the #payments_with_invoices variable would be refreshed since the user instance is re-initialized.
Maybe you could do it with observers:
# payment.rb
def self.cached_sum(force=false)
if #sum.blank? || force
#sum = includes(:invoice => :items)
end
#sum
end
def self.sum_within_range(range)
#sum = cached_sum
#sum.select { |x| range.cover? x.date }.sum(&total)
end
#payment_observer.rb
class PaymentObserver < ActiveRecord::Observer
# force #sum updating
def after_save(comment)
Payment.cached_sum(true)
end
def after_destroy(comment)
Payment.cached_sum(true)
end
end
You could find more about observers at http://apidock.com/rails/v3.2.13/ActiveRecord/Observer
Well your #sum is basically a cache of the values you need. Like any cache, you need to invalidate it if something happens to the values involved.
You could use after_save or after_create filters to call a function which sets #sum = nil. It may also be useful to also save the range your cache is covering and decide the invalidation by the date of the new or changed payment.
class Payment < ActiveRecord::Base
belongs_to :user
after_save :invalidate_cache
def self.sum_within_range(range)
#cached_range = range
#sum ||= includes(:invoice => :items)
#sum.select { |x| range.cover? x.date }.sum(&total)
end
def self.invalidate_cache
#sum = nil if #cached_range.includes?(payment_date)
end

Resources