updating a model attribute that will only change once - ruby-on-rails

I have a subject model with attributes including a start_date and end_date - as well as a completed boolean attribute.
In subject.rb, I have a method to find how many weeks are remaining for a given subject:
def weeks_left
time = (self.end_date.to_time - Date.today.to_time).round/1.week
if time < 0
"completed"
elsif time < 1
"less than 1 week"
elsif time == 1
"1 week"
else
"#{time} weeks"
end
end
I want to tick the completed attribute if self.weeks_left == "completed" and the best way to do that seems like a call back, but I'm a bit unsure about using after_find - in general it seems like it would be too many queries, and indeed too big of a pain (especially after reading this) - but in this case, once a subject is complete, it's not going to change, so it seems useless to check it's status more than once - what's the best way to handle this?

Why dont you make a scope for this?
scope :completed, ->{where("end_date <= ?", Time.now)}
and a method
def completed?
self.weeks_left == "completed"
end

Looks like you need ActiveRecord::Callbacks. You can see more information here or on rails guide
before_save :update_completed
def update_completed
if (end_date_changed?)
time = (self.end_date.to_time - Date.today.to_time).round/1.week
self.complete = time < 0
end
end
This way you update the complete flag whenever end_date changes and it would always be in sync.
However because this is a calculated value you could also not store it as an attribute and simply define a method to get it
def complete
time = (self.end_date.to_time - Date.today.to_time).round/1.week
return time < 0
end

Related

How to check range between two dates with rails 6

Hello i have this blog module where my posts can have 3 states "no_status" "in_draft" and "published", the user can set the publish_date and publish_end_date for his posts
while this range between the dates is fulfilled, the status of the post must be "published", and when it is finished return to "in_draft"
def post
if self.no_status? || self.in_draft?
if self.publish_date >=Date.today && self.publish_end <= Date.today
self.update_attribute :status, 'published'
end
elsif self.published?
if self.publish_date.past? && self.publish_end.past?
self.update_attribute :status, 'in_draft'
end
end
end
What is the proper way to manage this, i have a big problem with my conditions.
In the 1st branch your conditionals are mixed up now, they should be opposite (that's why it doesn't work as expected - you check than publish_date is greater that current date, but this is wrong - it must be in the past to have the post published today). So if you simply "mirror" your conditional operators it should work - but there are cleaner ways of writing the same:
Date.today.between?(publish_date, publish_end)
# or
(publish_date..publish_end).cover?(Date.today)
In the 2nd branch checking that pulish_end date is n the past should be enough. Checking if publish_date is in the past too is redundant - if it is not you have bigger problems what just a wrong status :) - this kind of basic data integrity is better to be addressed by model validations.
Also, the nested ifs are absolutely unnecessary here, they just make the code harder to reason about.
To summarize, something like the following should do the job (I'm not discussing here how this method is being used and whether it should be written this way or not - just addressing the initial question)
def post
new_status =
if published? && publish_end.past?
'in_draft'
elsif Date.today.between?(publish_date, publish_end)
'published'
end
update_attribute :status, new_status
end
You can use Object#in?
def post
if no_status? || in_draft?
update(status: 'published') if Date.today.in?(publish_date..publish_end)
elsif published?
update(status: 'in_draft') if publish_date.past? && publish_end.past?
end
end
(BTW you don't need self in Ruby every time)
You can use Range#cover?. It basically takes a range and checks if the date is withing the start/end;
(10.days.ago..1.day.ago).cover?(3.days.ago)
# true
So, in your case;
(publish_date..publish_end).cover?(Date.today)

Inconsistent read in database

I'm seeing inconsistent behavior, and am wondering what I could be doing wrong here.
I have subscription objects, whose state is defined by its cycle and con attributes, that are integers. months_passed returns an integer that counts how many FULL months has passed between the start_date of the subscription and Time.current.
def update_state
update_cycle
update_con
self.save
end
def update_cycle
self.cycle = if months_passed > 0
(months_passed - 1)/3 + 1
else
0
end
end
def update_con
self.con = if months_passed > 0
(months_passed - 1) % 3 + 1
else
0
end
end
def in_con1?
update_state
con == 1
end
However, when I call in_con1?, quickly in succession, I'll inconsistently get true or false.
Do I need to reload the object? Is something stale?
Argh, sorry guys. I found the culprit. Had nothing to do with inconsistent database reads. It was when months_passed was getting called, an hour before I expected.

ActiveRecord where method datetime passed

Thanks for your continuing support in my latest venture in a Rails app. It's been a while since I've made something in Rails so here is my latest question. I appreciate the help you've all given in the past.
I have a Model called Event.rb which contains a date:date and time:time fields for the Event.
I also have a method inside of the model which is..
def begins
DateTime.new(date.year, date.month, date.day, time.hour, time.min, time.sec)
end
As I can't see if something has truly passed because I only have Date and Time separate so I need them together.
My question is...
I want to be able to add in the DateTime :begins into the following other method in my Model...
def self.past
where("date <= ?", TIME_NOW)
end
Just like I have a method which is...
def upcoming?
self.date >= Time.now
end
Which I could easily change self.date to begins and would past I would imagine?
Thanks!
Perhaps something like this will work for querying the database for past events using your existing date and time columns:
scope :past, lambda {
where("date <= ? and time <= ?",
Time.now.strftime("%Y-%d-%m"),
Time.now.strftime("%H:%M:%S")
)
}
past_events = Event.past
For checking the current instance, you could continue to use your begins method:
def past?
begins < DateTime.now
end
#event = Event.first
#event.past?

How to specify a minimum count on an association using active record

I have Battle class which has many Participants. I'm using this class method to return the last Battle to be voted on:
def self.get_voteable_battle
#return the battle whose submissions_deadline ended less than 3 days ago
time_now = Time.now
end_of_day = time_now.end_of_day
return self.where(:submissions_deadline => end_of_day.ago(3.days)..time_now).first
end
But I also want to ensure that there are at least 2 participants.
I can add another condition like this:
def self.get_voteable_battle
#return the battle whose submissions_deadline ended less than 3 days ago
time_now = Time.now
end_of_day = time_now.end_of_day
battle = self.where(:submissions_deadline => end_of_day.ago(3.days)..time_now).first
if battle && battle.participants.count > 1
return battle
else
return nil
end
end
But this would require another query right? Is there a way to do it in one query using active record?
You could try
self.joins(:participants).where(:submissions_deadline => end_of_day.ago(3.days)..time_now).having('count(participants.id) > ?', 1).last
It may be the battle id in the having clause but one of the two will work and i'll update. head hurts right now.

Rails: set model attribute using a time reference

1. The environment
I have an Auction model with these attributes:
status:string
close_date:datetime
Status of my auctions must be, automatically, set to 'ended' when Time.now
equals auction.close_date.
To do so, I think I need a way to check for this equality every second.
This is my method:
def set_ended
auctions = Auction.all.active # named_scope to get only 'active' auctions
auctions.each do |auction|
if auction.close_date == Time.now
auction.status = 'ended'
end
end
2. The question:
How can I set my application to run this method every second?
Take a look at Whenever, that's what you need.

Resources