I have a little sample app where there are 3 models: Members, Groups and Subscriptions. The idea is that member can subscribe to groups.
class Member < ActiveRecord::Base
has_many :subscriptions, dependent: :delete_all
has_many :groups, through: :subscriptions
attr_accessible :email
validates :email, presence: true
end
class Group < ActiveRecord::Base
has_many :subscriptions, dependent: :delete_all
has_many :members, through: :subscriptions
accepts_nested_attributes_for :subscriptions
attr_accessible :name, :subscriptions_attributes
validates :name, presence: true, uniqueness: true
end
class Subscription < ActiveRecord::Base
belongs_to :group
belongs_to :member
attr_accessible :group_id, :introduction
validates :group_id, presence: true
validates :introduction, presence: true
end
I'm trying to create a form for new groups, and nest the introduction attribute inside.
My controller methods:
def new
#group = Group.new
#group.subscriptions.build
end
def create
#member = Member.first
#group = #member.groups.build(params[:group])
if #group.save
flash[:success] = "Saved"
redirect_to group_path(#group)
else
render :new
end
end
But it does not work. It throws the error group_id can't be blank. So I don't know how to assign the new group to the subscription.
Also, the member_id is being created as nil. But as you can see, I'm creating the group from the #member variable, so I think it should be initialized, but it does not.
Anyone can show me the light?
You can see the sample app here: https://github.com/idavemm/nested_form
Make sure all the attributes you're trying to assign are attr_accessible. You may just disable it and see if it works, or see at the warnings in the Rails server log.
Update: you should add accepts_nested_attributes_for to the Member model and use a multimodel form with fields_for.
I think you're thinking about your models in the wrong way. Will each member have a different introduction for each group. So, for example, will member1 have one introduction and member2 have a different introduction?
The Subscriptions model should store information about the relationship between and member and group. In that case, introduction would be better to have in the group model. The reason you are getting an error is because you are trying to create a subscription(when you set the introduction attribute) for a group that hasn't been made yet.
So, move introduction to the group model and then, if you want the creator of a group to be automatically subscribed to it (which you should), add the code to create a subscription to the controller in the create action after the record is saved. Then, on the subscription model, you can do cool things like having a state machine that tracks a member's status with the group (moderator, newbie, veteran member, etc).
After many hours of investigation and frustration, I reported it to Rails devs and I finally have a solution:
Rails 3 is unable to initialize group_id and member_id automatically in the way it is defined in the question.
So, for now, there are two ways to make it work:
Add the member_id as a hidden field in the view.
Change everything so is the Subscription model who has accepts_nested_attributes_for. That way, the new object to be created is the Subscription, and Group will be the nested model.
The first option has an important security hole, so I don't recommend it.
The second option, although not much logical, is the cleaner and supposedly the "Rails way" to fix this problem.
I just ran into the same issue, the solution was to remove the presence validation from the related model (based on this question), in your case, remove:
validates :group_id, presence: true
Once that validation it's gone, everything runs like clockwork
Related
Application
I am working on a college admissions system where a student can make an application to up to 5 courses. The way I have designed this is to have an Application model and a CourseApplication model. An application can consist of many course_applications:
class Application < ActiveRecord::Base
# Assosciations
belongs_to :user
has_many :course_applications, dependent: :destroy
has_many :courses, through: :course_applications
has_one :reference
# Validations
validates :course_applications, presence: true
end
Course Application
class CourseApplication < ActiveRecord::Base
# Intersection entity between course and application.
# Represents an application to a particular course, has an optional offer
# Associations
belongs_to :application
belongs_to :course
has_one :offer, dependent: :destroy
end
I want to make sure that a student cannot apply to the same course twice. I have already done some research but have had no success. This is the UI for a student making an application:
Screenshot of application form
When a course is selected, the course id is added to an array of course ids:
def application_params
params.require(:application).permit(:user, course_ids: [])
end
Right now a student can select the same course twice, I want to prevent them from doing this. Any help is much appreciated.
For the rails side, I would do on the CourseApplication
validates :course, uniqueness: { scope: :application }
For your reference this can be found at: http://guides.rubyonrails.org/active_record_validations.html#uniqueness
Also suggest on the database side to make a migration
add_index :course_applications, [:course, :application], :unique => true
For the validating on the form you will have to write javascript to make sure two things are selected, this will just return an error when someone tries to do it.
I have an issue creating a new record with nested associations in a clean way. Here's the controller code:
#listing = current_user.listings.build(params[:listing].permit(ATTRIBUTES_FOR_CREATE))
This builds an entity with several nested associations, like this:
class ListingDataField < ActiveRecord::Base
belongs_to :listing
validates_presence_of :listing
end
However, when I do #listing.save in controller, I get validation errors on those nested ListingDataField entities that 'listing can't be blank'. If I understand correctly, AutosaveAssociation first validates and saves nested associations, and eventually saves top-level entity. Thus, it fails validating ListingDataField, because Listing is not yet saved.
But I believe it's right having :listing validation in ListingDataField, so I wouldn't consider removing this. I can see 2 solutions:
in transaction - save Listing record, then build nested associations
one by one
#listing.save(:validate => false) but this is too ugly
Both aren't as much elegant as current_user.listings.build(...), so my question is - what is the proper Rails way for this?
P.S. I searched SO for similar question but I couldn't find any, hopefully this is not a duplicate :)
Have you tried adding:
class ListingDataField < ActiveRecord::Base
belongs_to :listing, inverse_of: :listing_data_fields
validates :listing, presence: true
end
and
class Listing < ActiveRecord::Base
has_many :listing_data_fields, inverse_of: :listing
end
This should make validation of presence work.
I have a newbie rails question. I'm trying to make sure a model has at least one association via a HABTM relationship. Basically I have created the following validation:
validate :has_tags?
def has_tags?
errors.add(:base, 'Must have at least one tag.') if self.tags.blank?
end
This works fine when I create a new record. The problem is when I take the model and try to remove the association, doing something like this:
tag = Tag.find(params[:tag_id])$
#command.tags.delete(tag)$
It is permitted, i.e. the association will be deleted. Based on my reading on HABTM associations (http://guides.rubyonrails.org/association_basics.html#the-has-and-belongs-to-many-association), I should "use has_many :through if you need validations, callbacks, or extra attributes on the join model."
I guess my question is how to perform validation on the .delete method for an association. Should I do this manually when I call delete (i.e. run a separate join to count the number of associations before executing a delete), or is there a way to use a validation model when deleting? Here is my model:
class Command < ActiveRecord::Base
has_many :tagmapsorters
has_many :tags, through: :tagmapsorters
validates :text, presence: true
validates :description, presence: true
validates :text, uniqueness: true
validate :has_tags?
def has_tags?
errors.add(:base, 'Must have at least one tag.') if self.tags.blank?
end
end
I appreciate you taking the time to help me.
Dan
Any callbacks that you need should be registered as before_destroy (for validations) or after_destroy (for cleanup) on the join model Tagmapsorter, as that is the record that is actually being destroyed.
I’m new to Rails and I’ve been reading Michael Hartl’s tutorial and I am not sure if I’m doing this correctly. I have Users, Posts, and Categories:
Users can create Posts and Categories
A Post can be assigned to only one Category.
Currently, when the User creates a post they type in the Category (let’s just say the Category will always exist in the database) and from there it looks up the ID of the Category and passes that along to the post creation. This is what I’m running to create the post and assign it to a Category in my Post_Controller:
category_id = Category.find_by_name(post_params[:category])
#post = current_user.posts.build(title: post_params[:title], content: post_params[:content], category_id: category.id)
My question is: Is this the proper way to enter data with two belong_to’s? I’ve dug around and I can’t find a simple answer to this. To me it seems that passing a category ID like this is not secure but I don’t know of another way to do this. Here’s my basic Model information (just the belong_to’s, has_many, etc). Please let me know if you need more:
class User < ActiveRecord::Base
attr_accessible :username, :email, :password, :password_confirmation
has_secure_password
has_many :posts
has_many :categories
class Post < ActiveRecord::Base
attr_accessible :title, :content, :category_id
belongs_to :user
belongs_to :category
class Category < ActiveRecord::Base
attr_accessible :name
belongs_to :users
has_many :posts
validates :name, presence: true, uniqueness: true, length: {maximum:30}
validates :user_id, presence: true
Is this the proper way to enter data with two belong_to’s?
It's fine. Does it work? If it works, it's fine. Maybe there are things you could do to tighten it up later, if you find you're making the Category.find... call a lot, but, you're also just starting, so don't worry about stuff like that too much.
To me it seems that passing a category ID like this is not secure but I don’t know of another way to do this.
Again, don't worry about that too much at the moment. If you want to read up on Rails security, though, take a look at this.
If all the information you need to create a new post is in post_params, it seems you're doing that Category.find_by_name unnecessarily. You should be collecting the category_id in your params rather than the category's name.
And then in your PostsController:
#post = current_user.posts.build(post_params)
Just remember to allow :category_id along with the other regular attributes in your post_params and you'll be golden.
I have a User model with devise, a Team model, a Player model and a Trainer model. I want that when the User signs up, its associated team is created along with the associated trainer and players. I have set up the models as below:
user.rb
has_one :team
has_one :trainer, :through => :team
has_many :players, :through => :team
accepts_nested_attributes_for :team, :allow_destroy => true
before_save :create_team
def create_team
#team = Team.new(params[:team])
#team.user = self
#team.save
end
team.rb
belongs_to :user
has_one :trainer
has_many :players
accepts_nested_attributes_for :trainer, :allow_destroy => true
accepts_nested_attributes_for :player, :allow_destroy => true
trainer.rb and player.rb
belongs_to :team
I have not added the create_trainer and create_player functions, since I want the user to select them later in the game. So they should be empty during the creation of the user.
But the sign up process gives the following error:
No association found for name `trainer'. Has it been defined yet?
and refers to the line:
accepts_nested_attributes_for :trainer, :allow_destroy => true
in team.rb. What is wrong with having the Trainer item not defined yet, if there is no validation of presence of Trainer defined in the Team model? I tried adding some lines to the Trainer model, to set the attributes to default values like:
morale.default => (5..12).to_a.sample
but it gave further errors, so is probably wrong. Any comment is greatly appreciated, especially anything criticising the basis of tinking here, since I am a noob.
A few things:
Dont use instance variables in your model. To access the team in the user model, just do team or self.team.
Don't use before_save since you don't want to create the team each time you save your user.
Your create_team method should be:
after_create :my_create_team
def my_create_team
create_team #create an empty team
end
But if the data for the new team is already present in the form when the user signs up, then the team should automatically be created since you have accepts_nested_attributes_for :team.
I'm going to answer some of your questions in the comments here:
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_one
So when you add has_one :team you have now access to all these methods (build_team, create_team, team= etc)
By "empty", I just meant that if you just call create_team without any attributes, it would create a "default" team: no name etc. But it would be linked to your user though.
If you wanted to just create an "empty" team, you could just do this I think:
after_create :create_team
Creating your own method would just allow you to pass default parameters.
But you have probably added validation to the team, like validating the presence of its name.
Anyways, since you have accepts_nested_attributes_for :team, :allow_destroy => true, it should create the team automatically if you have the required fields for the user's team in the sign up form.