I am trying to use a rake task that will run every night (using Heroku Scheduler) and update some attributes if some certain criteria is met.
A little logic about the application:
The application allows users to "take the challenge" and read a book a week over the course of a year. It's pretty simple: users sign up, create the first book that they will read for their first week, then can enter what they're going to read next week. After they've "queued" up next week's book, that form is then hidden until it's been 7 days since their first book was created. At that point, the book that was queued up gets moved to the top of the list and marked as 'currently reading', while the previous 'currently reading' book moves down to the second position in the list.
And IF a user doesn't 'queue' a book, the system will automatically create one if it's been 7 days since the latest 'currently reading' book was created.
Where I need some advice
The place I'm currently stuck is getting the books to update attributes if it's been 7 days since the last 'currently reading' book was created. Below is my book model and the method update_queue is what gets called during the rake task. Running the rake task currently gives no errors and properly loops through the code, but it just doesn't change any attribute values. So I'm sure the code in the update_queue method is not correct somewhere along the lines and I would love your help troubleshooting the reason why. And how I'm testing this is by adding a book then manually changing my system's date to 8 days ahead. Pretty barbaric, but I don't have a test suite written for this application & it's the easiest way to do it for me :)
class Book < ActiveRecord::Base
attr_accessible :author, :date, :order, :title, :user_id, :status, :queued, :reading
belongs_to :user
scope :reading_books, lambda {
{:conditions => {:reading => 1}}
}
scope :latest_first, lambda {
{:order => "created_at DESC"}
}
def move_from_queue_to_reading
self.update_attributes(:queued => false, :reading => 1);
end
def move_from_reading_to_list
self.update_attributes(:reading => 0);
end
def update_queue
days_gone = (Date.today - Date.parse(Book.where(:reading => 1).last.created_at.to_s)).to_i
# If been 7 days since last 'currently reading' book created
if days_gone >= 7
# If there's a queued book, move it to 'currently reading'
if Book.my_books(user_id).where(:queued => true)
new_book = Book.my_books(user_id).latest_first.where(:queued => true).last
new_book.move_from_queue_to_reading
Book.my_books(user_id).reading_books.move_from_reading_to_list
# Otherwise, create a new one
else
Book.my_books(user_id).create(:title => "Sample book", :reading => 1)
end
end
end
My rake task looks like this (scheduler.rake placed in lib/tasks):
task :queue => :environment do
puts "Updating feed..."
#books = Book.all
#books.each do |book|
book.update_queue
end
puts "done."
end
I would move the update_queue logic to the User model and modify the Book model somewhat, and do something like this:
# in book.rb
# change :reading Boolean field to :reading_at Timestamp
scope :queued, where(:queued => true)
scope :earliest_first, order("books.created_at")
scope :reading_books, where("books.reading_at IS NOT NULL")
def move_from_queue_to_reading
self.update_attributes(:queued => false, :reading_at => Time.current);
end
def move_from_reading_to_list
self.update_attributes(:reading_at => nil);
end
# in user.rb
def update_queue
reading_book = books.reading_books.first
# there is an edge-case where reading_book can't be found
# for the moment we will simply exit and not address it
return unless reading_book
days_gone = Date.today - reading_book.reading_at.to_date
# If less than 7 days since last 'currently reading' book created then exit
return if days_gone < 7
# wrap modifications in a transaction so they can be rolled back together
# if an error occurs
transaction do
# First deal with the 'currently reading' book if there is one
reading_book.move_from_reading_to_list
# If there's a queued book, move it to 'currently reading'
if books.queued.exists?
books.queued.earliest_first.first.move_from_queue_to_reading
# Otherwise, create a new one
else
books.create(:title => "Sample book", :reading_at => Time.current)
end
end
end
Now you can have the Heroku scheduler run something like this once a day:
User.all.each(&:update_queue)
Modify User.all to only return active users if you need to.
Oh, and you can use the timecop gem to manipulate times and dates when testing.
Probably the reason is that, the program flow is not going inside the if days_gone >= 7 condition.
you could check this in two ways
1 - Simple and easy way (but not a very good way)
use p statements every were with some meaning full text
Ex:
def update_queue
days_gone = (Date.today - Date.parse(Book.where(:reading => 1).last.created_at.to_s)).to_i
p "days gone : #{days_gone}"
# If been 7 days since last 'currently reading' book created
if days_gone >= 7
p "inside days_gone >= 7"
etc...
2 - Use ruby debugger and use debug points
in Gem file add
gem 'debugger'
and insert break points where ever needed
def update_queue
days_gone = (Date.today - Date.parse(Book.where(:reading => 1).last.created_at.to_s)).to_i
debugger
more help
HTH
Related
I want to send mail at remind_at time of each stage. i am using whenever gem to schedule task. There are user model that contains email of multiple user with role manager, and each manager has one_to_many association with project and project has one_to_many association with stage.
while sending email to each user i want to update attribute mail_status of stage and mail subject change to stage.name for each user. how can i achieve such goal??
stage.rb
def check_project_activity
current_date = Time.now.strftime("%Y-%m-%d").to_s
#stages = Stage.all
#stages.each do |stage|
ProjectMailer.activity_reminder(stage).deliver and stage.mail_status = true and stage.save! if stage.remind_at.strftime("%Y-%m-%d").to_s == current_date
end
end
schedule.rb
every 1.day, at: '4:30 am' do
runner 'stage.project_activity_check'
end
activity_mailer.rb
def activity_reminder(stage)
#stage = stage
mail(:to => User.joins(projects: :stages).where(role: 'manager', stages: { remind_at: Time.now.strftime("%Y-%m-%d") }).pluck(:email), :subject => "activity reminder " + stage.name)
end
i took reference from this post - Sending emails on user-selected dates
but my implementation is not working.
Here is code that i used but problem is it sends email once to all user but i want dynamic change is email subjet according to user and also template body changes-
activity_reminder.rb
desc 'activity reminder email'
task activity_reminder_email: :environment do
ProjectMailer.activity_reminder(self).deliver!
end
project_mailer.rb
def activity_reminder(stage)
#stage = stage
mail(:to => User.joins(projects: :stages).where(role: 'manager', stages: { remind_at: Time.now.strftime("%Y-%m-%d") }).pluck(:email), :subject => "Project Activity Remainder")
end
schedule.rb
every 1.day, at: '4:30 am' do
rake 'activity_reminder_email'
end
it works fine but i want email subject as stage.name to be according to each user and also it should loop though each task and update stage.email_status for executed task.
I think you can do some changes:
You're formatting the remind_at values as a stringified version of a Y-m-d date, in that case, you could just compare them as dates. to_date is enough.
Instead of selecting everything and filtering then, you can filter the rows within your SQL query; cast the remind_at to date and use your RDBMS function to get the current date, after that the comparison remains the same.
Left it untouched, but you could probably select the exact columns you're using in your query. This is often preferred so you get what you just need, nothing more, nothing less.
Prefer && over and, since they don't have the same precedence (&& being higher), which may led to inconsistences in your code (this isn't Python).
You can use update instead of a two-lines assignment.
def check_project_activity
Stage.where('remind_at::date = CURRENT_DATE').each do |stage|
ProjectMailer.activity_reminder(stage).deliver
stage.update(mail_status: true)
end
end
remind_at::date = CURRENT_DATE is PostgreSQL specific, but it can be easily adapted.
I'm trying to download a CSV file from an FTP server and if the record exists I want to update that record and not create a duplicate. To give a little more context - I'm trying to upload a group of orders from an FTP folder into my Rails app. There is a new file every hour - sometimes the orders in a certain drop contain duplicates from the previous drop to prevent one from slipping through the tracks or on occasion the order has been updated by the customer (change in qty, change in address, etc.) w/ the next drop. So my question is if the order is purely a duplicate with no changes how can I skip over those orders and if a record has been changed how can I update that record?
Ruby on Rails 5.1.4 - Ruby 2.4.1
Thank you!
The code below is from my model:
class Geek < ApplicationRecord
require 'csv'
def self.download_walmart_orders(out)
out ||= "#{Rails.root}/test_orders.csv"
CSV.foreach(out, :headers => true,
:converters => :all,
:header_converters => lambda { |h| h.downcase.gsub(' ', '_') }
) do |row|
geek = Geek.where(customer_order_id: row.to_h["customer_order_id"],
customer_name: row.to_h["customer_name"],
item_sku: row.to_h["item_sku"],
quantity_to_ship: row.to_h["quantity_to_ship"],
total_items_price: row.to_h["total_items_price"]).first_or_create
puts geek
end
end
end
I am assuming that customer_order_id is unique.
You could try something like this -
def self.update_or_create(attributes)
assign_or_new(attributes).save
end
Geek.where(customer_order_id: row.to_h["customer_order_id"]).update_or_create.(
customer_name: row.to_h["customer_name"],
item_sku: row.to_h["item_sku"],
quantity_to_ship: row.to_h["quantity_to_ship"],
total_items_price: row.to_h["total_items_price"])
^^^ Thank you, Michael, for the direction above. I ended up using this code and it worked perfectly. (For a slightly different project but exact same use case) my finalized model code is below:
class Wheel < ApplicationRecord
require 'csv'
def self.update_or_create(attributes)
obj = first || new
obj.assign_attributes(attributes)
obj.save!
end
def self.import(out)
out ||= "#{Rails.root}/public/300-RRW Daily Inv Report.csv"
CSV.foreach(out, :headers => true,
:converters => :all,
:header_converters => lambda { |h| h.downcase.gsub(' ', '_') }
) do |row|
Wheel.where(item: row.to_h["item"]).update_or_create(
item_desc: row.to_h["item_desc"],
total_quantity: row.to_h["total_quantity"])
end
end
end
I am testing my Tire / ElasticSearch queries and am having a problem with a custom method I'm including in to_indexed_json. For some reason, it doesn't look like it's getting indexed properly - or at least I cannot filter with it.
In my development environment, my filters and facets work fine and I am get the expected results. However in my tests, I continuously see zero results.. I cannot figure out where I'm going wrong.
I have the following:
def to_indexed_json
to_json methods: [:user_tags, :location_users]
end
For which my user_tags method looks as follows:
def user_tags
tags.map(&:content) if tags.present?
end
Tags is a polymorphic relationship with my user model:
has_many :tags, :as => :tagable
My search block looks like this:
def self.online_sales(params)
s = Tire.search('users') { query { string '*' }}
filter = []
filter << { :range => { :created_at => { :from => params[:start], :to => params[:end] } } }
filter << { :terms => { :user_tags => ['online'] }}
s.facet('online_sales') do
date :created_at, interval: 'day'
facet_filter :and, filter
end
end
end
I have checked the user_tags are included using User.last.to_indexed_json:
{"id":2,"username":"testusername", ... "user_tags":["online"] }
In my development environment, if I run the following query, I get a per day list of online sales for my users:
#sales = User.online_sales(start_date: Date.today - 100.days).results.facets["online_sales"]
"_type"=>"date_histogram", "entries"=>[{"time"=>1350950400000, "count"=>1, "min"=>6.0, "max"=>6.0, "total"=>6.0, "total_count"=>1, "mean"=>6.0}, {"time"=>1361836800000, "count"=>7, "min"=>3.0, "max"=>9.0, "total"=>39.0, "total_count"=>7, "mean"=>#<BigDecimal:7fabc07348f8,'0.5571428571 428571E1',27(27)>}....
In my unit tests, I get zero results unless I remove the facet filter..
{"online_sales"=>{"_type"=>"date_histogram", "entries"=>[]}}
My test looks like this:
it "should test the online sales facets", focus: true do
User.index.delete
User.create_elasticsearch_index
user = User.create(username: 'testusername', value: 'pass', location_id: #location.id)
user.tags.create content: 'online'
user.tags.first.content.should eq 'online'
user.index.refresh
ws = User.online_sales(start: (Date.today - 10.days), :end => Date.today)
puts ws.results.facets["online_sales"]
end
Is there something I'm missing, doing wrong or have just misunderstood to get this to pass? Thanks in advance.
-- EDIT --
It appears to be something to do with the tags relationship. I have another method, ** location_users ** which is a has_many through relationship. This is updated on index using:
def location_users
location.users.map(&:id)
end
I can see an array of location_users in the results when searching. Doesn't make sense to me why the other polymorphic relationship wouldn't work..
-- EDIT 2 --
I have fixed this by putting this in my test:
User.index.import User.all
sleep 1
Which is silly. And, I don't really understand why this works. Why?!
Elastic search by default updates it's indexes once per second.
This is a performance thing because committing your changes to Lucene (which ES uses under the hood) can be quite an expensive operation.
If you need it to update immediately include refresh=true in the URL when inserting documents. You normally don't want this since committing every time when inserting lots of documents is expensive, but unit testing is one of those cases where you do want to use it.
From the documentation:
refresh
To refresh the index immediately after the operation occurs, so that the document appears in search results immediately, the refresh parameter can be set to true. Setting this option to true should ONLY be done after careful thought and verification that it does not lead to poor performance, both from an indexing and a search standpoint. Note, getting a document using the get API is completely realtime.
On my User model, I have an attribute trial_end_date.
The column in the table looks like this:
# trial_end_date :date
However, if I try to change the date to far in the future, at the Rails console, this is what happens:
a = User.find(2)
a.trial_end_date = "2019-12-30"
=> "2019-12-30"
>> a.save
=> true
>> a.trial_end_date
=> Sat, 19 Nov 2011
WTF? Why does it do that? I have no idea why it does this?
Even if I try update_attributes(:trial_end_date => "2019-12-30") the same thing happens.
Here are all the methods in my User model that relate to trial_end_date:
after_validation :set_trial_end
def has_trial_expired?
if (self.trial_end_date <= Date.today)
return true
else
return false
end
end
def set_trial_end
plan = self.plan
end_of_trial = Date.today + self.plan.trial_duration.days
self.trial_end_date = end_of_trial.to_date
end
def trial_will_almost_end?
if (self.trial_end_date - Date.today <= 3)
return true
else
return false
end
end
def when_does_trial_end?
self.trial_end_date
end
marcamillion,
You commented that you thought that you thought that validation would happen "just on the initial user creation." As comments have pointed out, that's not true if you use after_validation, but it IS true if you use
before_validation_on_create
(See, for example, http://ar.rubyonrails.org/classes/ActiveRecord/Callbacks.html )
Using that would restrict the creation of dates by your users, but wouldn't prevent you (or them! Be careful!) from changing them later in other ways.
After validation the trial_end_date gets set based on the plan duration, no?
One refinement to Bob's answer: *_on_create and its ilk are deprecated in 3.0 and removed in 3.1. In the interest of maintainability, you probably want to adopt the new form:
before_validation :some_method, :on => :create
It's a quick tweak that'll save you headaches in the future.
Quick summary:
I have a Rails app that is a personal checklist / to-do list. Basically, you can log in and manage your to-do list.
My Question:
When a user creates a new account, I want to populate their checklist with 20-30 default to-do items. I know I could say:
wash_the_car = ChecklistItem.new
wash_the_car.name = 'Wash and wax the Ford F650.'
wash_the_car.user = #new_user
wash_the_car.save!
...repeat 20 times...
However, I have 20 ChecklistItem rows to populate, so that would be 60 lines of very damp (aka not DRY) code. There's gotta be a better way.
So I want to use seed the ChecklistItems table from a YAML file when the account is created. The YAML file can have all of my ChecklistItem objects to be populated. When a new user is created -- bam! -- the preset to-do items are in their list.
How do I do this?
Thanks!
(PS: For those of you wondering WHY I am doing this: I am making a client login for my web design company. I have a set of 20 steps (first meeting, design, validate, test, etc.) that I go through with each web client. These 20 steps are the 20 checklist items that I want to populate for each new client. However, while everyone starts with the same 20 items, I normally customize the steps I'll take based on the project (and hence my vanilla to-do list implementation and desire to populate the rows programatically). If you have questions, I can explain further.
Just write a function:
def add_data(data, user)
wash_the_car = ChecklistItem.new
wash_the_car.name = data
wash_the_car.user = user
wash_the_car.save!
end
add_data('Wash and wax the Ford F650.', #user)
I agree with the other answerers suggesting you just do it in code. But it doesn't have to be as verbose as suggested. It's already a one liner if you want it to be:
#new_user.checklist_items.create! :name => 'Wash and wax the Ford F650.'
Throw that in a loop of items that you read from a file, or store in your class, or wherever:
class ChecklistItem < AR::Base
DEFAULTS = ['do one thing', 'do another']
...
end
class User < AR::Base
after_create :create_default_checklist_items
protected
def create_default_checklist_items
ChecklistItem::DEFAULTS.each do |x|
#new_user.checklist_items.create! :name => x
end
end
end
or if your items increase in complexity, replace the array of strings with an array of hashes...
# ChecklistItem...
DEFAULTS = [
{ :name => 'do one thing', :other_thing => 'asdf' },
{ :name => 'do another', :other_thing => 'jkl' },
]
# User.rb in after_create hook:
ChecklistItem::DEFAULTS.each do |x|
#new_user.checklist_items.create! x
end
But I'm not really suggesting you throw all the defaults in a constant inside ChecklistItem. I just described it that way so that you could see the structure of the Ruby object. Instead, throw them in a YAML file that you read in once and cache:
class ChecklistItem < AR::Base
def self.defaults
##defaults ||= YAML.read ...
end
end
Or if you wand administrators to be able to manage the default options on the fly, put them in the database:
class ChecklistItem < AR::Base
named_scope :defaults, :conditions => { :is_default => true }
end
# User.rb in after_create hook:
ChecklistItem.defaults.each do |x|
#new_user.checklist_items.create! :name => x.name
end
Lots of options.
A Rails Fixture is used to populate test-data for unit tests ; Dont think it's meant to be used in the scenario you mentioned.
I'd say just Extract a new method add_checklist_item and be done with it.
def on_user_create
add_checklist_item 'Wash and wax the Ford F650.', #user
# 19 more invocations to go
end
If you want more flexibility
def on_user_create( new_user_template_filename )
#read each line from file and call add_checklist_item
end
The file can be a simple text file where each line corresponds to a task description like "Wash and wax the Ford F650.". Should be pretty easy to write in Ruby,