This question almost answers it, but I still think it's overkill.
Trouble with Rails has_many relationships
I really just want a to do assignments like this assign
#user.browsing_location = location1
#user.home_location = location2
I've done a lot of googling around and all the information is contradictory, or explains setting up many to many relationships, and explains methods using an intermediary table. But really all the database should need is for the user table to have two differently names id fields for the locations. Will something like the following work?
User Class
class User < ActiveRecord::Base
#locations created by this user
has_many :locations, :foreign_key => :creator_id
#locations for browsing and visiting
belongs_to :browsing_location, :source => :location
belongs_to :home_location, :source => :location
end
Location Class
class Location < ActiveRecord::Base
#Users who are just browsing this location now
has_many :browsing_users, :foreign_key => :browsing_location_id, :source => :users
#Users who are living here now
has_many :home_users, :foreign_key => :home_location_id, :source => :users
#User who created this location
has_one :user
end
Quite a lot of my models will need relationships like this so I would like to avoid having to create extra tables for this.
It appears that you're attempting to have two tables that inherit the location class, browsing_location and home_location and two tables that inherit the user class, browsing_user and home_user. For Rails 3:
You've got the general idea, but it appears that you have mixed things up a bit. :source is used for many to many relationships to determine which association to use. What you appear to need instead is :class_name
I would need to see your table definitions for users and locations to make sure you're using :foreign_key attribute correctly.
user.rb
class User < ActiveRecord::Base
# locations created by this user
has_many :locations, :foreign_key => :creator_id
# I'm assuming the locations belong to the user, as you're end goal was stated as
# the ability to call set user.browsing_location and user.home_location
# that would mean that browsing_location and home_location would need to belong to
# the User class, not the other way around.
has_many :browsing_locations, :class_name => :location
has_many :home_locations, :class_name => :location
end
class Location < ActiveRecord::Base
# User who created the location
belongs_to :user
# Users who are just browsing this location now
has_many :browsing_users, :class_name => :users
# Users who are living here now
has_many :home_users, :class_name => :users
end
Related
I have a model of users and things
Users can have one leader and many followers which are other users. User can also have things
So I have these definitions which use a self join relationship:
class User < ActiveRecord::Base
has_many :things, dependent: :destroy
has_many :followers, :class_name => 'User', :foreign_key => 'leader_id'
belongs_to :leader, :class_name => 'User', :foreign_key => 'leader_id'
end
class Thing < ActiveRecord::Base
belongs_to :user
end
So I can query ask for a list of things that a user has by asking, for example User.first.things. I can also get a list of followers of a user with User.first.followers.
How do I get a list of things that a user's followers have. I think I might need to use a has_many through relationship but I can't seem to figure it out as I'm not sure how to deal with the fact that a Leader can have things through a 'follower' but also directly themselves
Thanks
Something like this:
def all_things
Thing.where(:user_id => followers.map(&:id).push(id))
end
It returns a scope so you should be able to continue the chain, for example:
User.first.all_things.visible
Update
If you are only interested in the followers' things without adding the user's things to the batch it is better is you do it directly with a has_many through:
has_many :followers_things, :through => :followers, :source => :things
Check this other SO thread
How about:
all_things = (your_user.followers.each { |f| f.things }).flatten
This should return an array containing things that belong to all your_user's followers.
I'm trying to prevent my users from creating a relationship in a 'has many through' association with a record that doesn't belong to them.
My users have many locations through location_users. And their locations have many shops through location_shops. I have things protected currently with CanCan.
class User < ActiveRecord::Base
has_many :locationusers
has_many :locations, :through => :locationusers
end
class Location < ActiveRecord::Base
has_many :locationusers
has_many :users, :through => :locationusers
has_many :location_shops
has_many :shops, :through => :location_shops
end
class Shop < ActiveRecord::Base
has_many :location_shops
has_many :locations, :through => :location_shops
end
And my cancan abilities
class Ability
can [:manage], Shop, { :locationusers => {:user_id => user.id }}
can [:manage], Location, { :locationusers => {:user_id => user.id }}
end
I can handle the creation / editing of locations via this setup and my users can only view / edit their own locations / shops.
The issue is the creation of these relationships.
If a user posts a location id which doesn't belong to them, the relationship is created regardless of whether they have permission to create it. Granted, they can't view this relationship but I need to prevent the creation in the first place.
Eg, a user with a single location with ID 314
>> User.last.locations.map(&:id)
=> [314]
When creating a new shop, if I alter the params posted:
:shop=>{:shop_name=>"Ye Old Shoppe", :location_ids => [1,2,3,314]}}
The above creates the relationship for four locations obviously. I need it to validate the location ids before the creation of the relationship.
The only thing I could come up with was adding before_add in the model:
class Location
has_many :location_shops
has_many :shops, :through => :location_shops, :before_add => :check_location_ownership
end
Is this the correct way to go about it and if so, what should :check_location_ownership look like? Or, is there a better way to prevent the creation of the relationship?
Although what you have done does make sense, there are 2 other ways I can think of.
1) Use :conditions option on the has_many relationship.
2) A custom validation method.
class Location
has_many :location_shops
has_many :shops, :through => :location_shops
validate :check_location_ownership
end
I would personally choose one of these 3 depending on the case.
So this might be really bad form. I'm relatively new to rails. I'm not sure.
I have a project model and I want there to be many owners (who can read and write everything) and many collaborators (who can read and write some stuff).
In my project.rb file I have:
has_many :project_user_relationships, :dependent => :destroy
has_many :collaborators, :through => :project_user_relationships, :source => :user
has_many :project_owners_relationships, :class_name => "ProjectUserRelationship", :foreign_key => "project_id",
:before_add => Proc.new { |p,owner_r| owner_r.owner = true }, :conditions => "`project_user_relationships`.owner = true"
has_many :owners, :through => :project_owners_relationships, :source => :user
So this works reasonably well. If I add a new owner, that user is also a collaborator which is what I want. The issue I'm not sure how to solve is if I add a user that is already collaborator as an owner, I get two entries in the join table. I'd like for it to just amend the record that's already there. How do I do that?
Here's the data model I would suggest for this:
class Project < ActiveRecord::Base
has_many :memberships, :dependent => :destroy
...
end
class Membership < ActiveRecord::Base
belongs_to :project
belongs_to :user
...
end
class User < ActiveRecord::Base
has_many :memberships, :dependent => :destroy
has_many :projects, :through => :memberships
...
end
And then the membership table will have the following attributes:
:id
:user_id
:project_id
:is_owner (boolean)
A scope defined on the membership class:
scope :owner, where("is_owner")
And a special method for User instances:
def owned_projects
memberships.owner.includes(:projects).inject([]) {|array, m| array << m.project; array}
end
will allow you to retrieve a user's owned projects with the user.owned_projects call.
And just a call to user.projects to see a user's projects that they either collaborate on or own.
You have better data normalization with this data model, and a simple boolean attribute to define whether or not a user is a project owner.
This data model is used in this project, with the exception that s/Project/Group/, and there's some additional functionality to handle inviting users to the Project.
This doesn't answer your "real question", but I think part of the issue is that a data model where collaborators are owners are stored in the same table is needed to minimize redundancies and the need to manage two separate tables.
I am having a somewhat too nested database layout, however, I seem to need it. That is, Our website visitors may each have a single account with maintaining multiple users (think of identities) within.
Now they may create tickets, which are grouped by ticket sections, and we have ticket manager (operator) to process the incoming tickets.
Not every ticket manager may see every ticket but only those this manager is a member of the given ticket section for.
Now, I am totally fine in querying via raw SQL statements, but I failed to verbalizing those two special queries the Rails way.
Here is the (abstract) model:
# account system
class Account < ActiveRecord::Base
has_many :users
has_many :tickets, :through => :users
has_many :managing_ticket_sections, ... # TicketSection-collection this account (any of its users) is operate for
has_many :managing_tickets, ... # Ticket-collection this account (any of its users) may operate on
end
class User < ActiveRecord::Base
belongs_to :account
has_many :tickets
has_many :managing_ticket_sections, ... # TicketSection-collection this user is operate for
has_many :managing_tickets, ... # Ticket-collection this user may operate on
end
# ticket system
class Ticket < ActiveRecord::Base
belongs_to :author, :class_name => "User"
belongs_to :assignee, :class_name => "User"
belongs_to :section, :class_name => "TicketSection"
end
class TicketSection < ActiveRecord::Base
has_many :tickets
has_many :operators
end
class TicketSectionManager < ActiveRecord::Base
belongs_to :manager, :class_name => "User"
belongs_to :section
end
I am aware of basic has_many :through-constructs, however, here, I am accessing more than three tables to get the tickets.
Something that actually works for in the User's model is:
class User < ActiveRecord::Base
has_many :managing_relations, :class_name => "TicketSectionManager" # XXX some kind of helper, for the two below
has_many :managing_sections, :class_name => "TicketSection", :through => :managing_relations, :source => :section
has_many :managing_tickets, :class_name => "Ticket", :through => :managing_relations, :source => :section
end
Here I am using a helper relation (managing_relations), which is absolutely never used except by the two has_many relations below.
I were not able to describe a User.managing_sections nor User.managing_tickets relation without this helper, which is, where I need an advice for.
Secondly, the customer is to have a look at all of the tickets he can manage on any User (think of an identity) he has logged in, so what I need, is a way to collect all tickets (/sections) this Account is permitted to manage (identified by being a member of the corresponding TicketSection)
Here I even were not able to express this relation the ruby way, and I had to work around it by the following:
class Account
def managing_tickets
Ticket.find_by_sql("SELECT t.* FROM tickets AS t
INNER JOIN ticket_section_managers AS m ON m.section_id = t.section_id
INNER JOIN users AS u ON u.id = m.user_id
WHERE u.account_id = #{id}")
end
end
I'd appreciate any kind of advice, and
many thanks in advance,
Christian Parpart.
I'm working on a rails site that I've inherited and am trying to troubleshooting some sub-optimal model behavior. I have users, songs, and songs_download, each of which is its own model.
Here's the relevant line from the users model:
has_and_belongs_to_many :downloaded_songs, :class_name => 'Song', :join_table => :song_downloads
From the songs model:
has_and_belongs_to_many :downloaded_users, :class_name => 'User', :join_table => :song_downloads
And from the song_downloads model:
belongs_to :user
belongs_to :song
Here's the code to create a new song_download record when a user downloads a song (in the songs controller):
SongDownload.create( :song_id => #song.id,
:user_id => current_user.id,
:download_date => Date.today )
The problem I'm having is that once a user downloads a song, if I try to invoke the downloaded users from the interactive console, by, say, typing the following:
Song.find(<some id>).downloaded_users
I get back the complete record of the user, but the id in the returned objected is the primary key of the SongDownload, not the primary key of the User. All of the other fields are accurate, but the ID is not.
I didn't come up with this modeling scheme and it seems to me that :has_and_belongs_to_many might be more appropriately used with no explicitly modeled SongDownload object, but I'd rather not overhaul the codebase if I can help it. Are there any ways to get back the right user id given the current modeling scheme?
Thanks very much for your time and consideration!
Justin
Has and belongs to relationships are being phased out in favour of has many :through relationships.
On the upside you won't need to change any of your underlying structure, just the relationship declarations in the Song and User models.
class Song < ActiveRecord::Base
has_many :song_downloads
has_many :users, :through => :song_downloads
...
end
class User < ActiveRecord::Base
has_many :song_downloads
has_many :songs, :through => :song_downloads
...
end
Now
Song.find(<some id>).users
Returns an array of User objects which are joined to the selected song through the song_downloads table.
The has_many :through is recommended when the join table has more columns than simply two foreign keys.
class User < ActiveRecord::Base
has_many :song_downloads
has_many :downloaded_songs,
:through => :song_downloads,
:source => :song
end
class Song < ActiveRecord::Base
has_many :song_downloads
has_many :downloaded_users,
:through => :song_downloads,
:source => :user
end
Now you can get a list of users that have downloaded a particular song like so:
Song.find(1).downloaded_users