Comparing a model before/after save with other models - ruby-on-rails

I'm having trouble wrapping my head around this.
I have a model (Task) which has attributes: :day, :room, :begin_time, :end_time, :gear, :notes, and others (which aren't relevant). I also have a linked_id_task attribute in a migration set on the Task model. And I have these relevant scopes (which I chain together):
scope :for_day, lambda { |day| where(day: day) }
scope :for_room, lambda { |room| where(room: room) }
scope :with_gear, lambda { |gear| where(gear: gear) }
#tasks_on_same_day = Task.for_day(self.day).for_room(self.room).with_gear(self.gear).all
#task_on_previous_day = Task.for_day(self.day.yesterday).for_room(self.room).with_gear(self.gear).last
#task_on_next_day = Task.for_day(self.day.tomorrow).for_room(self.room).with_gear(self.gear).first
Whether I'm creating a new task or updating a new one, I want to:
Check for previous and next tasks (based on :begin_time) in private instance methods prev? and next?. If there is a previous task, I want to update its :notes and add that task to the :linked_task_id; if there's a next task, I want to set self.notes with info about the next task and add it to :linked_task_id. If there aren't any tasks on the current day (above), previous day or next day, I'll return nil.
The previous and next day is simple, because I'll get the last task from the previous day (if it exists) or first task from the next day. If there are several tasks on the current day, I want be able to have the task sorted in to easily find the previous and next task, i.e.
#self_id = #tasks_on_same_day.index(self)
and use that index to update_attributes of the prev or next tasks. The problem is, I don't think that a newly created task or an updated task (i.e., I update the begin_time) will be available in a before_save callback.
I can create a class method to sort self into a list of the current day tasks.
I'm not looking for someone to code this for me, but I am confused about the approach I should take.
Thanks.

To preface my answer: I'm a newbie.
After being frustrated for awhile, I started watching RailsCasts, picking up where I left off from last night. And it dawned on me (watching a video about ActiveRecord querying) that I'm an idiot.
I haven't tried this yet, but I can just query for something like...
#previous = Task.for_day(self.day).for_room(self.room).with_gear(self.gear).where('begin_time < ?' self.begin_time).last
#next = Task.for_day(self.day).for_room(self.room).with_gear(self.gear).where('begin_time > ?' self.begin_time).first
That's amazingly easy. To make this question useful, what's the performance effect of a long query string like that?

Related

How to delete Cart and LineItems before session end?

I am following this tutorial to make Cart in my Ecomerce App:
https://richonrails.com/articles/building-a-shopping-cart-in-ruby-on-rails
Anything do well but when session end and customer do not complete the order, this order and order item still saved in the database.
My idea is use callback method before_destroy session cart but i dont know exactly what method destroy session
Can anyone help me?
Unfortunately the callback method will be difficult, because you never know when the session ended, since websites are stateless. In other words, you never really know whether a visitor is just idle, or he actually left. The only signs you normally get, are requests (GET/POST/PATCH/..).
What you could do to make this work, is check the last_updated value, and delete all non-confirmed carts after let's say two hours. Or, you could even do a day, because who cares right? It's only a few bytes in your DB.
To make this work, you could write a rake task that queries and deletes all the abandoned carts.
Something like:
# cart model
class Cart
scope :unconfirmed { where(bought: false) }
scope :abandoned, { unconfirmed.where('last_update < ?', 1.day.ago}
end
# rake task
namespace :maintenance do
task delete_abandoned_carts: :environment do
Cart.abandoned.destroy_all
end
end
Learn more about rake tasks here: https://guides.rubyonrails.org/command_line.html#custom-rake-tasks

Expire cache based on saved value

In my app there is a financial overview page with quite a lot of queries. This page is refreshed once a month after executing a background job, so I added caching:
#iterated_hours = Rails.cache.fetch("productivity_data", expires_in: 24.hours) do
FinancialsIterator.new.create_productivity_iterations(#company)
end
The cache must expire when the background job finishes, so I created a model CacheExpiration:
class CacheExpiration < ApplicationRecord
validates :cache_key, :expires_in, presence: true
end
So in the background job a record is created:
CacheExpiration.create(cache_key: "productivity_data", expires_in: DateTime.now)
And the Rails.cache.fetch is updated to:
expires_in = get_cache_key_expiration("productivity_data")
#iterated_hours = Rails.cache.fetch("productivity_data", expires_in: expires_in) do
FinancialsIterator.new.create_productivity_iterations(#company)
end
private def get_cache_key_expiration(cache_key)
cache_expiration = CacheExpiration.find_by_cache_key(cache_key)
if cache_expiration.present?
cache_expiration.expires_in
else
24.hours
end
end
So now the expiration is set to a DateTime, is this correct or should it be a number of seconds? Is this the correct approach to make sure the cache is expired only once when the background job finishes?
Explicitly setting an expires_in value is very limiting and error prone IMO. You will not be able to change the value once a cache value has been created (well you can clear the cache manually) and if ever you want to change the background job to run more/less often, you also have to remember to update the expires_in value. Additionally, the time when the background job is finished might be different from the time the first request to the view is made. As a worst case, the request is made a minute before the background job updates the information for the view. Your users will have to wait a whole day to get current information.
A more flexible approach is to rely on updated_at or in their absence created_at fields of ActiveRecord models.
For that, you can either rely on the CacheExpiration model you already created (it might already have the appropriate fields) or use the last of the "huge number of records" you create. Simply order them and take the last SomeArModel.order(created_at: :desc).first
The benefit of this approach is that whenever the AR model you create is updated/created, you cache is busted and a new one will be created. There is no longer a coupling between the time a user called the end point and the time the background job ran. In case a record is created by any means other than the background job, it will also simply be handled.
ActiveRecord models are first class citizens when it comes to caching. You can simply pass them in as cache keys. Your code would then change to:
Rails.cache.fetch(CacheExpiration.find_by_cache_key("productivity_data")) do
FinancialsIterator.new.create_productivity_iterations(#company)
end
But if at all possible, try to find an alternative model so you no longer have to maintain CacheExpiration.
Rails also has a guide on that topic

Need Help: Model is not working

So what I'm doing is I have a rake task that everyday will decrease the days left on a subscription. Here is the rake task:
namespace :delete do
desc 'Remove a day for premium subscription days left'
task :premium_subscription_remove => :environment do
PremiumSubscription.find_each do | premium_subscription|
premium_subscription.premium_subscription_days_left -= 1
premium_subscription.save
end
end
end
This rake task will count down the days left on the subscription. Now I created a new model that will handle the days left once it hit zero. Once it hit zero the code will cause the subscription to auto renew. Here is the code for the renewal:
def self.renew_premium_subscription(user, premium_subscribe)
if premium_subscribe.premium_subscription_days_left <= 0
user.premium_subscriptions.where(:premium_subscribe_id => premium_subscribe.id).destroy_all
if user.points >= premium_subscribe.premium_subscription_cost
user.premium_subscriptions.where(:premium_subscribe_id => premium_subscribe.id).first_or_create
user.points = user.points - premium_subscribe.premium_subscription_cost
user.save
end
end
end
The problem I am having is that the premium_subscription_days_left is at negative two and the renew_premium_subscription has never been acted. I tried putting in random letters and the model hasn't givin an error. How does the model get acted upon inorder for the renewal? I have put code in the controller:
def renew_subscription
PremiumSubscription.renew_premium_subscription(current_user, #user)
end
But that hasn't worked at all. If anybody knows how to get this thing working it will be great. Thank you for the help : )
edit: Tried putting the update function inside of the rake task but that did not work at all.
edit 2: No such luck on getting this fixed. Anybody have a clue?
edit 3: So i though about something, is there a way to automatically call a model class. I just need to get this thing working.
Here is an outline of what I did:
Created a rake task. This rake task will be called with whenever to count down to zero.
I created a model in the premiumSubscription model that says when it hits zero it will either update the subscription or destroy it.
I have set the count down to zero, refreshed the page but the subscription isn't updated or destroyed.
edit 4: So I learned that the controller needs to be triggered by a route. Is there any way to trigger this code when the page loads?
A couple of notes:
user.save
This can fail. If save is unsuccessful it will return false. Your method does not test the return value, so an unsuccessful save will go unnoticed -- nothing is written to the log file, no error is raised, nada. Either check the result of save or call save!, which will raise an exception.
premium_subscription.premium_subscription_days_left -= 1
You might have a good reason for storing this value, but you should also consider storing the start date (or the expiration date) instead, and calculating the days left given the current date. Decrementing "days left" requires that the cron job runs when it is supposed to ... if it misses a day, the count will be off.

Correct Rails Method for Lots of Logic

I am currently building a site that runs an autonomous competition every week. The logic I have checks if the previous week has a winner assigned, and if it does not, it rolls through, finds a winner and assigns a trophy.
The logic all works, but a lot of it is run in the application controller, and in my core I feel this is not right.
For example, if first place has more votes than second place, and second place has more votes than third place, it would need to create a first second and third place trophy and reward it to the correct users.
if first_place > second_place && second_place > third_place
#week_previous.winner_id = week_entries[0].id
#week_previous.save
first_trophy = week_entries[0].user.trophies.new
first_trophy.week_id = #week_previous.id
first_trophy.user_id = week_entries[0].user_id
first_trophy.position = "first"
first_trophy.country = week_entries[0].user.country
first_trophy.pro = false
first_trophy.save
if second_place >= 1
second_trophy = week_entries[1].user.trophies.new
second_trophy.week_id = #week_previous.id
second_trophy.user_id = week_entries[1].user_id
second_trophy.position = "second"
second_trophy.country = week_entries[1].user.country
second_trophy.pro = false
second_trophy.save
end
if third_place >= 1
third_trophy = week_entries[2].user.trophies.new
third_trophy.week_id = #week_previous.id
third_trophy.user_id = week_entries[2].user_id
third_trophy.position = "third"
third_trophy.country = week_entries[2].user.country
third_trophy.pro = false
third_trophy.save
end
end
This is building the Trophies directly into the controller, and I've often heard the 'fat model skinny controller' argument, and I feel the way I am running this goes totally against that!
How would I move the trophy creations into the model? I am sure I could use something like after_save in the weeks model, but I am not entirely sure how to keep the logic working. I've had a few attempts, but I am often getting the error undefined method to_model.
I know I could just plough on and get it working, but I just feel like it's not the 'Rails Way' of doing things, so I'd like to work it out in its early stages.
Any help is greatly appreciated.
Thanks!
Edit based on comments:
Thanks for taking the time to look at this. In a nut shell, what I am trying to achieve is a system where a competition runs from Monday to Sunday. The 'Active Week' is scoped to the Week where the Date.today falls between the :start_date and :end_date.
The following Monday starts a new week, this moves what was the active week to the previous week, and it then allocates trophies to the top 3 entries from the previous week. It allocates the trophies by checking if the previous week has a winner_id. If it does, it doesn't run any of the logic, but once the scope moves onto a new week, the previous weeks :winner_id is now nil, so the logic runs the first time somebody comes to the site in the new week.
To dumb it down:
Week is a resource, which has_many Entries.
An Entry belongs_to a User, belongs_to a Week, and has_many Votes.
A Vote belongs_to an Entry
A User has_many Entries and has_many Trophies
A Trophy belongs to a User and belongs_to a Week
So, users Vote on an Entry on the current active Week. Once the week is outside of the active scope, it creates Trophies for the Users that placed in the top 3 positions of the week.
It is hard to give with a good advise without knowing the context. But what catches my eye is that there is a lot of repetion in that code and that you update user trophies in that repetitions. Therefore I would at least move that logic into the user as a first step:
# in user.rb
def record_trophy_win(prev_week, entry, position)
trophies.create(
week_id: prev_week.id,
user_id: entry.user_id,
position: position,
country: entry.user.county,
pro: false
)
end
That allows to change the partial in the controller to this:
if first_place > second_place && second_place > third_place
#week_previous.update_attribute(:winner_id, week_entries[0].id)
first_trophy = week_entries[0].user.record_trophy_win(
#week_previous, week_entries[0], 'first'
)
second_trophy = week_entries[1].user.record_trophy_win(
#week_previous, week_entries[1], 'second'
) if second_place >= 1
third_trophy = week_entries[2].user.record_trophy_win(
#week_previous, week_entries[2], 'third'
) if third_place >= 1
end
That logic might in a next step belong into a Trophy class. Depends on the context...
And I noticed that your week_entry has an user. And you add to that user a trophy that again has some user fields. Doesn't that result in a circular dependency? Or do you override an existing record with the exact same entries? That need some clearity.
One way to isolate business logic is the Service Object design pattern. Example:
class TrophyRewarder
def initialize(winners, last_week_winners)
#winners = winners
#last_week_winners = last_week_winners
end
def reward(options)
# All code to determine and store winners comes here
end
end
You can put this code in the folder app/services and call it in your controller like this:
trophy_rewarder = TrophyRewarder.new(users, Winners.from_last_week)
trophy_rewarder.reward(options)
The good thing is that you can call this service object from a controller but also from a background task. Another good thing is that the service object can use a lot of different data/AR models, but is is not tied to a model.
I hope this helps a little in showing how you can organize your code.

Creating Rails todo list app with goals and tasks on one page - missing params

I'm a Rails beginner, and have been reading tutorials and typing out applications for a few months now. I'm really enjoying it after a few years spent in the front end world, and beginning to get up to speed with it all. The time has come though for me to start building my own stuff without any handholding. So far, so good.
I'm creating a basic to-do list app, where goals and tasks are displayed on the same page - goals#index. My issue is that I'm not sure how to get all tasks for a particular goal (that belongs to a user). I understand that I need to pass an ID param to the Goal model in order to find out its tasks, like so:
Goal.find(1).tasks
The above works fine, as I've already set up foreign keys on the tasks table and have a has_many :tasks relationship for the Goal model and a belongs_to relationship for the Task model.
Here's my Goals controller:
def index
#user = current_user
#goals = #user.goals # list all goals for the current user and assign it to the #goals variable.
# Need to find all tasks for each goal ID and assign it to the #tasks variable. Goal ID needs to be supplied here, but it isn't as we're not in show action.
#tasks = Goal.find(1).tasks
As I said, I can find all tasks for a Goal when I manually enter the ID (1 in this example). This works fine in my app, no errors. But obviously I want to supply these IDs dynamically, and I'm just not sure how I get the params in there.
I have tried the below:
#tasks = Goal.find(params[:id]).tasks
and
#tasks = Goal.find(params[:goal_id]).tasks
And I get the "Couldn't find Goal without an ID" error when I try to iterate over #tasks in my view. Which makes sense, as I don't think the goal params are being passed to it as we're not in the Show action.
Surely there must be an easy Rails way?! I'm stumped and don't really know where to look. Thanks for your help and Happy New Year.
You are getting current user's goals, so when you will do this you have one array object. so when you will pass array object to find, it will have multiple IDS. so when need to find All the tasks from all goals you just need to pass Array of IDS instead of single value.
#tasks = Task.where(:goal_id => #goals)
This will run this SQL query.
SELECT "tasks".* FROM "tasks" WHERE "tasks"."goal_id" IN (SELECT
"goals"."id" FROM "goals")
So when you are dealing with array just pass ids. for e.g. [1,2,3]
Once you do #goals = #user.goals (assuming that's working, which it sounds like it is), you have your goals and there is no reason to go back to the DB to "find" them.
To get ALL your tasks from ALL of user's goals, you can do the following:
#tasks = []
#goals.each do |goal|
#tasks << goal.tasks
end
Ah of course, #goals is an array of the user's goals so I can just work with that. So simple when someone just tells you. Thanks for all your help!
Here's my final code that works (I left the controller unchanged). This gets the first goal in the array and then gets the tasks associated with each goal. I have a set number of goals so I can just use goals[0], goals[1] or goals[2] for each goal.
<% #goals[0].tasks.each do |task| %>
<li><div class="task-item"><%= task.task_name %></div></li>
<% end %>

Resources