Preventing / Validating the creation of child relationships before commit in Rails3 - ruby-on-rails

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.

Related

Joining multiple relationships from separate tables into one association

I have several models that are relevant to this question..
UserGroupPermission
UserGroup
User
UserPermission
Permission
Essentially, A User can be assigned a permission through either their UserGroup or directly. UserGroups are tied to specific permissions via the UserGroupPermission model. Users are tied to specific permissions via the UserPermission model.
The end result is to be able to call User.permissions and return every permission assigned to that user either directly or through a UserGroup.
Here is my current model
class User < ActiveRecord::Base
belongs_to :user_group
#Permissions
has_many :user_permissions
has_many :user_group_permissions, :through => :user_group
has_many :permissions, :through => :user_group_permissions #and :user_permissions
end
I've search high and low for a solution to this, but can't seem to find my exact case. Thanks in advance!
Edit: I've been told that is unclear what I'm asking. Hopefully this will help: Basically the User has_many :permissions, :through => :user_group_permissions and :user_permissions, but you can't use an and to join the results together. I'm trying to retrieve and combine the results of both of those relations into one.
Edit 2: I've been doing some work. This code gives the desired result, but I'm not sure how well it would work for potentially large databases.
#Permissions
has_many :user_permissions
has_many :user_group_permissions, :through => :user_group
#has_many :permissions, :through => :user_group_permissions
#has_many :permissions, :through => :user_permissions
def permissions
all_permissions = []
UserGroupPermission.where('user_group_id = ?', user_group_id).each do |ugp|
all_permissions.append(Permission.find(ugp.permission_id))
end
UserPermission.where('user_id = ?', id).each do |p|
all_permissions.append(Permission.find(p.permission_id))
end
return all_permissions
end

Has Many Through Association Callbacks with multiple associations using the same join table

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.

Creating multiple one to many relationships in rails

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

Rails modeling for a user

When building a rails app that allows a User to login and create data, is it best to setup a belongs_to :user association on every single model? For example, let's say a user can create Favorites, Colors and Tags.
And let's say Favorites has_many :tags and Colors also has_many :tags. Is it still important for Tags to belong_to :user assuming the User is the only person who has authority to edit those tags?
And a similar question along the same lines: When updating data in FavoritesController, I've come to the conclusion that you perform CRUD operations by always doing something like current_user.favorites.find(param[:id].update_attributes(param[:favorite]) so that they can definitely only update models that belong to them. Right?
Update Wasn't too happy with any of the answers, as no one really answered my question but instead went after the for-example-only Tags model suggesting better ways to do that. I'm assuming I was right, and models should belong_to :user. I also discovered some great security tips that address my questions here: http://asciicasts.com/episodes/178-seven-security-tips
As you describe the tags it seems that they are more of an aspect, so you can implement them as a polymorphic association. But you should do it many-to-many, as tags can be reused among users and taggable objects. Let's call the join model Tagging, which will be the one that belongs to user if you want to remember who created the tagging.
class Tag < ActiveRecord::Base
has_many :taggings, :dependent => :destroy
has_many :colors, :through => :taggings, :source => :taggable, :source_type => "Color"
has_many :favorites, :through => :taggings, :source => :taggable, :source_type => "Favorite"
end
class Tagging < ActiveRecord::Base
belongs_to :user
belongs_to :taggable, :polymorphic => true
belongs_to :tag
end
class Color < ActiveRecord::Base
belongs_to :user
has_many :taggings, :as => :taggable
has_many :tags, :through => :taggings
end
class Favorite < ActiveRecord::Base
belongs_to :user
has_many :taggings, :as => :taggable
has_many :tags, :through => :taggings
end
class User < ActiveRecord::Base
has_many :favorites
has_many :colors
has_many :taggings
has_many :tags, :through => :taggings
end
As for the Favorite updating, I agree with you: you will mostly work within the scope of a user (most likely the currently logged in user).
It depends on your model. Both cases are valid but I'd discorage making a circular relationships like that. Having a hierarchy is more flexible. For example: User->Favorites->Tags (unless you want to tag users as well)
User.favorites.find(params[:id]).update_attributes(param[:favorite])
is what you mean I guess (syntax). Whoever calls the URL will perform that action. Dont rely on the fact that that URL is visible to one user only (owner of the favorite). You should have checks in place that the currently logged in user is the only one performing actions on the objects that belong to him.
The proposed mechanism sounds a bit too complex for me. I prefer the current_user way. Assume there is a current_user (following the authlogic way) in your authentication system, then simple add a user references (user_id) in every relevant table. Update the current_user for new or update record via a controller filter.
In the models, put relevant belongs_to :users accordingly, put enough has_many in users model if needed.
:has_many and :belongs_to in AR will explains the relationship between models, but not necessarily you have to use them in your models, the associaton between them will be already present in the tables as a foreign key.
But adding :has_many or :belongs_to to your models will give you extra methods to your model
ex:
class User < ActiveRecord::Base
has_many :favorites
#def favorites
# Favorite.find_all_by_user_id(self.id)
# end
end
If you mention has_many it will give a new method in your model called favorites, that method will be invisible (will be present in the AR).
Similarly for any association, if you are planning to use this kind of methods you should use associations in your models.

Rails Modeling Question - Relationships and Primary Keys

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

Resources