Getting an array for user ids from an array of users - ruby-on-rails

I am using the amistad gem to handle friend relationships. Users have events associated with them. I would like to provide a feed of events for the a given user based on who they are friends with.
I have used the following code from http://ruby.railstutorial.org for follow relationships. However with amistad i don't have a user.friend_ids method only a user.friends method.
How can I get a similar feed type of result (that can be paged and all that) with the user.friends call that gives me a list of user objects and not just the ids?
class Micropost < ActiveRecord::Base
default_scope :order => 'microposts.created_at DESC'
# Return microposts from the users being followed by the given user.
scope :from_users_followed_by, lambda { |user| followed_by(user) }
private
# Return an SQL condition for users followed by the given user.
# We include the user's own id as well.
def self.followed_by(user)
following_ids = %(SELECT followed_id FROM relationships
WHERE follower_id = :user_id)
where("user_id IN (#{following_ids}) OR user_id = :user_id",
{ :user_id => user })
end
end
This is mostly pseudocode as it doesn't work, but here's what I think I'm trying to accomplish in code:
class Event< ActiveRecord::Base
default_scope :order => 'event.created_at DESC'
# Return events from friends of a user.
scope :from_friends, lambda { |user| friends_of(user) }
private
# Return an SQL condition for users followed by the given user.
# We include the user's own id as well.
def self.friends_of(user)
friend_ids = %(SELECT friendIDs FROM friendships)
where("user_id IN (#{friend_ids})")
end
end

You can manually add friend_ids method to the User model.
def friend_ids
self.friends.map(&:id)
//Here, I'm iterating over user.friends array and getting an array of ids
end
EDIT: As per your comment, I'm assuming you have the associations between user and events built up properly.
e.g A user has many events and an event belongs to a user/multiple users (depending on your requirements.)
Once you have the associations setup correctly, you can simply lookout for events with the user_ids which you got from above friend_ids method.

Related

How to create a scope based on user.photos.count

I have a User model and a Photo model. If I go into Heroku Console, I can search the database, for example:
u = User.find(2)
u.photos.count
=> 25
I want to create a scope in my User model so that I can sort the users based on their photos.count number and then paginate the users.
Class User
scope :photocount ??????
UsersController
def index
#users = User.photocount.paginate(page: params[:page])
end
What you want is counter_cache. Add a column photos_count to user model and in Photo model:
belongs_to :user, counter_cache:true
Then you can User.order(:photos_count).page(params[:page])
See more about counter cache in rails guides here
Scope will look like:
scope :by_photos_count, ->{ order(:photos_count) }
Without counter cache it is still possible, but will be very inefficient:
User.joins('join (select user_id, count(*) as sort from photos group by user_id) as sort ON users.id=sort.user_id').order('sort.sort').page(params[:page])

Which rails model do I put a multiple table query in

I have two tables called
Product (prodID: integer, prodName: string, userID: FK)
and
User(userID:integer,userName:string).
The user can have many products. I want to write a query that gets me all the products for userID=10. I don't however understand which model I should put this in- the user or the product model or does it not matter? Presumably the output of the model will be fed to the controller it relies on so I should put it in the model that relates to the view I want to show it in? Is this correct?
You can directly use association method, no need of writing model method for fetching user's products.
In user.rb:
has_many :products
In product.rb
belongs_to :user
and from controller
User.where('id = ?', params[:id]).first.try(:products)
So, above query will fetch products if user with given id is found.
In your controller:
#user = User.find(params[:id])
#products = User.of_products(params[:id])
If you don't want to use #user in your action then you can avoid calculating #user.
In user.rb:
has_many :products
def self.of_products(user_id)
User.includes(:products).where(id: user_id)
end
This will give you all products of #user

Unsure how to model a many-to-many relationship with a flag in Mongoid

I have two entities, projects and users. These are modeled in Rails using Mongoid with two Document instances, User and Project.
In this system, one user can create one project, but many users can follow many projects. For example, as user_id 1 I've created project_id 1. But user_ids 10, 11, 40, and 60 all follow project_id 1. I need to represent a many-to-many relationship between users and projects, and represent a specific user_id as the creator of the project, to assign him editing rights.
Practically speaking, when a user logs-in, he needs to be able to see all projects that he is following, including any that he created, commingled with other projects created by other users. His special creator status wont influence this list at all. When a user looks at a specific project, he needs to be able to view all users following a project, and if he's a creator, he can add new followers and delete existing ones.
In a RDBMS I would represents this with tables users, projects and a users_projects join table with a flag of is_creator. This would easily let me select which projects a user can see, and which users are followers, including which users are creators, of projects.
Mongoid supports many-to-many relationships, but unlike in an RDBMS there's no way for me to put a flag on the relationship. Instead, I'm thinking I'll add a creator field to the projects document, which will contain a link back to an _id field on the users document.
The user->projects relationship might look like this
class User
has_and_belongs_to_many :projects
end
class Project
has_and_belongs_to_many: users
end
But I can't figure out how to map the creator->created_projects relationship. I believe I can reference a user creator in Project like belongs_to :creator, :class_name => 'User' but I'm not sure how to set up the other side.
How best can I model these relationships in Mongoid?
the second version uses less space but you need an extra query to get the user details like usernames. An object ID has 12bytes, max. document size is 16mb so the array could hold around 1.3M user ids (theoretically!)..
here you go:
user.rb
class User
# projects this user owns
has_many :projects
has_many :followed_projects,
:class_name => 'Project',
:foreign_key => :follower_ids
# Uncomment if the relation does not work
#def followed_projects
# Project.where(:follower_ids => self.id)
#end
# get projects that this user has created and projects he is following
def related_projects
Project.any_of({:user_id => self.id}, {:follower_ids => self.id})
end
end
project.rb
class Project
# creator
belongs_to :user
field :follower_ids, :type => Array
# adds a follower
def add_follower!(user_obj)
# adds a user uniquely to the follower_ids array
self.add_to_set(:follower_ids, user_obj.id)
end
def remove_follower!(user_obj)
# remove the user
self.pull(:follower_ids, user_obj.id)
end
end
How to work with it:
#project = Project.first
#some_user = User.last
#project.add_follower!(#some_user)
#some_user.followed_projects
#some_user.related_projects
# create hash like #ids_to_user[user_id] = user
#ids_to_users = User.find(#project.follower_ids).inject({}) {|hsh, c_user| hsh[c_user.id] = c_user; hsh}
#project.followers.each do |c_follower|
puts "I'm #{#ids_to_users[c_follower].username} and I'm following this project!"
end
#project.remove_follower!(#some_user)
create an embedded document which holds all followers with their user_id and their username so you won't have to query the follower's usernames.
The benefits:
Not a single query to lookup a project's followers
Only a single query to lookup a user's followed projects
The downside:
If a user changes his name, you'll have to update all his "followships" but how often
do you change your name compared to how often you lookup your followed projects ;)
If you have many thousand followers per project you may reach the document limit of 16mb
user.rb
class User
# projects this user owns
has_many :projects
def followed_projects
Project.where('followers.user_id' => self.id)
end
# get projects that this user has created and projects he is following
def related_projects
Project.any_of({:user_id => self.id}, {'followers.user_id' => self.id})
end
end
project.rb
class Project
# creator
belongs_to :user
embeds_many :followers
# add an index because we're going to query on this
index 'followers.user_id'
# adds a follower
# maybe you want to add some validations, preventing duplicate followers
def add_follower!(user_obj)
self.followers.create({
:user => user_obj,
:username => user_obj.username
})
end
def remove_follower!(user_obj)
self.followers.destroy_all(:conditions => {:user_id => user_obj.id})
end
end
follower.rb
class Follower
include Mongoid::Document
include Mongoid::Timestamps
embedded_in :project
# reference to the real user
belongs_to :user
# cache the username
field :username, :type => String
end
How to work with it:
#project = Project.first
#some_user = User.last
#project.add_follower!(#some_user)
#some_user.followed_projects
#some_user.related_projects
#project.followers.each do |c_follower|
puts "I'm #{c_follower.username} and I'm following this project!"
end
#all_follower_user_ids = #project.followers.map{|c| c.user_id}
# find a specific follower by user_id
#some_follower = #project.followers.where(:user_id => 1234)
# find a specific follower by username
#some_follower = #project.followers.where(:username => 'The Dude')
#project.remove_follower!(#some_user)
PS: If you want a simpler solution, you could just embedd an array of ObjectIDs (user_ids) in the project and use the atomic updates $addToSet and $pullAll to add/remove a follower. But you'd need an extra query like User.where(:user_id.in => #project.follower_ids) (assuming the array is called follower_ids) to grab all users and their names ;)

Combining scoped activerecord queries in rails

I'm trying to build a facebook style feed of items for a user. The feed will contain recent notes (on books) made by a user or people the user follows combined with other notifications such as "user x that you follow started reading a new book". You get the idea.
So far I have a scope in my Note class which returns the notes I want:
class Note < ActiveRecord::Base
scope :from_users_followed_by, lambda { |user| followed_by user }
def self.followed_by(user)
followed_ids = %(SELECT followed_id FROM relationships WHERE follower_id = :user_id)
where("user_id IN (#{followed_ids}) OR user_id = :user_id", { :user_id => user })
end
end
and a similar scope in my Readings class which returns records built when user starts reading a book:
class Reading < ActiveRecord::Base
scope :from_users_followed_by, lambda { |user| followed_by(user) }
def self.followed_by(user)
# is this not at risk of sql injection??
followed_ids = %(SELECT followed_id FROM relationships WHERE follower_id = :user_id)
# return readings where user_id IN (an array of user_ids that the user follows)
where("reader_id IN (#{followed_ids}) OR reader_id = :user_id", { :user_id => user })
end
end
Now this works fine and I can get arrays of objects from these no problem. I'm struggling to combine the two queries into a feed which is correctly ordered by creation time. The best I can do at the moment is my user class with a combined feed method:
class User < ActiveRecord::Base
def combined_feed
feed = Note.from_users_followed_by(self) | Reading.from_users_followed_by(self)
feed.sort! do |a, b|
a.created_at <=> b.created_at
end
feed.reverse
end
end
Which gets me a combined feed but strikes me as being horrendously inefficient. How can I do the equivalent at the database level in rails?
I think I would probably create an entirely separate model called FeedItem. Then, when certain events occur (such as the creation of a new note), you just create a new FeedItem record. Then you only have one table to query from and it will already be in the correct order.

Ruby on Rails: how do you write a find statement that sums up a bunch of values if an object belongs to another object?

Lets say Users have BankAccounts which have a balance value.
So, how do I generate an array that Users and the total value of all their BankAccounts' values added together?
I'm not sure if this is quite what you want, but you can do something like:
ActiveRecord::Base.connection.select_all("select user_id, sum(balance) from accounts group by user_id;")
This will give you an array of user_ids and balances from the accounts table. The advantage of doing it this way is that it comes down to only one SQL query.
You'll want to do something like this. I don't believe it's possible to use #sum via an association.
class User
def net_worth
BankAccount.sum(:balance, :conditions => ["user_id = ?", self.id])
end
end
Edit
I see a reference to a #sum in AssociationCollection, so try this:
class User
def net_worth
self.bank_accounts.sum(:balance)
end
end
(I haven't tested this code)
First you need to find the users you want so I'll just assume you want all users.
#users = User.all
Then you need to take the array and collect it with only the elements you want.
#users.collect! {|u| [u.name, u.bank_account_total_value]}
For this kinda attribute I would set it in the model assuming you have has_many :transactions as an association
Class User
has_many :transactions
def bank_account_total_value
total = 0
self.transactions.each do |t|
total += t.amount
end
end
end

Resources