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.
Related
The Review class you see below represent a review that a user submitted for a product. Somewhere else in the code, Review.recent is called with a product_id, which is just a unique number that represents a single product. Fill in the code to make it work as expected!
Review.recent - This function should return the 5 most recent reviews (sorted by submit_time) with the specified product_id.
<=> - This special Ruby function is called when comparing two objects for sorting. It returns 1, 0, or +1 depending on whether the object is less than, equal to, or greater than the other object. You'll want to sort items by submit_time so recent items appear first.
set_submit_time - This function is called right before a review is created. We can use Ruby's Time class to set submit_time to now so we know when the review was created.
I'm new to ruby and I want this code for my very important work so how can I complete it help me please!
class Review < ApplicationRecord
# Every Review has a product_id and submit time
attr_accessor :product_id
attr_accessor :submit_time
# Before the new record is created, we'll call the :set_submit_time method
before_create :set_submit_time
def self.recent(product_id)
# Return only the 5 newest results for this product
# Reference: https://ruby-doc.org/core-2.4.2/Enumerable.html
Review.all
end
def <=>(other_review)
# Implement the comparison function for sorting
# Reference: http://ruby-doc.org/core-2.4.2/Comparable.html
end
private
def set_submit_time
# Set the submit_time
# Reference: https://ruby-doc.org/core-2.2.4/Time.html
end
end
self.recent
This is asking you to order by submit_time and return the first 5 results.
To perform the ordering, see: https://apidock.com/rails/ActiveRecord/QueryMethods/order
To perform the limit, see: https://apidock.com/rails/ActiveRecord/QueryMethods/limit
If you're still stuck on this problem, please show us what you've tried.
<=>
If you click the link in the comment you provided (http://ruby-doc.org/core-2.4.2/Comparable.html), the solution is almost identical to that example.
If you're still stuck on this problem, please show us what you've tried.
set_submit_time
It's worth having a quick look at: https://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html - to understand what is meant by a callback. Basically, this method is going to get automatically called whenever a new record is created. (You probably could have guessed this, based on the fairly self-explanatory name: before_create!)
Again, the first example on that page is almost identical to your scenario. You can use Time.now to get the current time.
If you're still stuck on this problem, please show us what you've tried.
I have an application with a series of tests (FirstTest, SecondTest etc.)
Each test has a calculation set out its relevant model that gets calculated before being saved to the database set out like this:
#first_test.rb
before_save :calculate_total
private
def calculate_total
...
end
I then have an index page for each user (welcome/index) which displays the current user's results for each test. This all works fine, however I want to work out various other things such as each users average score overall etc.
Is it possible to access the current user from the welcome model?
Currently my welcome.rb is accessing the data follows:
#welcome.rb
def self.total
FirstTest.last.total
end
This obviously access the last overall test NOT the last test from the current user.
I feel like I may have just laid the whole application out in a fairly unintelligent manner...
Thanks in advance x
Well you need to save user_id in a column for each record in FirstTest. Then you can find the total for current user
FirstTest.where(:user_id => current_user.id).last.total
In my Rails application I would like to record the time a user was last_seen.
Right now, I do this as follows in my SessionsHelper:
def sign_in(user)
.....
user.update_column(:last_seen, Time.zone.now)
self.current_user = user
end
But this is not very precise because a user might log in at 8 a.m. and in the evening the last_seen database column will still contain that time.
So I was thinking to update last_seen whenever the user takes an action:
class ApplicationController
before_filter :update_last_seen
private
def update_last_seen
current_user.last_seen = Time.zone.now
current_user.save
end
end
But I don't like that approach either because the database gets hit with every action that a user takes.
So what might be a better alternative to this?
Rails actually has this sort of behavior built in with touch:
User.last.touch
#=> User's updated_at is updated to the current time
The time it takes in any well-provisioned DB to handle updating a single column like this should be well under 5ms, and very likely under 1ms. Provided you're already going to be establishing that database connection (or, in Rails' case, using a previously established connection from a pool), the overhead is negligible.
To answer your question about whether your code is slower, well, you're thinking about this all wrong. You can optimize an already very fast operation for performance, but I instead you worry more about “rightness”. Here is the implementation of ActiveRecord's touch method:
def touch(name = nil)
attributes = timestamp_attributes_for_update_in_model
attributes << name if name
unless attributes.empty?
current_time = current_time_from_proper_timezone
changes = {}
attributes.each do |column|
changes[column.to_s] = write_attribute(column.to_s, current_time)
end
changes[self.class.locking_column] = increment_lock if locking_enabled?
#changed_attributes.except!(*changes.keys)
primary_key = self.class.primary_key
self.class.unscoped.update_all(changes, { primary_key => self[primary_key] }) == 1
end
end
Now you tell me, which is faster? Which is more correct?
Here, I'll give you a hint: thousands of people have used this implementation of touch and this very code has likely been run millions of times. Your code has been used by you alone, probably doesn't even have a test written, and doesn't have any peer review.
“But just because someone else uses it doesn't make it empirically better,” you argue. You're right, of course, but again it's missing the point: while you could go on building your application and making something other humans (your users) could use and benefit from, you are spinning your wheels here wondering what is better for the machine even though a good solution has been arrived upon by others.
To put a nail in the coffin, yes, your code is slower. It executes callbacks, does dirty tracking, and saves all changed attributes to the database. touch bypasses much of this, focusing on doing exactly the work needed to persist timestamp updates to your models.
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?
I want to display a random record from the database for a certain amount of time, after that time it gets refreshed to another random record.
How would I go about that in rails?
Right now I'm looking in the directions of cronjobs, also the whenever gem, .. but I'm not 100% sure I really need all that for what seems to be a pretty simple action?
Use the Rails.cache mechanism.
In your controller:
#record = Rails.cache("cached_record", :expires_in => 5.minutes) do
Model.first( :offset =>rand(Model.count))
end
During the first execution, result gets cached in the Rails cache. A new random record is retrieved after 5 minutes.
I would have an expiry_date in my model and then present the user with a javascript timer. After the time has elapsed, i would send a request back to the server(ajax probably, or maybe refreshing the page) and check whether the time has indeed expired. If so, i would present the new record.
You could simply check the current time in your controller, something like:
def show
#last_refresh ||= DateTime.now
#current ||= MyModel.get_random
#current = MyModel.get_random if (DateTime.now - #last_refresh) > 5.minutes
end
This kind of code wouldn't scale to more servers (as it relies on class variables for data storage), so in reality you would wan't to store the two class variables in something like Redis (or Memcache even) - that is for high performance. Depends really on how accurately you need this and how much performance you need. You could as well use your normal database to store expiry times and then load the record whose time is current.
My first though was to cache the record in a global, but you could end up with different records being served by different servers. How about adding a :chosen_at datetime column to your record...
class Model < AR::Base
def self.random
##random = first(:conditions => 'chosen_at NOT NULL')
return ##random unless ##random.nil? or ##random.chosen_at < 5.minutes.ago
##random.update_attribute(:chosen_at,nil) if ##random
ids = connection.select_all("SELECT id FROM things")
##random = find(ids[rand(ids.length)]["id"].to_i)
end
end