Three models:
User
List
Item
A User can have many Lists and a List can have many Items. Each List can have an Item added to it by ANY User. This means, for example, that you can create a list and I can add items to it. Make sense? Let's keep going.
I want to be able to find all Items created by X User at any point in time.
class User < ActiveRecord::Base
has_many :lists
has_many :items
end
class List < ActiveRecord::Base
belongs_to :user
has_many :items
end
class Item < ActiveRecord::Base
belongs_to :list
belongs_to :user
end
I don't like this. It has a funky smell but I can't put my finger on it. I feel like there should be something better. When creating a new Item I would have to write something that looks like the following:
list = List.find(params[:list_id])
#item = list.items.new(params[:item])
user = User.first
#item.user = user
#item.save!
So, what am I missing? Are my relationships wrong (very likely)? Tell me! :)
It seems like there can be two different relationships between items and users: 1) items are added to lists by users, and 2) lists are created by users.
Only one user can create a list. Many users can add items to lists after they are created.
So modeling this requires two different relationships
class User < ActiveRecord::Base
has_many :lists
has_many :items_added, :class_name => "Item", :foreign_key => "added_by_user_id"
end
class List < ActiveRecord::Base
belongs_to :user
has_many :items
end
class Item < ActiveRecord::Base
belongs_to :list
belongs_to :added_by_user, :class_name => "User", :foreign_key => "user_id"
end
It makes sense to assume that you all these relationships are required -- that is, a list needs a user_id when it's created to show who created it. Likewise, an item needs to record who added it to the list.
You'd need to add the added_by_user_id column to your items table to make this work and keep track of which user added the item.
So you could do this:
# To create a user and a list is easy!
user = User.create(user_params)
list = user.create_list(list_params)
# but when adding an item, you need to know which user is adding it
item = list.create_item({:added_by_user => current_user, :item_name => 'name', etc})
Now you can find all items added by a user using:
all_user_items = user.items_added
I haven't tested this and it's a bit complicated so I may have 1-2 mistakes in the code, but generally that's one way this could be modeled.
Related
In many to many fields delete method is deleting all the occurrence of collection. Say I have:
class user < ActiveRecord::Base
has_and_belongs_to_many :cars
end
class car < ActiveRecord::Base
has_and_belongs_to_many :users
end
users and cars are many to many relationship, I have defined my users_cars table. Now user can have repetitive car entry as relation. For example:
Car: A,B,C
User: U1,U2,U3
U1=[A,B,C,A,A,A,B]
Which can be implemented using many to many relationship, the way I have implemented. BUT, at the time when I want to delete one of the car entries of user the problem occurs.
User.cars.delete(car) #deletes all occurrence of car
User.cars.delete_at(User.cars.find_index(video_card)) #delete_at does not exist
Now how to resolve this?
First of all, you can't call User.cars unless you have defined a class level method cars in your User model, but in this way, you would return all cars, and that - in no way - would make sense.
Second, delete_at is a method that works on Array objects, and expects an integer to be passed in. So as a little hack, you can turn your ActiveRecord::Associations object into an array, and then call delete_at method.
user = User.first
user.cars.to_a.delete_at(Car.last.id) # assuming that the last car belongs
# to the first user, something you would never do in actual
# production code.
Edit:
You can also try the following to achieve the same functionality:
user = User.first
user.cars.where("cars.id = ?", Car.first.id).first.delete
Edit 2:
For what you asked in comment, you can have a model for the table cars_users.
rails g model CarUser
class Car < ActiveRecord::Base
has_many :cars_users
has_many :users, through: car_users
end
class User < ActiveRecord::Base
has_many :cars_users
has_many :cars, through: car_users
end
class CarUser < ActiveRecord::Base
belongs_to :car
belongs_to :user
end
And now, you can do:
CarUser.where("car_id = ? AND user_id = ?", Car.first.id, User.first.id).first.delete
I have the following structure:
class User < ActiveRecord::Base
end
class Item < ActiveRecord::Base
end
class Group < ActiveRecord::Base
end
A User can create (and thereby own) a Group.
A Group is a list of Items and a list of the Users who can access these items.
In other words, a user has a list of Items and can control which Users can see these items through the Group membership.
How should I set up the relationship?
Well, you're going to run into some issues with the fact that you want a double many-to-many relationship here. Ie. groups have and belong to many items, and items have and belong to many users.
So, I would setup the relationship this way, assuming you want a group to be able to have many items, and items may belong to more than one group:
User has_many :groups
User has_and_belongs_to_many :items
User has_many :own_items, :class_name => 'Item'
Group belongs_to :user
Group has_and_belongs_to_many :items
Item has_and_belongs_to_many :groups
Item has_and_belongs_to_many :users
Item belongs_to :owner, :class_name => 'User'
Your migrations will need to look like so:
# Group
:user_id, :integer
# Item
:owner_id, :integer
# GroupsItems
:group_id
:item_id
#ItemsUsers
:item_id
:user_id
Now, the structure you're looking at isn't the cleanest in the universe, but it will behave as you expect as long as you're careful about the user association.
For instance, to create a user's item:
#user = User.first
#user.own_items.create(...)
To assign users as able to view an item...
#item = Item.find(...) #or #user.own_items.find(...)
#item.users = [user1,user2,user3]
Now, this sets up the relationships you want, but you'll have to also write your own controller / view logic to limit access, or use a library like CanCan.
For instance:
# View
- if #item.users.include?(current_user)
...show item...
# Items Controller:
def show
#item = Item.find(params[:id])
if #item.users.include?(current_user)
...continue...
else
redirect_to :back, :alert => 'You are not authorized to view this item.'
end
end
I hope those examples point you in the right direction. You'll have a number of issues to deal with relating to access control, but trying to think of them and solve each one I can think of is beyond the scope of this question.
Also, note that this is the simplest setup I could think of. If you have more complex logic in the associations you might want to make a full-fledged join model and use has_many :through associations instead of HABTM.
Good luck!
It has been almost a week since I'm trying to find a solution to my confusion... Here it is:
I have a Program model.
I have a ProgramCategory model.
I have a ProgramSubcategory model.
Let's make it more clear:
ProgramCategory ======> Shows, Movies,
ProgramSubcategory ===> Featured Shows, Action Movies
Program ==============> Lost, Dexter, Game of Thrones etc...
I want to able to associate each of these models with eachother. I've got what I want to do particularly with many-to-many association. I have a categories_navigation JOIN model/table and all of my other tables are connected to it. By this way, I can access all fields of all of these models.
BUT...
As you know, has_many :through style associations are always plural. There is nothing such as has_one :through or belongs_to through. BUT I want to play with SINGULAR objects, NOT arrays. A Program has ONLY ONE Subcategory and ONLY ONE Category. I'm just using a join table to only make connection between those 3. For example, at the moment I can access program.program_categories[0].title but I want to access it such like program.program_category for example.
How can I have 'has_many :through's abilities but has_one's singular usage convention all together? :|
P.S: My previous question was about this situation too, but I decided to start from scratch and learn about philosophy of associations. If you want so you may check my previous post here: How to access associated model through another model in Rails?
Why a join table where you have a direct relationship? In the end, a program belongs to a subcategory, which in turn belongs to one category. So no join table needed.
class Program < ActiveRecord::Base
belongs_to :subcategory # references the "subcategory_id" in the table
# belongs_to :category, :through => :subcategory
delegate :category, :to => :subcategory
end
class Subcategory < ActiveRecord::Base
has_many :programs
belongs_to :category # references the "category_id" in the table
end
class Category < ActiveRecord::Base
has_many :subcategories
has_many :programs, :through => :subcategories
end
Another point of view is to make categories a tree, so you don't need an additional model for "level-2" categories, you can add as many levels you want. If you use a tree implementation like "closure_tree" you can also get all subcategories (at any level), all supercategories, etc
In that case you skip the Subcategory model, as it is just a category with depth=2
class Program < ActiveRecord::Base
belongs_to :category # references the "category_id" in the table
scope :in_categories, lambda do |cats|
where(:category_id => cats) # accepts one or an array of either integers or Categories
end
end
class Category < ActiveRecord::Base
acts_as_tree
has_many :programs
end
Just an example on how to use a tree to filter by category. Suppose you have a select box, and you select a category from it. You want to retrieve all the object which correspond to any subcategory thereof, not only the category.
class ProgramsController < ApplicationController
def index
#programs = Program.scoped
if params[:category].present?
category = Category.find(params[:category])
#programs = #programs.in_categories(category.descendant_ids + [category.id])
end
end
end
Tree-win!
So I have a rails app That lets users create lists and put items on that list. As a feature a user can put any one item on multiple lists. Example:
List_1
-item_1
-item_2
List_2
-item_3
-item_2
The relationship side of it all is complete and working fine.
The next step was to let the user arrange the items in their lists. I installed act_as_list and using jQuery sortable got a simple drag and drop working for sorting items. The issue is each item has one position. So if I change item_2's position to 1 on list_1, item_2's position is now 1 in every list.
I'm trying to think of a way to store item positions relative to the list. Anyone have any thoughts?
Edit to Add code
List model
class List < ActiveRecord::Base
belongs_to :user
has_many :assignments, dependent: :destroy
has_many :items, through: :assignments, order: 'position'
end
Item model
class Item < ActiveRecord::Base
belongs_to :user
has_many :assignments, dependent: :destroy
has_many :lists, through: :assignments
acts_as_list scope: :list
end
Assignment model
class Assignment < ActiveRecord::Base
belongs_to :item
belongs_to :list
validates_uniqueness_of :item_id, :scope => :list_id
end
So an answered my own question with a little time an effort. I've provided an example below for anyone who may run into the issue in the future.
Basically #thedeeno's answer would have worked fine if I was able to use scopes. In my case the list will be user created and hence scopes are not an option.
Solution:
So my solution was adding a position column to my assignments table (the join table for my has many through)
This way each assignment could now have a list_id and item_id and a position.
Then in my controller I did the following to populate the #items hash
#items = #list.items.order(:position)
This gave makes #item has ordered by item positions for their respective list position.
Now the last step was updating their position using sortable. To update the correct item's postion I needed sortable to send the list_id along with each order of the id's. To get the list_id sent with the post I adding a hidden li with and id of the #list like so
<li id='list_<%=#list.id %> style='display:none'></li>
which meant that my params now looked like this
{"list"=>["31"], "item"=>["30", "58", "59", "24", "80", "81"]}
Finally using the skills I had learned from http://railscasts.com/episodes/147-sortable-lists I changed my sort action to the following
def sort
#items = params[:item]
#items.each_with_index do |id, index|
assignment = Assignment.where(list_id: params[:list], item_id: id).update_all(['position=?', index+1])
end
render :nothing => true
end
That's it! users can now create any number of lists and item to multiple lists and sort each list independently. Hope this helps someone!
You can promote these 'lists' to a formal model and then scope acts_as_list position to that model. Similar to the example from the docs:
class List < ActiveRecord::Base
has_many :items, :order => "position"
end
class Item < ActiveRecord::Base
belongs_to :list
acts_as_list :scope => :list
end
The point of scoping is to make the position relative to another model. You can also have multiple positions scoped to multiple models.
EDIT:
If multiple scopes is your issue, you might want to checkout the sortable gem.
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