RoR ActiveRecord Join Help for semi AI App - ruby-on-rails

Working on a RoR app. Think Tinder for art with a little basic AI. As you like or dislike art the app will show you art that you will hopefully like more based on other users’ preferences. I am working on the code to pick the next image to show.
I use the following models:
Basic user info:
class User < ApplicationRecord
has_many :artworks
has_many :artists
has_many :art_views
end
Info about each piece (price, height, width, etc)
class Artwork < ApplicationRecord
belongs_to :user
belongs_to :artist
has_many :art_views
end
To represent which art has been show to a user (boolean liked indicates if the user liked the artwork)
class ArtView < ApplicationRecord
belongs_to :user
belongs_to :artwork
end
The logic is simple: after finding the last Artwork liked by current_user (in ArtView), find other Users who also liked the same Artwork (also in ArtView) then find new Artwork liked by those other Users that has not been seen by the current_user (no record in ArtView)
I am using the following join:
#artwork = Artwork.joins(:artview)
.where(:artviews => { :liked => true})
.where(:artviews => { :artwork_id => #artwork_id})
.where.not(:artviews => { user_id: #id })
I get the following error:
Can't join 'Artwork' to association named 'artview'; perhaps you misspelled it?
Much appreciate any guidance/suggestions (both in fixing the error and/or improving the logic, in general)

An ArtView has many ArtViews, so you have to reference that relationship when using joins.
It should work like this:
Artwork.joins(:art_views)
The same when using the joined columns (no matter the kind of relationship), you should use the "real" name of the table; it's pluralized way - that's how Rails name the tables it creates, unless otherwise specified:
Artwork
.joins(:art_views)
.where(:art_views => { :liked => true})
.where(:art_views => { :artwork_id => #artwork_id})
.where.not(:art_views => { user_id: #id })

Related

Flatten a has_many association and get an attribute sum

I would like to understand the most efficient way to flatten some has_many associations, and subsequently get the sum of some attribute on the child associations. Suppose you have the following data structures:
class Restaurant < ActiveRecord::Base
has_one :address
has_many :menu_items
end
class Address < ActiveRecord::Base
belongs_to :restaurant
end
class MenuItem < ActiveRecord::Base
belongs_to :restaurant
end
Say that MenuItem has the attribute "cost", and Address has the attribute "zip_code". What I would like to do is maybe find all restaurants in the zipcode '10101' and get the sum of their menu item's cost attributes. Say that I want to be able to show the average cost of restaurants' offerings in an area.
I think there are quite a few brute force-y ways to do this, but I know there should be something better. For example, in C#/LINQ if I had a similar set of data structures it would be easy to write:
var sum=restaurants.Where(r => r.zip_code==10101).SelectMany(r => r.MenuItems).Sum(mi => mi.Cost);
The best I have come up with, but that feels wrong to me, is:
def thing_I_dont_know_how_to_do
find_zip=Address.where(:zip_code => zip_code)
restaurants=Restaurant.joins(:address, :menu_items).merge(find_zip)
restaurant_ids=restaurants.map(&:id)
sum=MenuItems.sum(:cost, :conditions => {:restaurant_ids => venue_ids})
end
Can anyone help me improve upon this?
You can do it with a single SQL request which selects all the menu items of restaurants in zip code 10101, and sums all their cost:
MenuItem.joins(restaurant: :address).where(addresses: { zip_code: '10101' }).sum('cost')

Has_many: through in Rails. Can I have two foreign keys?

I am a rails newbie and I am trying to create a database schema that looks like the following:
There are many matches. Each match has 2 teams.
A team has many matches.
The team model and match model are joined together through a competition table.
I have that competition model with a match_id and a team1_id and a team2_id.
But I don't know how to make this work or if it's even the best way to go about it. I don't know how to make certain teams team1 and others team2.... two foreign keys? Is that possible?
The match table also needs to hold additional data like team1_points and team2_points, winner and loser, etc.
You can have as many foreign keys as you want in a table. I wrote an application that involved scheduling teams playing in games.
The way that I handled this in the Game class with the following:
class Game < ActiveRecord::Base
belongs_to :home_team, :class_name => 'Team', :foreign_key => 'team1_id'
belongs_to :visitor_team, :class_name => 'Team', :foreign_key => 'team2_id'
You can add appropriate fields for team1_points, team2_points, etc. You'll need to set up your Team model with something like:
class Team < ActiveRecord::Base
has_many :home_games, :class_name => 'Game', :foreign_key => 'team1_id'
has_many :visitor_games, :class_name => 'Game', :foreign_key => 'team2_id'
def games
home_games + visitor_games
end
#important other logic missing
end
Note that some of my naming conventions were the result of having to work with a legacy database.
I faced a similar problem, and extending the previous answer, what I did was:
class Game < ActiveRecord::Base
def self.played_by(team)
where('team1_id = ? OR team2_id = ?', team.id, team.id)
end
end
class Team < ActiveRecord::Base
def games
#games ||= Game.played_by(self)
end
end
This way, Team#games returns an ActiveRecord::Relation instead of an Array, so you can keep chaining other scopes.

Making record available only to certain models in Rails 3

I have a weird design question. I have a model called Article, which has a bunch of attributes. I also have an article search which does something like this:
Article.project_active.pending.search(params)
where search builds a query based on certain params. I'd like to be able to limit results based on a user, that is, to have some articles have only a subset of users which can see them.
For instance, I have an article A that I assign to writers 1,2,3,4. I want them to be able to see A, but if User 5 searches, I don't want that user to see. Also, I'd like to be able to assign some articles to ALL users.
Not sure if that was clear, but I'm looking for the best way to do this. Should I just store a serialized array with a list of user_id's and have -1 in there if it's available to All?
Thanks!
I would create a join table between Users and Articles called view_permissions to indicate that a user has permission to view a specific article.
class ViewPermission
belongs_to :article
belongs_to :user
end
class User
has_many :view_permissions
end
class Article
has_many :view_permissions
end
For example, if you wanted User 1 to be able to view Article 3 you would do the following:
ViewPermission.create(:user_id => 1, :article_id => 3)
You could then scope your articles based on the view permissions and a user:
class Article
scope :viewable_by, lambda{ |user| joins(:view_permissions).where('view_permissions.user_id = ?', user.id) }
end
To search for articles viewable by a specific user, say with id 1, you could do this:
Article.viewable_by(User.find(1)).project_active.pending.search(params)
Finally, if you want to assign an article to all users, you should add an viewable_by_all boolean attribute to articles table that when set to true allows an article to be viewable by all users. Then modify your scope to take that into account:
class Article
scope :viewable_by, lambda{ |user|
joins('LEFT JOIN view_permissions on view_permissions.article_id = articles.id')
.where('articles.viewable_by_all = true OR view_permissions.user_id = ?', user.id)
.group('articles.id')
}
end
If an Article can be assigned to multiple Writers and a Writer can be assigned to multiple Articles, I would create an Assignment model:
class Assignment < AR::Base
belongs_to :writer
belongs_to :article
end
Then you can use has_many :through:
class Article < AR::Base
has_many :assignments
has_many :writers, :through => :assignments
end
class Writer < AR::Base
has_many :assignments
has_many :articles, :through => :assignments
end

Rails polymorphic many to many association

I'm trying setup a generic sort of web of related objects. Let say I have 4 models.
Book
Movie
Tag
Category
I would like to able to do:
book = Book.find(1)
book.relations << Tag.find(2)
book.relations << Category.find(3)
book.relations #=> [Tag#2, Category#3]
movie = Movie.find(4)
movie.relations << book
movie.relations << Tag.find(5)
movie.relations #=> [Book#1, Tag#5]
Basically I want to be able to take any 2 objects of any model class (or model class that I allow) and declare that they are related.
Obviously I don't want to create a huge mess of join tables. This seems like it's not quite a has many through association, and not quite a polymorphic association.
Is this something that Rails can support via it's association declarations or should I be rolling my own logic here?
Support for polymorphism has improved dramatically since the early days. You should be able to achieve this in Rails 2.3 by using a single join table for all your models -- a Relation model.
class Relation
belongs_to :owner, :polymorphic => true
belongs_to :child_item, :polymorphic => true
end
class Book
has_many :pwned_relations, :as => :owner, :class_name => 'Relation'
has_many :pwning_relations, :as => :child_item, :class_name => 'Relation'
# and so on for each type of relation
has_many :pwned_movies, :through => :pwned_relations,
:source => :child_item, :source_type => 'Movie'
has_many :pwning_movies, :through => :pwning_relations,
:source => :owner, :source_type => 'Movie'
end
A drawback of this kind of data structure is that you are forced to create two different roles for what may be an equal pairing. If I want to see all the related movies for my Book, I have to add the sets together:
( pwned_movies + pwning_movies ).uniq
A common example of this problem is the "friend" relationship in social networking apps.
One solution used by Insoshi, among others, is to register an after_create callback on the join model ( Relation, in this case ), which creates the inverse relationship. An after_destroy callback would be similarly necessary, but in this way at the cost of some additional DB storage you can be confident that you will get all your related movies in a single DB query.
class Relation
after_create do
unless Relation.first :conditions =>
[ 'owner_id = ? and owner_type = ? and child_item_id = ? and child_item_type = ?', child_item_id, child_item_type, owner_id, owner_type ]
Relation.create :owner => child_item, :child_item => owner
end
end
end
I have come up with a bit of solution. I'm not sure it's the best however. It seems you cannot have a polymorphic has_many through.
So, I fake it a bit. But it means giving up the association proxy magic that I love so much, and that makes me sad. In a basic state, here is how it works.
book = Book.find(1)
book.add_related(Tag.find(2))
book.add_related(Category.find(3))
book.related #=> [Tag#2, Category#3]
book.related(:tags) #=> [Tag#2]
I wrapped it up in a reusable module, that can be added to any model class with a single has_relations class method.
http://gist.github.com/123966
I really hope I don;t have to completely re-implement the association proxy to work with this though.
I think the only way to do it exactly as you described is the join tables. It's not so bad though, just 6, and you can pretty much set-and-forget them.
depending on how closesly related your movies/books db tables are
what if you declared
class Items < ActiveRecord::Base
has_many :tags
has_many :categories
has_and_belongs_to_many :related_items,
:class => "Items",
:join_table => :related_items,
:foreign_key => "item_id",
:associated_foreign_key => "related_item_id"
end
class Books < Items
class Movies < Items
make sure you put type in your items table

How to setup and work with RoR Complex Joins?

This code is taken from a previous question, but my question directly relates to it, so I've copied it here:
class User < ActiveRecord::Base
has_many :group_memberships
has_many :groups, :through => :group_memberships
end
class GroupMembership < ActiveRecord::Base
belongs_to :user
belongs_to :role
belongs_to :group
end
class Role < ActiveRecord::Base
has_many :group_memberships
end
class Group < ActiveRecord::Base
has_many :group_memberships
has_many :users, :through > :group_memberships
end
New Question Below
How you take the above code one step further and add and work with friends?
class User < ActiveRecord::Base
has_many :group_memberships
has_many :groups, :through => :group_memberships
has_many :friends # what comes here?
has_many :actions
end
In the code above I also added actions. Let's say that the system kept track of each user's actions on the site. How would you code this so that each action was unique and sorted with the most recent at the top for all the user's friends?
<% for action in #user.friends.actions %>
<%= action.whatever %>
<% end %>
The code above may not be valid, you could do something like what I have below, but then the actions wouldn't be sorted.
<% for friend in #user.friends %>
<% for action in friend.actions %>
<%= action.whatever %>
<% end %>
<% end %>
UPDATE
I guess the real issue here is how to define friends? Do I create a new model or join table that links users to other users? Ideally, I'd like to define friends through the common group memberships of other users, but I'm not sure how to go about defining that.
has_many :friends, :through => :group_memberships, :source => :user
But that doesn't work. Any ideas or best practice suggestions?
It's been a while since I've worked with Rails (think Rails 1.x), but I think you can do something like the following:
Action.find(:all, :conditions => ['user_id in (?)', #user.friends.map(&:id)], :order => 'actions.date DESC')
and then in your view:
<% #actions.each do |action| %>
<%= action.whatever -%>
<% end %>
To add friends you can use has_and_belongs_to_many or has_many :through. Here is example how to do it. You should self join users table.
To list recent actions, get them from db using :order => :created_at (or :updated_at). Than you can filter out actions that not belongs to users.
#actions = Action.all(:order => :created_at).select {|a| a.user.friends.include?(#user)}
Probably you can also write sql query to do this.
You could make a "friend" object that uses the "user" table and go from there or you could
define a friend as a partial entity (friend name and user_id reference) and pull the user object into the friend object.
I like this last model better because it allows you to represent "deleted" friends more easily. You would still have a copy of the friend name (even if it is a duplicate of the user name) and the ID to identify if the user is gone or not. Keeps the "friend" concept a little further away from the "user" concept and any security implications. I will argue it's appropriate de-normalization to duplicate the "name" field as it adds flexibility and "shadow" information about a user that used to exist.
My take on it, though. You would still have to eventually self-join back to the user table, but perhaps not as much. If you REALLY hate to de-normalize anything you can have a Person object that Friend and User both refer to for information. Personally, I think that is a nice theoretical construction, but not worth the complexity it would introduce into the bulk of your code (80% of it being talking about the user and 20% talking about the friend). I would make the user easy to talk about and the friend a little hard... not both. :)
Hope this helps. I enjoyed mentally toying with this one. :)

Resources