Given that I have the next models:
user.rb
has_many :favorites, dependent: :destroy
has_many :sports, through: :favorites
sport.rb
has_many :favorites, dependent: :destroy
has_many :users, through: :favorites
In the form for create a new user, there is a list of checkboxes of sports, and in the user validation I want to validate that at least one is selected.
I'm doing it like this:
In the user controller create action:
#user = User.new(params[:user])
#user.sports << Sport.find(params[:sports]) unless params[:sports].nil?
if #user.save ...
In the user model
validate :user_must_select_sport
def user_must_select_sport
if sports.empty?
errors.add(:Sport, "You have to select at least 1 sport")
end
end
And it's actually working, but I'm guessing that it has to be a better way of doing this. I'd appreciate any help.
You can use "validates_presence_of"
class User < ActiveRecord::Base
has_many :sports
validates_presence_of :sports
end
But there is a bug with it if you will use accepts_nested_attributes_for with :allow_destroy => true.
You can look into this : Nested models and parent validation
Related
I have setup a polymorphic liking in my my app where a user can like other models e.g book, chapter, article... Now I tried to take it a little further by allowing a user like another user but I'm running to this error:
Validation failed: User must exist
pointing to
likes.where(item: item).create!
This is my initial setup for liking other models excluding the user model
like.rb
belongs_to :user
belongs_to :item, polymorphic: true
user.rb
has_many :likes, dependent: :destroy
def toggle_like!(item)
if like = likes.where(item: item).first
like.destroy
else
likes.where(item: item).create!
end
end
def likes?(item)
likes.where(item: item).exists?
end
likes_controller.rb
class LikesController < ApplicationController
before_action :authenticate_user!
def toggle
if params[:book_id]
item = Book.friendly.find(params[:book_id])
elsif params[:user_id]
item = User.find_by(username: params[:username])
end
current_user.toggle_like!(item)
redirect_back(fallback_location: root_path)
end
end
book.rb
has_many :likes, as: :item, dependent: :destroy
For a user to like another user, i adjusted the user.rb from
has_many :likes
to
has_many :likes, as: :item, dependent: :destroy
This is when I get the error
Validation failed: User must exist
pointing to
likes.where(item: item).create!
in the user.rb
Keep has_many :likes, dependent: :destroy and also add has_many :received_likes, as: :item, class_name: "Like", dependent: :destroy. I think that will fix it. Because when you put User has_many: :likes, as: :item, and removed User has_many: :likes, this means that the association Like belongs_to :user was one-sided.
The application have Posts, Products and Services. I want the user selects the specific content to relate. Example:
I have 2 services. And I'm adding a new PRODUCT. In this product, I want to relate these 2 services and other 1 product.
The first thing I thought is to create a field in the database like related_content in all resources and save the ids with comma, like it: service_25, service_302, product_408. I did it other times, and... works.
I save the prefix service_ and product_ because the same item can be related with products and services.
But I think it is not the right way. Perhaps the right way is to use the many to many association. But I don't know how to do.
MODELS
product.rb
class Product < ActiveRecord::Base
has_many :menu_assigns, as: :menu_item, dependent: :destroy
has_many :categorizings, as: :item, dependent: :destroy
has_many :categories, -> { where for: Category.fors[:for_products] }, through: :categorizings
has_one :attach, as: :attached, dependent: :destroy
has_one :attachment, through: :attach
end
service.rb
class Service < ActiveRecord::Base
has_many :menu_assigns, as: :menu_item, dependent: :destroy
has_many :categorizings, as: :item, dependent: :destroy
has_many :categories, -> { where for: Category.fors[:for_services] }, through: :categorizings
has_one :attach, as: :attached, dependent: :destroy
has_one :attachment, through: :attach
end
post.rb
class Post < ActiveRecord::Base
has_many :menu_assigns, as: :menu_item, dependent: :destroy
has_many :categorizings, as: :item, dependent: :destroy
has_many :categories, -> { where for: Category.fors[:for_posts] }, through: :categorizings
has_one :attach, as: :attached, dependent: :destroy
has_one :attachment, through: :attach
end
This code has already some associations:
Menu: Using menu_assigns, the user can add Post, Product and Service to menu.
Category: The resources has categories. Using categorizings, the content is related.
Attachment: Is the featured image. Using attach we relate an image.
But, how to relate each other using the associations?
I imagine something like it: #page.related_content and returns an object with the registers.
My idea
1) Create a model called related_groups with these fields:
rails g model RelatedGroup item_id:integer item_type:string order:integer
Item ID is the ID of the related content. The item Type is the model (Product, Service, Post). The order field is the order to show.
2) In that model, create the relation:
class RelatedGroup < ActiveRecord::Base
belongs_to :item, polymorphic: true
end
3) Do the relation in the resources (Product, Service, Post). Below the example to post:
class Post < ActiveRecord::Base
has_many :related_groups
end
4) Join the results in all models (Product, Service, Post). Below the example to post:
class Post < ActiveRecord::Base
has_many :related_groups
with_options through: :related_groups, source: :item do
has_many :posts, source_type: 'Post'
has_many :products, source_type: 'Product'
has_many :services, source_type: 'Service'
end
end
This seems to be correct, but I'm not sure.
And the controllers? And the views?
In Post (example), how to create the checkboxes to check the related contents? And in the controller, how to save the data? And how to set the order?
I'm using rails 4.2
I appreciate any help. Tks!
Check the has_and_belongs_to_many association:
https://apidock.com/rails/ActiveRecord/Associations/ClassMethods/has_and_belongs_to_many
Depending on your requirements you could either have one join table for each kind of related object (Post/Product/Service) or have a combined join table with an additional attribute to distinguish what kind of object you are associating with, e.g.
class CreatePostRelationJoinTable < ActiveRecord::Migration
def change
create_table :post_relations, id: false do |t|
t.integer :post_id
t.string :relation_type
t.integer :relation_id
end
end
end
sorry for the title I didn't really know what to put in there to be really clear. So I'll be clear in my explanation.
Users can create Groups and Links via form. The users can join Groups via Member where member has group_id and user_id in the table. I would like to be able to share the Users Link within the group.
So when a user creates a group, other users join this group. And for now when a user creates a link, it's only for himself but I want the user to be able to share (or not) the links he created with the groups he is part of. If the user is a member of several groups then the user can choose in which group he wants to share the link he created.
I have 4 models : Link.rb, User.rb, Member.rb and Group.rb. Here are my relations :
#Link.rb:
class Link < ApplicationRecord
belongs_to :user
end
#User.rb :
class User < ApplicationRecord
has_many :links, dependent: :destroy
has_many :members, :dependent => :destroy
has_many :groups, :through => :members
end
#Member.rb :
class Member < ApplicationRecord
belongs_to :user
belongs_to :group
validates :user_id, :presence => true
validates :group_id, :presence => true
validates :user_id, :uniqueness => {:scope => [:user_id, :group_id]}
end
#Group.rb :
class Group < ApplicationRecord
has_secure_token :auth_token
has_many :members, :dependent => :destroy
has_many :users, through: :members, source: :user
belongs_to :owner, class_name: "User"
def to_param
auth_token
end
end
What I tried :
I added a reference of group_id in the link table. And added belong_to :group in link.rb, and has_many :links, dependent: :destroy in group.rb.
In my new link form I added the current_user group_id in a select (to retrieve only the groups where the user is in) and on create the link is created with the group_id and the current_user id. That works.
Problem is that I have to enter a group_id which means I give no choice to the user and he has to give a group_id so basically he must share the link to the group. Which is not what I want.
What I thought about :
Maybe I should just go for the same idea as I did for members. Which means having a new table like grouplinks where I give the group_id, link_id and the user_id with relations in place so I can use the grouplink.id to share in my group or not. Is this a good option ? If yes what are the relations I should put in place ? Any other suggestion, maybe I'm completly wrong and there is something easy to do and I don't see it.
I will try to give a shot with some code I thought about :
#Link.rb:
class Link < ApplicationRecord
belongs_to :user
belongs_to :grouplink
end
#User.rb :
class User < ApplicationRecord
has_many :links, dependent: :destroy
has_many :grouplinks, dependent: :destroy
has_many :members, :dependent => :destroy
has_many :groups, :through => :members
end
#Grouplink.rb :
class Member < ApplicationRecord
belongs_to :user
belongs_to :group
belongs_to :link
end
#Group.rb :
class Group < ApplicationRecord
has_secure_token :auth_token
has_many :members, :dependent => :destroy
has_many :users, through: :members, source: :user
belongs_to :owner, class_name: "User"
has_many :groupslinks, :dependent => :destroy
has_many :links, through: :grouplinks
def to_param
auth_token
end
end
Could that work out ?
Thanks for your help.
It's better to create a join table with group_id and link_id. Since the Group Admin is the user who created the group, you don't even need to add user_id(member_id I suppose) since every member of that group can access the link. Get the user groups and add that link to that particular group and check the association.
class GroupLink
belongs_to :group
belongs_to :link
You can just do
#group = #user.groups.find(params[:group_id])
#group_link = #group.group_links.build(link_id: link_id)
#group_link.save
Let me know if you need anything else or clarify this.
Rails 4.2 newbie:
2 Questions;
1) Is the first has_many redundant? Since its name is a plural of Save Class?
can I have only:
has_many :savers, through: :saves, source: :saver
Or even better;
has_many :savers, through: :saves
If the answer is yes, where can I set "dependent: :destroy"?
class Post < ActiveRecord::Base
belongs_to :user
has_many :saves, class_name: "Save", foreign_key: "saver_id", dependent: :destroy
has_many :savers, through: :saves, source: :saver
end
class Save < ActiveRecord::Base
belongs_to :saver, class_name: "User"
validates :saver_id, presence: true
end
class User < ActiveRecord::Base
has_many :posts, dependent: :destroy
...
end
2) This is the typical blog model, where user can 'save' posts posted by another user to their timeline. Does this model make use best practices? Specially in db performance, doing a Join to get posts saved by a User. The 'Save' table that will have 100MM rows?
Lets first alter your example a bit to make the naming less confusing:
class User
has_many :bookmarks
has_many :posts, through: :bookmarks
end
class Post
has_many :bookmarks
has_many :users, through: :bookmarks
end
class Bookmark
belongs_to :user
belongs_to :post
end
Lets have a look at the query generated when we do #user.posts
irb(main):009:0> #user.posts
Post Load (0.2ms) SELECT "posts".* FROM "posts" INNER JOIN "bookmarks" ON "posts"."id" = "bookmarks"."post_id" WHERE "bookmarks"."user_id" = ? [["user_id", 1]]
=> #<ActiveRecord::Associations::CollectionProxy []>
Now lets comment out has_many :bookmarks and reload:
class User
# has_many :bookmarks
has_many :posts, through: :bookmarks
end
irb(main):005:0> #user.posts
ActiveRecord::HasManyThroughAssociationNotFoundError: Could not find the association :bookmarks in model User
So no, the first has_many is not redundant - in fact its the very core of how has_many through: works. You setup a shortcut of sorts through another relation.
Note in has_many :posts, through: :bookmarks :bookmarks is the name relation we are joining through. Not the table which contains the joins.
To fix your original code you would need to do:
class Post < ActiveRecord::Base
has_many :saves, dependent: :destroy
has_many :savers, through: :saves
end
class Save < ActiveRecord::Base
belongs_to :saver, class_name: "User"
belongs_to :post # A join table with only one relation is pretty worthless.
validates :saver_id, presence: true
end
class User < ActiveRecord::Base
has_many :posts
has_many :saves, dependent: :destroy
has_many :posts, through: :saves
end
Note that you don't need half the junk - if you have has_many :savers, through: :saves ActiveRecord will look for the relation saver by itself. Also you only want to use dependent: destroy on the join model - not on the post relation as that would remove all the posts a user has "saved" - even those written by others!
Teaching Rails myself, I want to learn the professional way to use the framework and following Rails guidelines best practices. That's not easy, because I usually find answers that 'just works'
I'll try to answer myself and maybe it could be useful for Rails Newbies:
Using has_many through, association, Rails firstly infers the association by looking at the foreign key of the form <class>_id where <class> is the lowercase of the class name, in this example; 'save_id'.
So, if we have the column name 'save_id', we will have the following simplified model:
class Post < ActiveRecord::Base
belongs_to :user
has_many :saves, through: :saves
end
class Save < ActiveRecord::Base
belongs_to :savers, class_name: "User"
validates :save_id, presence: true
end
class User < ActiveRecord::Base
has_many :posts, dependent: :destroy
...
end
If we have 3 models => Customer, User and Thing and another model Owner thats inherits from User and we try create a has_many through association like this:
class Customer < ActiveRecord::Base
has_many :things, :dependent => :destroy
has_many :owners, through: :things
end
class Thing < ActiveRecord::Base
belongs_to :customer, foreign_key: "customer_id"
belongs_to :owner, foreign_key: "owner_id"
end
class Owner < User
has_many :things, :dependent => :destroy
has_many :customers, through: :things
end
Why will #owner.things not work for us? (#owner is an instance of Owner). It gives undefined method "things" error.
#owner is the current_user, but how do you specify it to be an instance of User?
Is the only solution to change owner_id to user_id or is there a better solution, please?
As you state, current_user is an instance of User, not of its subclass, Owner.
If you want to add that relation to the current_user, you could add it to its class, User, instead of Owner:
class User < ActiveRecord::Base # or whatever superclass you have for User
has_many :things, dependent: :destroy
end
Otherwise, if you want to stick to Owner, you should overwrite the creation of current_user so that it uses the Owner class instead of User.