I'm new to rails , and I have a problem with the nested forms and all of that.
I have a User model, and an Organization model.
When I want to create a user, I want to specify from which organization does he comes from.
Either the organization name is already in the database or if it's not, I want to create a new record and associate that record the User model.
I have hard time understanding all the relations (many-to-many etc) implications in the rails framework, but so far I've got this.
model/organization.rb
class Organization < ActiveRecord::Base
has_many :user
validates_presence_of :name
end
model/user.rb (short)
class User < ActiveRecord::Base
belongs_to :organization
accepts_nested_attributes_for :organization
#####
end
From this, in the console, I can create user and specify and organization name , and it will create a new record for the user and a new record for the organization.
The problem is that it creates a new organization each time.
I want to be able to associate an already existing organization to a new user.
I can get the list of organization with things like typeahead.js for the form, so the name will be the same when the user selects one. But I don't know how to relate the two (the newly created user and already existing organization).
I thought of putting a hidden field with the id of the organization, and check in the controller if this id exists. If it does, put this id, if it doesn't create a new one. But I don't even know how to do this. In the console, when I update the attributes of a user, for example , with an organization_id = 3 which exists :
u.update_attributes( :organization_attributes => { id: 3 } )
It rejects saying he didn't find a user with ID=... with Organization.id = 3 ...
I don't understand.
I suppose since this is a common case, that this should be easy , but it's messing with my head.
If someone is willing to explain to me, I'd be very grateful.
Thank you.
EDIT
i've just tried something in my controller but that doesn't work either.
def create
#user = User.new(user_params) # :user object built from user inputform
org = Organization.find_by(name:user_params[:organization_attributes][:name])
if org
#user.organization.id = org.id
end
if #user.save
# signin the user (token etc)
sign_in #user
flash[:success] = "Registration sucessfull !"
redirect_to #user
else
render 'new'
end
end
+user_controller (strong params)
def user_params
params.require(:user).permit(:lname,:email,:fname,:password,:password_confirmation,
:gender,:role,:display_private,:link_li,:country,:city,:phone,:hobbies,
:avatar,:org_name, :organization_attributes => [ :id, :name])
end
+form.html.erb
<%= u.fields_for :organization do |o| %>
<%= o.label "Organization" %>
<!-- PUT ORGA -->
<%= o.text_field :name, class:"form-control" %>
<% end %>
I would write a custom method for this:
#in User
def organization_name
(org = self.organization) && org.name
end
def organization_name=(name)
if org = Organization.find_by_name(name)
self.organization = org
else
self.organization = Organization.create(:name => name)
end
end
Now, because you've got a getter and setter method (ie two methods with the same name, apart from the = sign), you can treat organization_name like an attribute of User and put it in a form field like
f.input :organization_name
The input will get the current value from #user.organization_name and will call #user.organization_name= with the new value.
First take away the accepts_nested_attributes from the model.
Then in your controller you should do something like:
def create
#user = User.new(user_params) # :user object built from user inputform
org = Organization.where(name: user_params[:organization_attributes][:name]).first || Organization.create(name: user_params[:organization_attributes][:name])
#user.organization = org
if #user.save
# signin the user (token etc)
sign_in #user
flash[:success] = "Registration sucessfull !"
redirect_to #user
else
render 'new'
end
end
In your app/model/user.rb
def self.create(name, attribute1, ... ,organization)
user = User.new(:name => name, :atr_1 => attribute_1, ....:atr_n => attribute_n)
user.organization = organization
raise "user not created" if !user.save
user
end
In users_controller.rb
def create
org = Organization.find params['organization'] #expecting the param to be Organization#id
user = User.create(params['name'], ..., org)
render :json => {:message => "user created"}
end
Related
I've read several SO links on this topic. Even if you can hack it to get current_user in model, you shouldn't do it. So, what are my options in my case?
I'm using the devise_invitable gem, and one of the commands is User.invite!({:email => email}, current_user), which stores who the user is invited by (current_user). I'd like to have this information.
Currently, users are invited to join a private group, and this process is handled in my group.rb model:
# group.rb
def user_emails
end
def user_emails=(emails_string)
emails_string = emails_string.split(%r{,\s*})
emails_string.each do |email|
user = User.find_for_authentication(email: email)
if user
self.add user
GroupMailer.welcome_email(user)
else
User.invite!(email: email) # But I want this: User.invite!({:email => email}, current_user)
user = User.order('created_at ASC').last
self.add user
end
end
end
If relevant, it's just a text_area that receives these emails to process:
# groups/_form.html.erb
<%= f.text_area :user_emails, rows: 4, placeholder: 'Enter email addresses here, separated by comma', class: 'form-control' %>
Without having to re-arrange too much, how can I run User.invite!({:email => email}, current_user) in this process, so that this useful information (who is invited by whom) is stored in my database? Much thanks!
Update:
With #Mohamad's help below, I got it working.
# group.rb
def emails
end
def invite_many(emails, inviter)
emails.split(%r{,\s*}).each do |email|
if user = User.find_for_authentication(email: email)
add user
GroupMailer.group_invite user
else
add User.invite!({:email => email}, inviter)
end
end
end
# groups_controller.rb
def update
#group = Group.friendly.find(params[:id])
if #group.update_attributes(group_params)
emails = params[:group][:emails]
#group.invite_many(emails, current_user) # also put this in #create
redirect_to #group
else
flash[:error] = "Error saving group. Please try again."
render :edit
end
end
And then nothing in my User model because User.invite is defined already by devise_invitable and I didn't need to do anything else. This process is working great now!
There are some subtle issues with your code. There's a potential race condition on the else branch of your code where you try to add the last created user. I'm also unsure that you need a setter method here unless you are access emails from elsewhere in the instance of Group.
As suggested by others, pass the current user as an argument form the controller. I'm not sure how invite! is implemented, but assuming it returns a user, you can refactor your code considerably.
I would do somethng like this:
def invite_many(emails, inviter)
emails.split(%r{,\s*}).each do |email|
if user = User.find_for_authentication(email: email)
add user
GroupMailer.welcome_email user
else
add User.invite!(email, inviter)
end
end
end
# controller
#group.invite_many(emails, current_user)
# User.invite
def invite(email, inviter)
# create and return the user here, and what else is necessary
end
If you are calling user_emails() from the controller (and I'm guessing you are as that must be where you are receiving the form to pass in emails_string), you can pass in the current_user:
user_emails(emails_string, current_user)
and change user_emails to receive it:
def user_emails=(emails_string, current_user)
You can store the current_user with global scope ,like #current_user,which can be assigned in sessions controller,so in model you will just #current_user as the current user of the app.
In my app I want to add feature to create several objects in one action:
on localhost:3000/bank_accounts/new I have a form for creating one object of #bank_account
= simple_form_for #bank_account do |f|
= f.input :bank_name
= f.input :account_number
= f.button :submit
controller:
def new
#bank_account = BankAccount.new
end
def create
#bank_account = BankAccount.create(bank_params)
if #bank_account.save
redirect_to root_url
else
render 'new'
end
end
Is there a method to create several objects of BankAccount on localhost:3000/bank_accounts/new?
In my opinion you should follow a 'standard' way,
probably bank account belongs to a user,
so you have a BankAccount that belongs to a User and a User has many BankAccounts.
In that case, in the controller you should have something like:
#user = User.find 1234 # get a user
10.times do
#user.bank_accounts.build
end
and in the view you should use a loop to create the fields for all the user's bank accounts (you can user fields_for)
Finally, to create those bank accounts you can even use mass assignment with:
accepts_nested_attributes_for :bank_accounts
I hope this can help :)
When a user creates a new booking record, part of the data entered is an email address. This email address is used to create a new guest record at the same time if they don't already exist. The booking should have the guest id as part of the record.
In my models I have defined the relationships so:
accommodations has_many bookings
guests has_many bookings
bookings belongs_to accommodations
bookings belongs_to guests
This is what I have so far in the create action of my BookingsController:
...
def create
accommodation = current_user.accommodation
#booking = accommodation.bookings.build(post_params)
#guest = accommodation.guests.build(params[:email])
if #booking.save
flash[:success] = 'The booking has been added successfully.'
redirect_to :controller => 'bookings', :action => 'index'
else
render 'new'
end
end
...
My questions are:
Should I use 'build' twice as I want the new booking to have the guest id?
How can I check if guest exists already using email?
Is it safe/secure to use params[:email] when building the guest?
If you're not using #guest in the view, there's no need for it to be an instance variable. So:
accommodation = current_user.accommodation
guest = Guest.find_or_create_by(:email => params[:email])
#booking = accommodation.bookings.new(post_params.merge(:guest_id => guest.id))
You don't need to use build in your #create method because its main use is to maintain association ties with objects that still don't have a primary key. But since you're persisting your stuff in here, we can go with good old new from Ruby.
In my Rails app I have an update action that users can use to update their profile.
The tricky thing I want to achieve is that if a user enters a new email address and saves it, that email address won't get saved to the email database field straightaway, but rather to a database field called new_email. The field email should remain untouched (at least until the user has confirmed that email address later on).
def update
current_email = #user.email
new_email = params[:user][:email].downcase.to_s
if #user.update_attributes(params[:user])
if new_email != current_email
#user.change_email(current_email, new_email)
flash[:success] = "Profile updated. Please confirm your new email by clicking on the link that we've sent you."
else
flash[:success] = "Profile updated."
end
redirect_to edit_user_path(#user)
else
render :edit
end
end
User model:
def change_email(old_email, new_email)
self.new_email = new_email.downcase
self.email = old_email
self.send_email_confirmation_link
end
The function above kind of works but is hard to test and doesn't feel right. Is there a smoother way to achieve this?
Thanks for any help.
If you change your form so that you're updating new_email, you can just put it all in a simple after_update hook.
after_update :check_new_email
private
def check_new_email
send_email_confirmation_link if new_email_changed?
end
I think you could use "virtual" attribute, called - let's say - email_input and show field for this attribute (instead of email) in view:
<%= f.text_field :email_input %>
Then in your model you should have:
class User < ActiveRecord::Base
attr_accessor :email_input
attr_accessible :email_input
before_save :set_email, :if => lambda{|p| p.email_input.present?}
# ...
def set_email
email_input.downcase!
if new_record?
self.email = email_input
else
self.new_email = email_input
send_email_confirmation_link
end
end
end
I may just be missing something simple, but I am relatively inexperienced so it is likely. I've searched extensively for a solution without success.
I am using the fields_for function to build a nested form using the accepts_nested_attributes_for function. If the submit on the form fails the params are passed to the render of the new template only for the parent model. How do I pass the nested params for the child model so that fields that have been filled out previously remain filled. Note that I am using simple_form and HAML but I assume this shouldn't impact the solution greatly.
My models:
class Account < ActiveRecord::Base
attr_accessible :name
has_many :users, :dependent => :destroy
accepts_nested_attributes_for :users, :reject_if => proc { |a| a[:email].blank? }, :allow_destroy => true
end
class User < ActiveRecord::Base
attr_accessible :email, :password, :password_confirmation
belongs_to :account
end
My accounts controller:
def new
#account = Account.new
#account.users.build
end
def create
#account = Account.new(params[:account])
if #account.save
flash[:success] = "Welcome."
redirect_to #account
else
#account.users.build
<- I suspect I need something here but unsure what
render :new
end
end
The key part of the accounts/new view:
= simple_form_for #account do |f|
= f.input :name
= f.simple_fields_for :users do |u|
= u.input :email
= u.input :password
= u.input :password_confirmation
= f.button :submit, :value => "Sign up"
My params on a failed save are:
:account {"name"=>"In", "users_attributes"=>{"0"=>{"email"=>"u#e.com", "password"=>"pass", "password_confirmation"=>"pass"}}}
As you can see, the key information, in the users_attributes section, is stored but I can't seem to have the email address default into the new form. Account name on the other hand is filled automatically as per Rails standard. I'm not sure if the solution should live in the accounts controller or in the accounts/new view, and have not had any luck with either.
Answers with .erb are, of course, fine.
I'm fairly new to Ruby and Rails so any assistance would be much appreciated.
The problem lies with attr_accessible, which designates the only attributes allowed for mass assignment.
I feel a bit silly in that I actually stated the problem in a comment last night and failed to notice:
accepts_nested_attributes_for :users will add a users_attributes= writer to the account to update the account's users.
This is true, but with attr_accessible :name, you've precluded every attribute but name being mass-assigned, users_attributes= included. So when you build a new account via Account.new(params[:account]), the users_attributes passed along in params are thrown away.
If you check the log you might note this warning:
WARNING: Can't mass-assign protected attributes: users_attributes
You can solve your original problem by adding :users_attributes to the attr_accessible call in the account class, allowing it to be mass-assigned.
Amazingly, after reading a blog post this evening, and some more trial and error, I worked this out myself.
You need to assign an #user variable in the 'new' action so that the user params are available for use in the 'create' action. You then need to use both the #account and #user variables in the view.
The changes look like this.
Accounts Controller:
def new
#account = Account.new
#user = #account.users.build
end
def create
#account = Account.new(params[:account])
#user = #account.users.build(params[:account][:user]
if #account.save
flash[:success] = "Welcome."
redirect_to #account
else
render :new
end
end
The accounts/new view changes to:
= simple_form_for #account do |f|
= f.input :name
= f.simple_fields_for [#account, #user] do |u|
= u.input :email
= u.input :password
= u.input :password_confirmation
= f.button :submit, :value => "Sign up"
In this case the params remain nested but have the user component explicitly defined:
:account {"name"=>"In", "user"=>{"email"=>"user#example.com", "password"=>"pass", "password_confirmation"=>"pass"}}
It has the additional side effect of removing the #account.users.build from within the else path as #numbers1311407 suggested
I am not certain whether their are other implications of this solution, I will need to work through it in the next few days, but for now I get the information I want defaulted into the view in the case of a failed create action.
#Beerlington and #numbers1311407 I appreciate the help in guiding me to the solution.