model method trigger boolean to true? - ruby-on-rails

How can I trigger method accomplished_challenge upon days_left_challenged == 0?
challege.rb
before_save :days_left_challenged_sets_deadline
# makes ongoing_challenge become past (before_save) THIS WORKS
def days_left_challenged_sets_deadline
if self.date_started != nil
if days_left_challenged <= 0
self.accomplished = true
self.deadline = self.date_started
end
end
end
# makes ongoing_challenge become past (whenever gem) THIS DOESN'T
def self.accomplished_challenge
self.all.each do |challenge|
if challenge.days_left_challenged <= 0
challenge.accomplished = true
challenge.deadline = self.date_started
end
end
end
# Counts down how many days left in days_challenged using committed
def days_left_challenged
self.days_challenged - ((date_started.to_date)..Date.yesterday).count do |date|
committed_wdays.include? date.wday
end + self.missed_days
end
Challenge.last
id: 1,
action: "Run",
committed: ["sun", "mon", "tue", "wed", "thu", "fri", "sat", ""],
date_started: Sat, 06 Feb 2016 00:00:00 EST -05:00,
deadline: nil,
accomplished: nil,
days_challenged: 10,
missed_days: 0,
I can't trigger it with a callback or validation I don't think since days_left_challenged can turn to 0 at any point in the life of a challenge.

I suggest you use a gem like Whenever to setup a cron job to run every day or so and do that checking for all Challenges. It would be something like:
every 1.day, :at => '0:01 am' do
runner "Challenge.accomplished_challenge"
end
And your accomplished_challenge must be a class method that checks all (or the one you choose using a filter) Challenges:
def self.accomplished_challenge
self.all.each do |challenge|
if challenge.days_left_challenged == 0
challenge.update_attributes(deadline: self.date_started, accomplished: true)
end
end
end
---- EDIT to work on Heroku ----
Create a task on /lib/tasks/scheduler.rake:
# /lib/tasks/scheduler.rake
desc "This task is called by the Heroku scheduler add-on"
task :check_accomplished_challenges => :environment do
puts "Checking accomplished challenges..."
Challenge.accomplished_challenge
puts "done."
end
Go to your heroku app Resources page and add 'Heroku Scheduler'. Open the scheduler and add the task:
rake check_accomplished_challenges
Set it to run every day.
More details: https://devcenter.heroku.com/articles/scheduler

Related

Rails 5 - Sidekiq worker shows job done but nothing happens

I'm using Sidekiq for delayed jobs with sidekiq-status and sidekiq-ent gems. I've created a worker which is reponsible to update minor status to false when user is adult and has minor: true. This worker should be fired every day at midnight ET. Like below:
#initializers/sidekiq.rb
config.periodic do |mgr|
# every day between midnight 0 5 * * *
mgr.register("0 5 * * *", MinorWorker)
end
#app/workers/minor_worker.rb
class MinorWorker
include Sidekiq::Worker
def perform
User.adults.where(minor: true).remove_minor_status
rescue => e
Rails.logger.error("Unable to update minor field. Exception: #{e.message} : #{e.backtrace.join('\n')}")
end
end
#models/user.rb
class User < ApplicationRecord
scope :adults, -> { where('date_of_birth <= ?', 18.years.ago) }
def self.remove_minor_status
update(minor: false)
end
end
No I want to check this on my local machine - to do so I'm using gem 'timecop' to timetravel:
#application.rb
config.time_zone = 'Eastern Time (US & Canada)'
#config/environments/development.rb
config.after_initialize do
t = Time.local(2021, 12, 21, 23, 59, 0)
Timecop.travel(t)
end
After firing up sidekiq by bundle exec sidekiq and bundle exec rails s I'm waiting a minute and I see that worker shows up:
2021-12-21T22:59:00.130Z 25711 TID-ovvzr9828 INFO: Managing 3 periodic jobs
2021-12-21T23:00:00.009Z 25711 TID-ovw69k4ao INFO: Enqueued periodic job SettlementWorker with JID ddab15264f81e0b417e7dd83 for 2021-12-22 00:00:00 +0100
2021-12-21T23:00:00.011Z 25711 TID-ovw69k4ao INFO: Enqueued periodic job MinorWorker with JID 0bcd6b76d6ee4ff9e7850b35 for 2021-12-22 00:00:00 +0100
But it didn't do anything, the user's minor status is still set to minor: true:
2.4.5 :002 > User.last.date_of_birth
=> Mon, 22 Dec 2003
2.4.5 :001 > User.last.minor
=> true
Did I miss something?
EDIT
I have to add that when I'm trying to call this worker on rails c everything works well. I've got even a RSpec test which also passes:
RSpec.describe MinorWorker, type: :worker do
subject(:perform) { described_class.new.perform }
context 'when User has minor status' do
let(:user1) { create(:user, minor: true) }
it 'removes minor status' do
expect { perform }.to change { user1.reload.minor }.from(true).to(false)
end
context 'when user is adult' do
let(:registrant2) { create(:registrant) }
it 'not change minor status' do
expect(registrant2.reload.minor).to eq(false)
end
end
end
end
Since this is the class method update won't work
def self.remove_minor_status
update(minor: false)
end
Make use of #update_all
def self.remove_minor_status
update_all(minor: false)
end
Also, I think it's best practice to have some test cases to ensure the working of the methods.
As of now you can try this method from rails console and verify if they actually work
test "update minor status" do
user = User.create(date_of_birth: 19.years.ago, minor: true)
User.adults.where(minor: true).remove_minor_status
assert_equal user.reload.minor, false
end
I think you need to either do update_all or update each record by itself, like this:
User.adults.where(minor: true).update_all(minor: false)
or
class MinorWorker
include Sidekiq::Worker
def perform
users = User.adults.where(minor: true)
users.each { |user| user.remove_minor_status }
rescue => e
Rails.logger.error("Unable to update minor field. Exception: #{e.message} : #{e.backtrace.join('\n')}")
end
end
You may also want to consider changing update to update! so it throws an error if failing to be caught by your rescue in the job:
def self.remove_minor_status
update!(minor: false)
end

How to call controller action from _Sidekiq Worker in Rails 5.2.4?

My RoR application triggers data treatment scripts (SAS technology) from Linux operating system. An execute method in the Scheduler::ProductionExecutionsController drives the interactions with the scripts.
Scheduler::ProductionExecutionsController
# POST /production_executions/1/execute
def execute
#production_execution = ProductionExecution.find(params[:id])
#production_execution.update_attributes(started_at: Time.now,
status_id: statuses.find { |x| x["code"] == "RUNNING" }.id)
--- Scripts interaction ---
#production_execution.update_attributes(ended_at: Time.now,
status_id: statuses.find { |x| x["code"] == "FINISHED" }.id,
source_records_count: global_input_count,
processed_count: global_output_count,
error_message: nil
)
respond_to do |format|
format.html { redirect_back fallback_location: #production_execution, notice: #msg }
format.js
end
This method is called using this syntax: link_to "Go!", execute_scheduler_production_execution_path(execution). The route is defined and it works as expected.
As some scripts can take over a minute to execute, I need to execute the scripts in dedicated jobs, and sometimes schedule them. So I installed Sidekiq and defined a Scheduler::ScriptWorker :
class Scheduler::ScriptWorker
include Sidekiq::Worker
sidekiq_options queue: :default, tags: ['script']
def perform(execution_id)
puts "Sidekiq job start"
puts execution_id
redirect_to execute_scheduler_production_execution_path(execution_id) and return
end
end
The executions are queued with Scheduler::ScriptWorker.perform_async(#execution.id)
Sidekiq operates correctly, but each time the worker is invoked, it raise the following error:
WARN: NoMethodError: undefined method `execute_scheduler_production_execution_path' for #<Scheduler::ScriptWorker:0x000000000b15ded8>
Is it the right way to do this, and how can I solve this issue?
Thanks for you help!
The short answer is: you can't
You can't perform a redirect from an asynchronous Sidekiq worker. When the user clicks "Go!", an HTTP request is made to your Rails web application. Rails hands the request off to a controller action, which in your case spawns an async sidekiq job. The controller action logic continues and Rails completes the HTTP request with an HTTP response, even if your sidekiq worker is still running. You cannot broadcast a message from a sidekiq worker back to the user who initiated the task
I'm sorry this does not resolve your issue. You'll need to take a different approach.
As stated by Sean Huber, converting a front-end job into a backend job requires a touch of architecture. Through refactoring, the experimental execute method was moved to a helper and renamed execute_ssh. A new run_once method in the controller fires the helper's execute_ssh method and reloads the page.
script_worker.rb
class Scheduler::ScriptWorker
include Sidekiq::Worker
include SchedulerExecutionHelper
include ParametersHelper
sidekiq_options queue: :default, tags: ['script'], retry: false
def perform(execution_id)
puts "Sidekiq job start"
puts execution_id
execute_ssh execution_id
end
end
scheduler_execution_helper.rb
def execute_ssh(execution_id)
puts "--- Switched to helper"
#production_execution = ProductionExecution.find(execution_id)
puts #production_execution.id
#production_execution.update_attributes(started_at: Time.now, status_id: statuses.find { |x| x["code"] == "RUNNING" }.id)
--- Scripts interaction ---
#production_execution.update_attributes(ended_at: Time.now,
status_id: statuses.find { |x| x["code"] == "FINISHED" }.id,
source_records_count: global_input_count,
processed_count: global_output_count,
error_message: nil
)
end
production_schedules_controller
def run_once
#job = #production_schedule.parent
#execution = #job.production_executions.build(playground_id: #job.playground_id,
production_job_id: #job.id,
environment_id: #production_schedule.environment_id,
owner_id: current_user.id,
status_id: options_for('Statuses', 'Scheduler').find { |x| x["code"] == "READY" }.id || 0)
if #execution.save
#job.production_events.where(production_execution_id: nil).each do |event|
execution_event = event.dup
execution_event.production_execution_id = #execution.id
execution_event.return_value = 0
execution_event.status_id = statuses.find { |x| x["code"] == "READY" }.id
execution_event.save
end
Scheduler::ScriptWorker.perform_async(#execution.id)
redirect_to scheduler_production_job_path(#job)
else
end
end
This way, the controller remains thin, the worker can be easily reused, and the logic is in the helper module.

How to override Date.today when running rails tests?

I have some tests that were failing on specific dates, because someone wrote them to use Date.today. I want to reproduce the failures on previous select dates.
Is there a way to run rake test with an ENV variable that will override the system clock? So that calls to Date.today, Time.now, and 1.day.ago and 1.day.from_now will use the date I set?
Like, for example:
> DATE_TODAY='2017-01-04' rake test
For testing you can use timecop gem.
It offers you two useful methods Timecop.freeze and Timecop.travel.
For example, you can use freeze to statically set time in before hooks:
describe 'your tests' do
before do
Timecop.freeze(Time.new(2017, 1, 4))
end
after do
Timecop.return
end
it 'should do something' do
sleep(10)
Time.now # => 2017-01-04 00:00:00
end
end
Or in a block:
Timecop.freeze(Time.now - 3.months) do
assert product.expired?
end
While with the travel method, you change the starting moment, but time is still passing by.
Timecop.travel(Time.new(2017, 1, 4))
sleep(10)
Time.now # => 2017-01-04 00:00:10
As of Rails 4.1 you can do
travel_to Time.new(2004, 11, 24, 01, 04, 44)
The full API docs are here: http://api.rubyonrails.org/classes/ActiveSupport/Testing/TimeHelpers.html

select! always returns nil on heroku

Everything works fine on local.
This doesn't work on Heroku:
class Ticket
def self.how_many_today
todays_tickets = Ticket.all.to_a.select!{ |t| t.created_at.to_date == Date.today }
todays_tickets == nil ? 0 : todays_tickets.count
end
# This method is scheduled with cron
def self.reset_todays_nr
#todays_nr = nil
end
def self.set_todays_nr
if #todays_nr.nil?
#todays_nr = how_many_today + 1
else
#todays_nr += 1
end
end
end
Namely, playing on heroku run console reveals this inconsistency:
irb(main):023:0* set_todays_nr
=> 1
irb(main):024:0> set_todays_nr
=> 2
irb(main):025:0> set_todays_nr
=> 3
irb(main):026:0> Ticket.all.to_a.select!{ |t| t.created_at.to_date == Date.today }
=> nil
irb(main):028:0> Ticket.first.created_at
=> Sat, 20 Dec 2014 16:19:31 UTC +00:00
irb(main):029:0> Ticket.first.created_at.to_date
=> Sat, 20 Dec 2014
irb(main):030:0> Date.today
=> Sat, 20 Dec 2014
irb(main):031:0> Date.today.to_date
=> Sat, 20 Dec 2014
irb(main):032:0> Date.today == Ticket.first.created_at.to_date
=> true
irb(main):033:0> Date.today.to_date == Ticket.first.created_at.to_date
=> true
irb(main):034:0>
irb(main):035:0* Ticket.all.to_a.select!{ |t| t.created_at.to_date == Date.today }
=> nil
irb(main):036:0> Ticket.all.map(&:created_at)
=> [Sat, 20 Dec 2014 16:19:31 UTC +00:00, Sat, 20 Dec 2014 16:21:12 UTC +00:00]
irb(main):037:0> _[0].to_date == Date.today
=> true
It looks like the condition for select! is properly parsed, manual check shows there are some elements to that condition, but select! does not return any array. Once again, this does work locally.
Database has been migrated and fixtures loaded just as on local.
Although self.reset_todays_nr is scheduled with cron which might cause problems, this method is not triggered in this case, so it's rather irrelevant for the problem, but I posted it here just in case this problem is more advanced than I suppose.
Could anyone help me out here, please?
That is weird indeed. Particularly because I ran some commands in my Rails console just now and array.select!{} shouldn't return nil unless the array was empty to begin with.
[1].select!{ |t| false } #=> []
[].select!{ |t| false } #=> nil
So recheck what the output of Ticket.all.to_a is.
Also, your select condition can be set simply as:
var = Ticket.select{ |t| t.created_at.to_date == Date.today }
That will select all tickets itself and then filter.
But it would be preferable to filter and count in the query rather than load up everything in memory and then do further operations for comparisons. Check this out:
Ticket.where("DATE(created_at) = DATE(?)", Date.today).count
Or change the DATE(?) part with your SQL's "get today's date" function.
Alternatively, you could:
now = DateTime.now
range = (today.beginning_of_day)..(today.end_of_day)
Ticket.where(created_at: range).count
Keep the possible discrepancy of time-zone in mind i.e. the created_at column might have a different time-zone than generated by DateTime.now. You'll have to check.
#Humza, thank you very much! It isn't the precise solution, but helped me to solve the case. Thanks a lot for Ticket.where("DATE(created_at) = DATE(?)", Date.today).count - I was thinking exactly the same, i.e. not to load the entire array and only then evaluate it, so thanks for a way of doing it. If you look at my code, you see that in set_todays_nr I purposedly place how_many_today in a condition so as to run the search at most as often as cron is scheduled to reset #todays_nr. After changing it, bug became more visible: because of the flow of the app, the new how_many_today was returning 1 - the ticket is created before this method is called. Though the mystery of strange heroku behaviour remains unsolved, I didn't sleuth further as changing the method to the below form solved the problem. Now it looks like this:
def self.how_many_today
Ticket.where("DATE(created_at) = DATE(?)", Date.today).count
end
# This method is scheduled with cron; check config/schedule.rb
def self.reset_todays_nr
#todays_nr = nil
end
def self.set_todays_nr
if #todays_nr.nil?
#todays_nr = how_many_today
else
#todays_nr += 1
end
end

RSpec test custom validator

I have the following validator in my model:
class ContinuumValidator < ActiveModel::Validator
def validate(record)
if !record.end_time.nil? and record.end_time < record.start_time
record.errors[:base] << "An event can not be finished if it did not start yet..."
end
end
end
class Hrm::TimeEvent < ActiveRecord::Base
validates_with ContinuumValidator
end
How can I test it using Rspec?
Here is what I have tried so far: (thanks to zetetic)
describe "validation error" do
before do
#time_event = Hrm::TimeEvent.new(start_time: "2012-10-05 10:00:00", end_time: "2012-10-05 09:00:00", event_type: 2)
end
it "should not be valid if end time is lower than start time" do
#time_event.should_not be_valid
end
it "raises an error if end time is lower than start time" do
#time_event.errors.should include("An event can not be finished if it did not start yet...")
end
end
But I get the following errors:
1) Hrm::TimeEvent validation error raises an error if end time is lower than start time
Failure/Error: #time_event.errors.should include("An event can not be finished if it did not start yet...")
expected #<ActiveModel::Errors:0x007fd1d8e02c50 #base=#<Hrm::TimeEvent id: nil, start_time: "2012-10-05 08:00:00", end_time: "2012-10-05 07:00:00", event_type: 2, employee_id: nil, created_at: nil, updated_at: nil, not_punched: false, validated: false, replace_id: nil>, #messages={}> to include "An event can not be finished if it did not start yet..."
Diff:
## -1,2 +1,5 ##
-["An event can not be finished if it did not start yet..."]
+#<ActiveModel::Errors:0x007fd1d8e02c50
+ #base=
+ #<Hrm::TimeEvent id: nil, start_time: "2012-10-05 08:00:00", end_time: "2012-10-05 07:00:00", event_type: 2, employee_id: nil, created_at: nil, updated_at: nil, not_punched: false, validated: false, replace_id: nil>,
+ #messages={}>
What am I doing wrong? And how can I achieve my goal?
Any help or suggestion would be appreciated.
Thanks.
The problem is that you're expecting #time_event.errors to behave like an array of strings. It doesn't, it returns ActiveModel::Errors. As others pointed out, you also need to trigger the validations with a call to valid?:
it "raises an error if end time is lower than start time" do
#time_event.valid?
#time_event.errors.full_messages.should include("An event can not be finished if it did not start yet...")
end
This solution works for me (using Mongoid):
The model
class OpLog
...
field :from_status, type: String
field :to_status, type: String
...
validate :states_must_differ
def states_must_differ
if self.from_status == self.to_status
errors.add(:from_status, "must differ from 'to_status'")
errors.add(:to_status, "must differ from 'from_status'")
end
end
...
end
The test:
it 'is expected to have different states' do
expect { create(:oplog, from_status: 'created', to_status: 'created').to raise_error(Mongoid::Errors::Validations) }
end
So in your case I'd write a test like this (if using ActiveRecord):
it 'raises an error if end time is lower than start time' do
expect { create(Hrm::TimeEvent.new(start_time: "2012-10-05 10:00:00", end_time: "2012-10-05 09:00:00", event_type: 2)) }.to raise_error(ActiveRecord::Errors)
end
There are no errors because you haven't called an event that triggers the errors. This happens normally when a record is created or saved. You may not want to hit the database in your test though and then you can use the method valid? like this:
it "raises an error if end time is lower than start time" do
#time_event.valid?
#time_event.errors.should include("An event can not be finished if it did not start yet...")
end
Me personally would put these two tests into one since valid? is called in the first case.
Also a minor: if record.end_time is better than if !record.end_time.nil?. (In my opinion at least.... :-) )
I think the record wasnt validated therefore the validatior didn't run and no error was aded. You can see this in the code output. "validated: false"
try:
it "raises an error if end time is lower than start time" do
#time_event.valid?
#time_event.errors.should include("An event can not be finished if it did not start yet...")
end
You have not tested the validation actually, plus i would suggest you to make a single spec.
describe "validation error" do
before { #time_event = Hrm::TimeEvent.new(start_time: "2012-10-05 10:00:00", end_time: "2012-10-05 09:00:00", event_type: 2) }
it "raises an error if end time is lower than start time" do
#time_event.valid?
#time_event.errors.should include("An event can not be finished if it did not start yet...")
end
end
class ContinuumValidator < ActiveModel::Validator
def validate(record)
if record.end_time and record.end_time < record.start_time
record.error.add_to_base << "An event can not be finished if it did not start yet..."
end
end
end

Resources