I have three models:
Course
User
CourseCompletion
In addition to stuff like title and content, each course also has a point value. When a user completes a course, they are awarded the point value for the course, which is added to their total_points. CourseCompletion simply tracks which user has completed which courses (with columns user_id, course_id and completion_date).
One weakness with this data model is that if an admin user edits the point value of a course after a user has completed that course, the user's points are not updated. I need a way to do this retroactively. For example, if a user completes a course and earns 10 points, and then an admin changes the course to be worth 20 points, the user should have 20 points total in the end. I haven't done this sort of thing before - what would be the best approach?
My current plan is two-fold. In the first part, I make changes to the Course and CourseCompletion models:
Add a points_earned column to CourseCompletion that records how many points the user has earned for that completion.
Add a points_recently_changed column to Course. If a course's points are updated at any time, set this column to true for that course. (see my related question)
In the second part, a script or task (?) runs once per day and does the following:
Get all courses where points_recently_changed equals true
Find all course completions for those courses
Calculate the difference between course.points and course_completion.points_earned
Update the corresponding user's point total accordingly
Change course.points_recently_updated back to false.
Are there any glaring problems with this approach, or is there a "Rails Way" of doing stuff like this?
Why don't you use ActiveRecord::Calculations to get the sum of the points for the whole related courses and store them in the column. Update the column each time the admin does a change in the course points.
You can track changes in the points using ActiveRecord::Dirty
http://api.rubyonrails.org/classes/ActiveModel/Dirty.html
And calculate points using Calculations:
http://api.rubyonrails.org/classes/ActiveRecord/Calculations.html
As a possible solution:
class Course < ActiveRecord::Base
before_save :update_user_points
def update_user_points
User.all.each {|user| user.update_points } if self.points_changed?
end
end
class User < ActiveRecord::Base
def update_points
self.points = self.courses.sum(:points)
end
end
Suggestion:
I dislike saving the points in the database as it is a variable. I suggest you to do the calculation each time the user logins and keep it as cached number with expire date. So it has to be recalculated each day.
I tried Jorge's suggestion but was not satisfied with it. I ended up going with a similar approach whereby I recalculate a user's points during the login process.
User.rb
def recalculate_points
new_course_points = self.course_completion_courses.sum(:worth)
self.update_attribute(:points, self.course_completion_courses.sum(:worth))
self.save
end
session_helper.rb
def sign_in(user)
...
current_user.recalculate_points
end
Points are still stored in the User table - simply caching them doesn't work because I do some reporting that needs that information to persist in the database.
Related
The Challenge:
I need to find the most popular discussion in a forum.
Background Information:
forum has many discussions
discussion belongs to forum
discussion has an attribute called views which stores how many
times a user has viewed a discussion.
using a postgres database.
My Solution:
Create an instance method in the Forum model that loops through every single discussion and sees how many view each one has:
def most_popular_discussion
record_view = 0
self.discussions.each do |d|
record_views = d.views if d.views > record_views
end
record_views
end
Why I've Made A Question:
My solution appears to be disastrously inefficient as it queries the discussion table for every single entry. This method will get slower and slower as the database gets bigger and bigger. I wouldn't mind too much, but the most_popular_discourse method is also going to be requested a lot (on every user's profile page), and will really slow things up.
So how should I find the largest integer in a table? or (and I think this is probably the better way) should I actually save the record number of views, rather than working it out every time?
Maybe have another table called statistics for my application to use, with just two columns, name:string and information:string and use it store miscellaneous statistics?
Then, every-time someone views a discussion, I'd do something like this:
def iterate_views(ip)
current_views = self.views + 1
self.views = current_views
record_views_statistic = Statistic.find_by(name: 'record_views')
record_views_statistic.update_attributes(information: current_views.to_s) if current_views > record_views_statistic.information
# I convert current_views to a string before saving because the statistics table's `information` column holds strings in order to keep the table open and miscellaneous.
end
what do you think about that approach? Both interact with the database a fair bit, but this second approach wouldn't slow down proportionally to the amount of data in the database.
This approach will give you the most popular discussion, and is much simpler than your two solutions.
def most_popular_discussion
self.discussions.order(views: :desc).first
end
To get the highest number of views, you could either use most_popular_discussion.views or use a function like:
def record_views
self.discussions.maximum(:views)
end
Note that I've included ways to find both the most viewed discussion and the highest number of views, because your challenge says you'd like to find the most popular discussion but both of your solutions just seem to find the record number of views among a forum's discussions.
As for your solutions, your second one seems to be closer to a good solution, but why not just cache the most popular discussion's views count in the Forum model? Say we add a record_views column to the forums table.
class Discussion < ActiveRecord::Base
belongs_to :forum
def iterate_views
self.views += 1
if self.forum.present? && self.views > self.forum.record_views
self.forum.record_views = self.views
end
end
end
Then, to find the most popular discussion in the Forum model (assuming ties don't matter):
def most_popular_discussion
self.discussions.where(views: self.record_views).first
end
In my Rails app I have an Invoice model with the attributes date and due_date.
For reasons of simplicity I don't want the user to manually enter the due_date but rather simply enter the number of days that should be added to the date.
This is why I set up a virtual attribute days_allowed.
class Invoice < ActiveRecord::Base
belongs_to :user
before_save :save_date
attr_accessor :days_allowed
def days_allowed # attribute reader (I need that too!)
(due_date - date).to_i
end
def save_date
self.due_date = date + days_allowed.days
end
end
However, when a user picks a date in the form and enters a number of days, e.g. 10, I get wrong results because the save_date callback refers to the days_allowed method rather than the attribute of the same name.
The key problem seems to be that I am using the callback on two different attributes that depend on each other (date and days_allowed).
Can anybody tell me how to solve this puzzle?
Thanks for any help.
How about this approach (no before_save is necessary):
class Invoice < ActiveRecord::Base
belongs_to :user
def days_allowed
(due_date - date).to_i
end
def days_allowed=(days)
self.due_date = date + days
end
end
EDIT Not supposed to work with mass assignment when both date and days_allowed are present unless date always goes first.
I think you need to store all three fields, just not provide any form attributes for editing the calculated one.
While your use-case at it is might seem like this is a violation of 'don't repeat yourself', I think this is an instance of coincidental duplication. Let me explain:
Consider your next problem - you are going to start having due dates on weekends, holidays, etc. For exactly that reason I wrote the business_time gem:
https://github.com/bokmann/business_time
so in your solution where you add 10 days, you can now add 10 business days. The exact due date can now fluctuate across weekends, 3-day weekends created by holidays, etc. And if you have to deal with snow days, like most of the East Coast of the U.S. has had to lately, you can just add tham as a holiday and recalculate. The number of business days is still 10, but the due date has changed. The number of business days was a critical piece of data to save, not a piece of data to calculate. The due data needs to be saved, because its a piece of information calculated in a context that you'll want to save for querying rather than try to recalculate every time.
In my current application, I need the ability to track points on a weekly basis so that the point totals for the user reset back to zero each week. I was planning on using the gem merit: https://github.com/tute/merit to track points.
In my users profile I have a field that is storing the points. What I have been unable to locate is how I can have rails on an auto basis for all users clear this field.
I have come across some information Rails reset single column I think this may be the answer in terms of resetting it every Sunday at a set time -- but I am uncertain on this last part and in addition where the code would go (model or controller)
Also, would welcome any suggestions if their is a better method.
You'd be better making a Point model, which belongs_to :user
This will allow you to add any points you want, and can then query the table based on the created_at column to get a .count of the points for the timespan you want
I can give you more info if you think it appropriate
Models
One principle we live by is to extend our models as much as possible
You want each model to hold only its data, thus ensuring more efficient db calls. I'm not super experienced with databases, but it's my opinion that having a lot of smaller models is more efficient than one huge model
So in your question, you wanted to assign some points to a user. The "right" way to do this is to store all the points perpetually; which can only be done with its own model
Points
#app/models/point.rb
Class Point < ActiveRecord::Base
belongs_to :user
end
#app/models/user.rb
Class User < ActiveRecord::Base
has_many :points
end
Points table could look like this:
points
id | user_id | value | created_at | updated_at
Saving
To save the points, you will literally just have to add extra records to the points table. The simplest way to achieve this will be to merge the params, like this:
#app/controllers/points_controller.rb
class PointsController < ApplicationController
def new
#points = Point.new
end
def create
#points = Point.new(points_params)
#points.save
end
private
def points_params
params.require(:points).permit(:value).merge(:user_id => current_user.id)
end
end
You can define the "number" of points by setting in the value column (which I'd set as default 1). This will be how StackOverflow gives different numbers of points; by setting the value column differently ;)
Counting
To get weekly countings, you'll have to create some sort of function which will allow you to split the points by week. Like this:
#app/models/point.rb -> THIS NEEDS MORE WORK
def self.weekly
where(:created_at => Time.now.next_week..Time.now.next_week.end_of_week)
end
That function won't work as it is
I'll sort out the function properly for you if you let me know a little more about how you'd like to record / display the weekly stats. Is it going to be operated via a cron job or something?
Based on your description, you might want to simply track the users points and the time that they got them. Then you can query for any 1 week period (or different periods if you decide you want all-time, annual, etc) and you won't lose historical data.
I have a "Vote" table in my database which is growing in size everyday, currently at around 100 million rows. For internal analytics / insights I used to have a rake task which would compute a few basic metrics, like the number of votes made daily in the past few days. It's just a COUNT with a where clause on the date "created_at".
This rake task was doing fine until I deleted the index on "created_at" because it seems that it had a negative impact on the app performance for all the other user-facing queries that didn't need this index, especially when inserting a new row.
Currently I don't have a lot of insights as to what is going on in my app and in this table. However I don't really want to add indexes on such a large table if it's only for my own use.
What else can I try ?
Alternately, you could sidestep the Vote table altogether and keep an external tally.
Every time a vote is cast, a separate tally class that keeps a running count of votes cast will be invoked. There will be one tally record per day. A tally record will have an integer representing the number of votes cast on that day.
Each increment call to the tally class will find a tally record for the current date (today), increment the vote count, and save the record. If no record exists, one will be created and incremented accordingly.
For example, let's have a class called VoteTally with two attributes: a date (date), and a vote count (integer), no timestamps, no associations. Here's what the model will look like:
class VoteTally < ActiveRecord::Base
def self.tally_up!
find_or_create_by_date(Date.today).increment!(:votes)
end
def self.tally_down!
find_or_create_by_date(Date.today).decrement!(:votes)
end
def self.votes_on(date)
find_by_date(date).votes
end
end
Then, in the Vote model:
class Vote < ActiveRecord::Base
after_create :tally_up
after_destroy :tally_down
# ...
private
def tally_up ; VoteTally.tally_up! ; end
def tally_down ; VoteTally.tally_down! ; end
end
These methods will get vote counts:
VoteTally.votes_on Date.today
VoteTally.votes_on Date.yesterday
VoteTally.votes_on 3.days.ago
VoteTally.votes_on Date.parse("5/28/13")
Of course, this is a simple example and you will have to adapt it to suit. This will result in an extra query during vote casting, but it's a hell of a lot faster than a where clause on 100M records with no index. Minor inaccuracies are possible with this solution, but I assume that's acceptable given the anecdotal nature of daily vote counts.
It's just a COUNT with a where clause on the date "created_at".
In that case the only credible index you can use is the one on created_at...
If write performance is an issue (methinks it's unlikely...) and you're using a composite primary key, clustering the table using that index might help too.
If the index has really an impact on the write performance, and it's only a few persons which run statistics now and then, you might consider another general approach:
You could separate your "transaction processing database" from your "reporting database".
You could update your reporting database on a regular basis, and create reporting-only indexes only there. What is more queries regarding reports will not conflict with transaction-oriented traffic, and it doesn't matter how long they run.
Of course, this increases a certain delay, and it increases system complexity. On the other hand, if you roll-forward your reporting database on a regular basis, you can ensure that your backup scheme actually works.
Frustrated with the Active Record Reputation gem, which I found very buggy, I'm trying to make my own reputation system for a Rails app. It's very primitive. I created a Contribution resource with a user_id and a value field, with an association between User.rb and Contribution.rb. Every time a user contributes to the app in some way, they get some points. If they ask a question, these lines get included in the create action of the Questions controller.
#contribution = current_user.contributions.build({:value => 3})
#contribution.save
If a user edits some Tags on the site, I do the same thing to reward superusers for their administrative work
#contribution = current_user.contributions.build({:value => 2})
#contribution.save
It then becomes very easy to calculate a user's total reputation.
One problem with this is that, in an imaginary world where users care about this app and their reputation, it would be very easy to game the system. For example, a user could just keep updating the categories or tags, and every time they do so they get 2 more points. Therefore, I wanted to somehow record what type of action the user did.
Right now, all of the work users can earn points for is somehow associated with a Question.rb, however, they get points for updating Tags, updating Categories, upvoting other people's answers etc, therefore merely storing the question_id in the contributions model wouldn't be sufficient.
Based on what I told you, can you give me some idea how I might build out the Contributions resource in order to accomplish what I want?
For example, I thought of one way of doing it that would have left a lot of null fields in my database, so I assumed it wasn't a good way. I could add a question_id and several boolean fields such as 'answering_question' 'updating_category' 'updating_tags' and each time an action is performed, record with a 'true' whether, for example, 'updating_category' is being performed. However, as mentioned, if I start rewarding lots of different types of contributions, there's going to be a lot of columns in each row that aren't being used.
I'm not sure if that's a 'real problem' (i've read about it but not sure how necessary it is to avoid), or if there's a better way of recording what type of activity each user is engaging in to earn points.
some of the current associations
User has_many :answers
Question.rb has_many :categories
Question.rb has_many :tags
for my rails application I am using thumps_up gem which is better than active_record_reputations_system ,Its more simple.
https://github.com/bouchard/thumbs_up