Creating many related models before_save - ruby-on-rails

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.

Related

Rails: Validating an array of ids

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.

rails join table issue with different roles (owner, non-owner)

In my app users can create products so at the moment User has_many :products and Product belongs_to :user. Now I want the product creator product.user to be able to invite other users to join the product, but I wanna keep the creator the only one who can edit the product.
One of the setups I've got in my mind is this, but I guess it wouldn't work, since I don't know how to distinguish between created and "joined-by-invitation" products when calling user.products.
User
has_many :products, through: :product_membership
has_many :product_memberships
has_many :products # this is the line I currently have but think it wouldn't
# work with the new setup
Product
has_many :users, through: :product_membership
has_many :product_memberships
belongs_to :user # I also have this currently but I'd keep the user_id on the product
# table so I could call product.user and get the creator.
ProductUsers
belongs_to :user
belongs_to :product
Invitation
belongs_to :product
belongs_to :sender, class: "User"
belongs_to :recipient, class: "User"
To work around this issue I can think of 2 solutions:
Getting rid of the User has_many :products line that I currently have and simply adding an instance method to the user model:
def owned_products
Product.where("user_id = ?", self.id)
end
My problem with this that I guess it doesn't follow the convention.
Getting rid of the User has_many :products line that I currently have and adding a boolean column to the 'ProductUsers' called is_owner?. I haven't tried this before so I'm not sure how this would work out.
What is the best solution to solve this issue? If none of these then pls let me know what you recommend. I don't wanna run into some issues later on because of my db schema is screwed up.
You could add an admin or creator attribute to the ProductUsers table, and set it to false by default, and set it to true for the creator.
EDIT: this is what you called is_owner?
This seems to be a fairly good solution to me, and would easily allow you to find the creator.
product.product_memberships.where(is_owner?: true)
should give you the creator

Building in model callback returning nil for value

First, thanks for taking the time to read. I'm new to Rails and have been stuck on this one for many hours.
In my Rails 3.2 app, I have three models: User, Organization, and Membership (the last is a join model between User and Organization).
When a user creates an organization, he/she should become a member upon create. So, in my Organization model, I've included a before_create callback that builds a Membership. The problem is that while the Membership builds when the new Organization is created, the user_id on the Membership object is set to "nil.," and therefore the current user is not a member.
Hardcoding in the user_id attribute in the callback actually does correctly build the membership, i.e. (:user_id => "1"), but in general asking the Organization model to be aware of current user state seems like bad MVC practice.
What's the proper way to set the current user ID on the new Membership? It seems like my associations should handle that, but I might be wrong.
Here are my models — I'm leaving out some validation lines for readability's sake. Thanks so much in advance.
user.rb
class User < ActiveRecord::Base
has_many :memberships
has_many :organizations, :through => :memberships
end
membership.rb
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :organization
end
organization.rb
class Organization < ActiveRecord::Base
has_many :memberships
has_many :users, :through => :memberships
accepts_nested_attributes_for :memberships, :allow_destroy => true
...
before_create :add_membership
protected
def add_membership
self.memberships.build
end
end
You are right in the fact that allowing your model to magically know about the current user is bad MVC practice. So you have to somehow pass the current user id during creation. you can do this in many ways ; for example in the controller :
def create
#organization = Organization.new( params[:organization] ) do |org|
org.memberships.build( user_id: current_user.id )
end
# save, etc.
end
Doing this in the controller is fine, but it would be better if your business logic would reflect the fact that a user creating an organization should automatically belong to it. You could override new and / or create on Organization (or create your own method if you fear overrides) :
def new( params = {}, options = {} )
creator = options.delete( :creator )
super( params, options ) do |org|
org.memberships.build( user_id: creator.id ) if creator
yield org if block_given?
end
end
passing the user is easy now :
def create
#organization = Organization.new(params[:organization], creator: current_user)
end
If you don't like this approach, or if you don't want to override new or create a specific factory method, you can also make something similar to nested_attributes :
attr_accessible :creator_id
def creator_id=( user_id )
memberships.build user_id: user_id
end
then in your view :
f.hidden_field :creator_id, current_user.id
optional :
with first approach, for additional clarity / ease of use, you can also create a method on User :
def new_organization( params = {}, options = {}, &block )
Organization.new( params, options.merge(creator: self), &block )
end
... ok, Organization is hardcoded here (bad !) but your workflow is now quite understandable :
def create
# we know at first glance that the user is responsible for the organization
# creation, and that there must be specific logic associated to this
#organization = current_user.new_organization( params[:organization] )
# etc
end
with a little more thinking, it should be possible to avoid hardcoding Organization into User (using an association extension for instance)
EDIT
To be able to setup a validation on membership's organization presence, you need to do this :
class Organization < ActiveRecord::Base
has_many :memberships, inverse_of: :organization
end
class Membership < ActiveRecord::Base
belongs_to :organization, inverse_of: :memberships
validates :organization, presence: true
end
Let's explain this :
inverse_of sets up your associations to be bidirectional. By default, associations are one-way, which means that when you do organization.memberships.first.organization, rails tries to load the organisation again because it does not know how to "climb back" the association. When using inverse_of, rails knows it does not have to reload the organization.
validates MUST be setup on organization and NOT on organization_id. This way the validator knows we're "climbing back" the association, it knows that organization is a "parent" record and that it's in the process of being saved - so it does not complain.

Proper Usage of Rails After_Filter

In a small app I am building, I have a controller that creates an exchange. When a user creates an exchange they are simultaneously the organizer of the exchange and a participant in the exchange. Participants are tracked by a join table that joins a user_id and an exchange_id. Organizers are tracked by a foreign user_id key in the exchange table.
I am trying to figure out where to put the code that will automatically create a new membership record for the organizer of the exchange. Should I put this in the exchange_controller's create action itself, or in an after_filter triggered by the create action? Or maybe somewhere else? Part of the problem is that I could not find any good examples of proper after_filter use (guides.rubyonrails.org only had sparse mention of it), so any links pointing in the correct direction would be appreciated as well.
Here is relevant model code:
app/models/user.rb:
# Returns array of exchanges user is participating in
has_many :participations,
:through => :memberships,
:source => :exchange
# Returns array of exchanges user has organized
has_many :organized_exchanges,
:foreign_key => :organizer_id,
:class_name => "Exchange"
app/models/membership.rb:
class Membership < ActiveRecord::Base
attr_accessible :exchange_id, :user_id, :role
belongs_to :exchange
belongs_to :user
end
app/modles/exchange.rb:
belongs_to :organizer,
:foreign_key => :organizer_id,
:class_name => "User"
has_many :memberships, :dependent => :destroy
has_many :participants,
:through => :memberships,
:source => :user
And here is the relevant controller code:
app/controllers/exchanges_controller.rb:
def create
#exchange = Exchange.new(params[:exchange])
#exchange.organizer_id = current_user.id
if #exchange.save
redirect_to exchange_path(#exchange.id)
else
render 'new'
end
end
after_filter is a completely different thing in this context. It is called when your view is completely processed and so you want to call some action to do something.
You can use after_create callback that is triggered when a record is created in the database.
In your case, a user is creating an exchange and so after the exchange is created, the after_create callback is triggered and you can apply your functionality over there to make the current user who created the exchange to be a participant.
The way to write in a model is like this:
after_create :do_something
def do_something
something.do!
end
Note: It is not good to use after_save here because it is triggered every time you save a record or even if you update a record.
There is a nice SO post that clearly tells you the difference between the after_create and after_save.
See this SO post for the difference between the two.
More on the callbacks is here.

Nested form and has_many :through

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

Resources