I have a User and Post model and am using the acts_as_votable gem (which gives me a 'votes' table) to upvote/downvote a post. I want to assign karma to each user, where karma is the number of upvotes a user has gained from all his/her posts minus the total number of downvotes.
Currently I have this instance method on the User's model to calculate karma:
def karma
count = 0
self.posts.each do |post|
count += post.upvotes.size - post.downvotes.size
end
return count
end
I don't think the above is very efficient with a run time of O(2n), because for every post two additional database queries are required, one for the upvotes, and one for the downvotes.
Any ideas on how to combined the above into a single query, or otherwise make it more efficient?
Not only about efficiency, such design is not good in terms of OOP. The User model considers too much beyond its scope. Karma is something belongs to user and should not be tied in anything else.
A better approach is to separate "Karma" from Post.
# Add a "karma" column
$ rails g migration AddKarmaToUser
# Or use a dedicated table
class User < ActiveRecord::Base
has_one :karma
Then, use Controller or service object to change karma. I'll cover Controller here for simplicity. Do not use Model callbacks as that is cross models.
class PostsController < ApplicationController
def upvote
post = Post.find(params[:id])
post.upvote # pseudo API of acts_as_votable
current_user.increase_karma
end
def downvote
# ...
current_user.decrease_karma
end
end
# Model
class User < ActiveRecord::Base
attr_accessible :karma
def increase_karma(count=1)
update_attribute(:karma, karma + count)
end
def descrease_karma(count=1)
# ...
end
end
# View
user.karma
Related
I have a ruby on rails site which has a user model and a post model
# models/user.rb
class User < ActiveRecord::Base
has_many :posts
end
# models/post.rb
class Post < ActiveRecord::Base
belongs_to :user
is_impressionable
end
As you can see, I've used the impressionist gem to count each post's page views.
To get a particular post's page view count, I use post.impressionist_count.
How do I efficiently get the sum of page views of all the posts by a particular user? It's preferable to store this value in a database, as I may need it for more operations. Also, if I store this value in a database, how do I update it each time a post's view count changes? Thanks in advance!
Update
Thank you all for your answers. I've tried to incorporate your solutions, but I'm still not able to find one.
I used counter_cache
add_column :posts, :view_count, :integer, default: 0
# models/post.rb
is_impressionable :counter_cache => true, :column_name => :view_count
Added a column to the user table
add_column :users, :total_view_count, :integer, default: 0
Then did this
# posts_controller
before_action :update_user_total_view_count, only: :show
private
def update_user_total_view_count
#post = Post.find(params[:id]);
#post.user.update_user_total_view_count
end
# user model
def update_user_total_view_count
update(total_view_count: posts.sum(:view_count))
end
I can't use posts.sum(:impressionist_count) as it is not a column in the database. I thought I had got this working, but the view_count on the post model doesn't update properly and is stuck at 1 even though the impressionist_count increases. Any solutions? Thanks!
I think you can make something like this:
total_impressions = Rails.cache.fetch("user:#{particular_user.id}:total_impressions") do
Post.where(user: particular_user).reduce(0) do |sum, post|
sum + post.impressionist_count(:filter=>:all)
end
end
As you can see this code calculate total impressions count and cache it.
I don't known impressionist gem very well but probably it is possible to improve this code and make calculation of total impressions on DB side.
I think I'd create an attribute on the user model:
rails g migration add_total_post_views_to_users total_post_views:integer
Then have a before action on the show action (Controller):
class PostsController < ApplicationController
before_action :update_total_post_views, only: :show
def show
# code for displaying the post
end
private
def update_total_post_views
Post.find(params[:id]).user.update_total_post_views
end
User Model:
class User
has_many :posts
# more code...
def update_total_post_views
update(total_post_views: posts.sum(:impressionist_count)
end
You may need to use ActiveRecord built-in mechanism of counter_cache, you are defining new column which will store the cached value. There is very quick description how to proceed:
https://coderwall.com/p/tlvgag/easily-keep-track-of-rails-relationship-count-with-counter_cache
What described above is to implement the solution on your own, but the gem has it's own way to store cached counter. Have you tried what is described here?
https://github.com/charlotte-ruby/impressionist#adding-a-counter-cache
I have a requirement where I need to calculate the average of units sold for a product based on the company they were sold at. From there I will calculate the percentage difference to unit sold. There is one model with products in it. Each product has the attributes of:
product_name
unit_sold
company
There are many companies.
This code works for calculating the average on all records, however I'd like to calculate the average conditionally based on the attribute 'company'.
def average_UnitSold
self.class.average(:unit_sold)
end
def averagechange_UnitSold
(self.unit_sold - average_UnitSold) / average_UnitSold * 100
end
I came up with this, but it is not working:
def average_UnitSold
self.class.sum(:unit_sold), :conditions => "company = self.company")) / :unit_sold
end
Any ideas?
On another note, is a more viable approach storing all these averages somewhere and only updating them on a daily basis more efficient?
Based on the answer, I have now implemented this code, and it seems to work:
def self.average_unit_sold(company)
where(company: company).average(:unit_sold)
end
def average_unit_sold
self.class.average_unit_sold(self.company)
end
def averagechange_UnitSold
(self.unit_sold - average_unit_sold) / average_unit_sold * 100
end
It's very strange that you're doing this in an instance method, since the result doesn't actually have anything to do with a particular instance. Instead, define a class method:
class Product < ActiveRecord::Base
# `self.average_unit_sold` is a class method that takes `company` as an
# argument and executes an SQL query like this (where 'some_company' is the
# company given in the argument):
#
# SELECT AVG(products.unit_sold) FROM products
# WHERE products.company = 'some_company'
#
def self.average_unit_sold(company)
where(company: company).average(:unit_sold)
end
end
# ...then...
Product.average_unit_sold(some_company)
If you really want to have an instance method, you can add one (but keep the logic in a class method):
# `average_unit_sold` is an instance method that takes the value of the
# instance's own `company` attribute and calls `Product.average_unit_sold`:
def average_unit_sold
self.class.average_unit_sold(self.company)
end
(This could also be a scope, but for aesthetic reasons I prefer to use scopes only when the result is a model instance or collection of instances, which isn't the case here.)
Assuming you have your associations set up correctly this is pretty easy to accomplish. So assuming that a Company has many products:
class Company < ActiveRecord::Base
has_many :products
end
class Product < ActiveRecord::Base
belongs_to :company
end
Average units sold for all companies:
Product.average(:unit_sold)
Average units sold for one company:
company = Company.find(1)
company.products.average(:unit_sold)
I'm trying to retrieve an associated column named "contribution_amount" for each user but I'm getting undefined method error and I can't figure out why.
Controller has:
#payments = Payment.where(:contribution_date => Date.today).pluck(:user_id)
#users = User.where(:id => #payments).find_each do |user|
user.payments.contribution_amount
end
models have:
class User < ActiveRecord::Base
has_many :payments
end
class Payment < ActiveRecord::Base
belongs_to :user
end
Exact error in console is
`undefined method `contribution_amount' for #<ActiveRecord::Associations::CollectionProxy::ActiveRecord_Associations_CollectionProxy_Payment:0x007fb89b6b2c08>`
user.payments is a scope; that is, it represents a collection of Payment records. The contribution_amount method is only available on individual Payment records. You could say user.payments.first.contribution_amount, but I'm not sure that's your goal.
Are you trying to sum the contribution amounts? In that case, you'd want to use a method which aggregates collections of records: user.payments.sum(:contribution_amount).
Veering off-topic for a moment, it is generally better to push scoping methods down into your models. For example:
class User < ActiveRecord::Base
def self.with_payment_contribution_after(date)
joins(:payments).merge(Payment.with_contribution_after(date))
end
def self.with_contribution_amount
joins(:payments).group("users.id")
.select("users.*, sum(payments.contribution_amount) as contribution_amount")
end
end
class Payment < ActiveRecord::Base
def self.with_contribution_after(date)
where(:contribution_date => date)
end
end
# In your controller
#users = User.with_payment_contribution_after(Date.today)
.with_contribution_amount
# In a view somewhere
#users.first.contribution_amount
The advantages to structuring your code this way are:
Your scopes are not longer locked away in a controller method, so you can easily reuse them other places.
Your controller method can become simpler and more declarative. That is, it can express what information it wants, not how that information is acquired.
Breaking scopes down into smaller pieces implies that our code is better decomposed, and that which has been decomposed can be recomposed.
It's easier to test scopes via model unit tests then via controller testing.
I have a rails app, with two separate DB tables, users and products. A user has_many products, and a product belongs_to a user.
When I create a product, I want it to automatically add the user_id to the user_id database column in the products table. What changes to my mvc do I need to make to ensure that the correct user_id is added when a new product is created?
You can scope the creation of the new product through the user.
For example, instead of this:
Product.create(params[:product])
you do this:
current_user.products.create(params[:product])
where "current_user" is the user creating the product.
Just as a suggestion, you may want to go back and accept the answers to some of your previous questions, which will improve your response rate and increase the likelihood someone will answer your questions in the future.
There are a few ways to do this, one approach:
Create current user function
class ApplicationController < ActionController::Base
private
# Finds the User with the ID stored in the session with the key
# :current_user_id This is a common way to handle user login in
# a Rails application; logging in sets the session value and
# logging out removes it.
def current_user
#_current_user ||= session[:current_user_id] &&
User.find_by_id(session[:current_user_id])
end
end
http://guides.rubyonrails.org/action_controller_overview.html#session
Make sure to be cognizant of security concerns. A gem like Devise can also help.
Add to products controller
class ProductsController < ApplicationController
def create
current_user.products.create! params[:product] # make sure attr_accessible is setup on products
end
end
I am building an Invoicing Application that basically follows the following pattern:
Users < Clients < Projects < Invoices
Now in order to generate autoincrementing invoice numbers for each User I put this in my Invoice model:
before_create :create_invoice_number
def create_invoice_number
val = #current_user.invoices.maximum(:number)
self.number = val + 1
end
However, it seems that the current_user variable cannot be accessed from within models in Rails?
What can I do to solve this problem?
This is due to separation of concerns in Rails and is a somewhat sticky issue to deal with. In the Rails paradigm, models should have no knowledge of any application state beyond what they're passed directly, so most Rails coders will tell you that any model needing to know about a current_user is code smell.
That said, there are three ways to do this, each "more correct" (or at least I would consider them so).
First, try creating an association to the user inside the invoice and link the invoice to the user in the controller:
class InvoicesController < ApplicationController
...
def create
#invoice = current_user.invoices.create(params[:invoice])
...
end
And in your model:
belongs_to :user
def create_invoice_number
self.user.invoices.maximum(:number) + 1
end
If that doesn't work, do this manually in the controller. It's true that controllers should always be as skinny as you can manage, but since this is clearly an application-level concern the controller is the place to put it:
class InvoicesController < ApplicationController
...
def create
#invoice = Invoice.create(params[:invoice])
#invoice.update_attribute(:number, current_user.invoices.maximum(:number))
...
end
Lastly, if you really, really want to bridge the controller and model, you can do so with ActionController::Sweepers. They are not intended for this purpose but will certainly get the job done for you.
there should not be any arise of such case still if you want then make use of observers in rails