Using form_for with "becomes" loses child association - ruby-on-rails

When building out a form that handles STI, if I use becomes to transform the object to its parent class, I lose the ability to use nested fields with it.
I have two models
class Login < ActiveRecord::Base
belongs_to :user
end
class User < ActiveRecord::Base
has_one :login
accepts_nested_attributes_for :login
end
I also have a few subclasses of User.
class Consumer < User
end
class Admin < User
end
class Agent < User
end
Initially I had problems with the routing, since Rails would assume that I wanted a route specific to the current class rather than the parent class, so I used #user.becomes(User), which is apparently the way to handle that. For the most part it works fine, however this causes #user.login to disappear.
Controller
class Admin::UsersController < AdminController
load_and_authorize_resource
before_filter :authenticate_user!
def index
render 'index'
end
def new
#user = User.new
#user.build_login
render 'new'
end
def create
#user = User.new(user_params)
if #user.save
flash[:notice] = "Account confirmation instructions sent to #{#user.login.email}"
redirect_to new_user_path
else
flash.now[:error] = #user.errors.full_messages.to_sentence
# At this point, I can confirm that #user.login still exists...
render 'new'
end
end
private
def user_params
params.require(:user).permit(
:type,
:dealership_id,
login_attributes: [
:email
])
end
end
Here's the most relevant form view bit
<%= simple_form_for(#user.becomes(User), html: {class: "user-form"}) do |f| %>
<%= f.simple_fields_for :login do |l| %>
<div class="field">
<%= l.label :email %><br />
<%= l.email_field :email %>
</div>
<% end %>
<div class="field">
<%= f.label :type %>
<%= f.select :type, options_for_select(current_user.types_can_create), include_blank: "- Select -", class: "form-control", id: "select_type" %>
</div>
<div class="actions">
<%= f.submit "Register" %>
</div>
<% end %>
The text field for :email doesn't display because #user.login is now nil. Is this expected behavior when using becomes?

Having only used becomes once before, I can only attest to my scant experience -- whenever you use it, it essentially invokes a new instance of the class.
I'm not sure as to the specifics, but the bottom line is that I would surmise that your #user.becomes(User) is overriding #user.build_login...
Returns an instance of the specified klass with the attributes of the current record.
--
In your case, I would set the path explicitly (as you're using User anyway):
<%= simple_form_for #user, url: user_path, method: :post html: {class: "user-form"} do |f| %>

In Rails 5, this can be solved with
<%= form_with scope: :user,
model: #user,
url: #user.id ? user_path(user) : users_path,
local: true do |f| %>
...
...
<% end %>
The previous code instructs the FormBuilder to fill in the fields with the #user attributes, but it also instructs it to submit to the main route (not the inherited ones) and, with scope:, it also instructs it to name the fields using the User class name, not the child class names.

Related

Getting my Ruby on Rails page to save the user's email without using devise

I am building a one page website where visitors will simply be able to submit their email address. The only goal in the database is to get an email (no name, etc). There is only one page visible at first, which is the homepage. If the user submits an email already in use, it sends the user to an error page. If the email is not in use, it sends the user to a success page.
I have asked a question about this previously, and after a lot of comments and trial and error, it appeared that it worked and then it stopped working. When I do Rails C, there is only one user in the system and that user doesnt have an email...
Here is what my user migration looks like :
class CreateUsers < ActiveRecord::Migration[5.2]
def change
create_table :users do |t|
t.string :email
t.timestamps
end
add_index :users, :email, unique: true
end
end
Here is what my user model looks like:
class User < ApplicationRecord
end
Here is what users/new.html.erb looks like:
<%= form_for #user, as: :post, url: users_path do |f| %>
<div class="wrapper">
<%= f.email_field :email , id: "search", class:"search input" %> <br />
<%= f.submit "yep", class: "submit input" %>
</div>
<% end %>
Here is my user controller:
class UsersController < ApplicationController
def new
#user = User.new
end
def create
#user = User.new(params[:email])
if #user.save
redirect_to '/users/success'
else
redirect_to '/users/error'
end
end
def show
end
end
Here are my routes:
Rails.application.routes.draw do
root "users#new"
resources :users
end
When i run the code, it renders the homepage but when i click on submit, it sends me on a page called show.html.erb with http://localhost:3000/users/error on my brownser. No users are being saved in the console.
EDIT:
My model is
class User < ApplicationRecord
validates_uniqueness_of :email
end
It is still not working....
change new.html.erb as
<%= form_with(model: #user, local: true) do |f| %>
<div class="wrapper">
<%= f.email_field :email , id: "search", class:"search input" %> <br />
<%= f.submit "yep", class: "submit input" %>
</div>
your controller will be
class UsersController < ApplicationController
def new
#user = User.new
end
def create
#user = User.new(user_params)
if #user.save
redirect_to user_path(#user), notice: "yeh!!!!"
else
redirect_to new_user_path, notice: "email already registered"
end
end
def show
#user = User.find(params[:id])
end
private
def user_params
params.require(:user).permit(:email)
end
end
add
<p id="notice"><%= notice %></p> to your application.html.erb in layouts
rest as your question
There are a couple things wrong here.
You're so close, but you're misusing the as: attribute of form_for. Perhaps you think that will send as a POST request, but instead that is actually wrapping your form params in an object called "post". I saw this in the comments on another thread.
Remove the as: attribute and the helper will again wrap your params in the user object. While we're at it, you should also be able to remove the url: attribute as well since Rails form helpers are smart enough to infer that this is a new resourceful record and output the create URL as well as the POST action accordingly.
You need your controller to expect a whole "user" object instead of just checking for the email param. ALSO, assuming you're on Rails 4 or higher, you need to permit the email attribute to be mass-assigned on your User object. See the code.
def create
#user = User.new(params.require(:user).permit(:email)) # Not params[:email]
if #user.save
redirect_to '/users/success'
else
redirect_to '/users/error'
end
end
Also be careful about duplicate emails with different cases. The default in Rails is case-sensitive validation which means "JIM#gmail.com" would not trigger a validation error against "jim#gmail.com". You can fix this with.
class User < ApplicationRecord
validates :email, uniqueness: { case_sensitive: false }
end
BONUS!
Nowadays, it's better to move over to form_with (instead of form_for). It's on its way to becoming the new Rails standard and also makes a few of these things easier. The one point you'll want to keep in mind is that with form_with (and general Rails assumptions), forms are remote by default. So if you want to trigger a full page submit/refresh, add local: true to your form_with helper.
<%= form_with model: #user, local: true do |f| %>
<div class="wrapper">
<%= f.email_field :email , id: "search", class:"search input" %> <br />
<%= f.submit "yep", class: "submit input" %>
</div>
<% end %>
As you are using resources in routes so def show is called when route is /users/:id. That's why its calling show.html.erb file.
When you try to validate an email, then in model write the validation for it
class User < ApplicationRecord
validates_uniqueness_of :email
end
Hope this helps.
Try to add validates_uniqueness_of in your model
class User < ApplicationRecord::Base
attr_accessor :email
validates_uniqueness_of :email
end
And
def show
#user = User.find(email: params[:email])
end
And if you wanna check all
def show
#user = User.all
end
Please try this.
I hope that helpful

Update model attributes in a new controller

I have two models implemented User and Order with their own Controllers being used by AdminController.
Now I need to edit/update this model from Client with a ClientController.
I've created the following so far:
class Client::BaseController < ApplicationController
end
class Client::OrdersController < Client::BaseController
end
class ClientArea::UsersController < ClientArea::BaseController
end
I tried to add a new action to UserController in order to get the attributes of the User model and then change its password:
def update_password
#user = User.find(current_user.id)
end
and added the following to my view/client/user/update_password
<%= simple_form_for #user do |f| %>
<%= f.input :email, :label => 'Your email please' %>
<%= f.input :crypted_password, :hint => 'No special characters.' %># <%= f.input :remember_me, :as => :boolean %>
<%= f.button :submit %>
<% end %>
I know this is not the write code to update some attribute, but just with this code I'm already getting this error:
undefined method `individual_path' for #<#:0x007ffef8d06930>
I believe this error comes from the ClientController not-knowing the Model User, but I'm not sure, and I have no idea of how I could do this.

Best practice in creating belongs_to object

Let's say we have the following situation:
class User < ActiveRecord::Base
has_many :tickets
end
class Ticket < ActiveRecord::Base
belongs_to :user
end
For simplicity let's say Ticket has only some text field description and integer user_id. If we open User's views/users/show.html.erb view and inside User controller we have this code which finds correct user which is selected:
def show
#user = User.find(params[:id])
end`
Now inside that show.html.erb view we also have small code snipped which creates user's ticket. Would this be a good practice in creating it?
views/users/show.html.erb
<%= simple_form_for Ticket.new do |f| %>
<%= f.hidden_field :user_id, :value => #user.id %>
<%= f.text_area :description %>
<%= f.submit "Add" %>
<% end %>
controller/tickets_controller.rb
def create
#ticket = Ticket.new(ticket_params)
#user = User.find(ticket_params[:user_id])
#ticket.save
end
def ticket_params
params.require(:ticket).permit(:user_id, :description)
end
So, when we create a ticket for user, ticket's description and his user_id (hidden field inside view) are passed to tickets_controller.rb where new Ticket is created.
Is this a good practice in creating a new object which belongs to some other object? I am still learning so I would like to make this clear :) Thank you.
You should be able to do something like this in your form:
<%= f.association :user, :as => :hidden, :value => #user.id %>
This will pass user_id through your controller to your model and automatically make an association. You no longer need the #user= line in your controller.
Don't forget that the user could modify the form on their end and send any id they want. :)
See https://github.com/plataformatec/simple_form#associations for more info.
How about getting the user from the controller using current_user so that you protect yourself from anyone that would manipulate the value of the user_id in the form. Also I think this way is much cleaner
views/users/show.html.erb
<%= simple_form_for Ticket.new do |f| %>
<%= f.text_area :description %>
<%= f.submit "Add" %>
<% end %>
controller/tickets_controller.rb
def create
#ticket = Ticket.new(ticket_params)
#ticket.user = current_user
#ticket.save
end
def ticket_params
params.require(:ticket).permit(:user_id, :description)
end

Rails Nested Form Not Saving Nested Attributes

I have the following User controller:
class UsersController < ApplicationController
def index
#users = User.all
end
def new
#user = User.new
end
def create
#user = User.new(user_params)
#customer = Customer.new
if #user.save
flash.notice = "User '#{#user.email}' was succefully created."
redirect_to user_path(#user)
else
render 'new'
end
end
def show
#user = User.find(params[:id])
end
private
def user_params
params.require(:user).permit(:email, :password, :password_confirmation, customer_attributes: [:id, :company])
end
end
And I have the following User model:
class User < ActiveRecord::Base
has_one :customer
accepts_nested_attributes_for :customer, :allow_destroy => true
end
And the following Customer model:
class Customer < ActiveRecord::Base
belongs_to :user
end
Finally, here is the form:
<%= form_for [#user] do |f| %>
<% if #user.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(#user.errors.count, "error") %> prohibited this user from being saved:</h2>
<ul>
<% #user.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :email %><br>
<%= f.text_field :email %>
</div>
<div class="field">
<%= f.label :password %><br>
<%= f.password_field :password %>
</div>
<div class="field">
<%= f.label :password_confirmation %><br>
<%= f.password_field :password_confirmation %>
</div>
<%= f.fields_for :customers do |company| %>
<div class="field">
<%= company.label :company %><br>
<%= company.text_field :company %>
</div>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
When I submit the form, I see: `Unpermitted parameters: customers' in the log but it appears that I m indeed permitting it.
Also, I want to show the company name for each user in the show and index views. I'm not sure how to do that.
I remember using the build method in the past to get something similar to work but I can't seem to figure it out this time.
Further to #Mandeep's answer, let me give you some further information:
You need to "build" your associated objects for your form
You need to process this as per the association your model has
You need to save the attributes as per said association
The way to do this is relatively simple (outlined by Mandeep). However, the reason why might be a little less obvious:
Build
First, you need to build your associative association. This is vitally important, primarily because Rails (by virtue of being built on Ruby), is an object orientated framework.
Object orientation, without getting into too much detail, means that everything you do with Rails is going to be based around objects. In the case of our beloved Rails, it means that every Model is an object.
By virtue of this fact, the nested model paradigm has to be built in Rails whenever you want to create such a form. To do this, you need to use the build methods - which tell ActiveRecord (Rails' object relational mapper) that you have another associated model / object which you want to populate:
#app/controllers/users_controller.rb
class UsersController < ApplicationController
def new
#user = User.new #-> initializes "User" object
#user.build_customer #-> "builds" the associated object
end
end
This gives Rails a set of associated data which it can populate with your form (considering you call the correct methods)
--
Association
Second, you need to consider the association you have. This is important as singular & multiple associations are handled differently in the "build" process.
You're using a has_one relationship, which means you need to use singular association names (although you can call the associations whatever you want):
If you used a has_many association, you'd need to use the plural association methods:
This explains the need to use the build_customer method; but also should give you the presidence to use the singular association name for all the methods you need to get this working, namely fields_for and params:
#app/views/users/new.html.erb
<%= form_for #user do |f| %>
...
<%= f.fields_for :customer do |c| %>
...
<% end %>
<%= f.submit %>
<% end %>
#app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
#user = User.new user_params
#user.save
end
private
def user_params
params.permit(:user).permit(:user, :params, customer_attributes: [:x. :y])
end
end
--
Save
The above controller code will save the attributes you require.
You must understand that passing nested attributes means that the model you're sending the associative data to needs to be subordinated to your "main" model. This happens with the ActiveRecord associations in your models, as discussed initially.
Hopefully this gives you some more clarity
Change your code to this:
def new
#user = User.new
#user.build_customer
end
your form:
<%= form_for #user do |f| %>
// user fields
<%= f.fields_for :customer do |customer| %>
// customer fields
<% end %>
<% end %>
Also there is not need of #customer = Customer.new in your create method.

Multi-model nested form, can't add users to current account

I've searched everywhere for a solution but haven't come up with any.
The part that works: My app allows customers to create an account using a nested form. The data collected creates records in four models - accounts, users, accounts_users (because a user can be associated with many accounts), and profile (to store the user's fname, lname, phone, etc).
That part that doesn't work: Once logged in, I want the users to be able to add more users to their account using the form below. I don't receive any errors upon submit but I am brought back to the same form with no additional records created. Any help would be awesome!
Here is the nested form...
<%= form_for #user, :validate => true do |f| %>
<fieldset>
<%= f.fields_for :profile do |p| %>
<div class="field">
<%= p.label :first_name %>
<%= p.text_field :first_name %>
</div>
<div class="field">
<%= p.label :last_name %>
<%= p.text_field :last_name %>
</div>
<div class="field">
<%= p.label :phone %>
<%= p.text_field :phone %>
</div>
<% end %>
<div class="field">
<%= f.label :email %>
<%= f.text_field :email %>
</div>
<div class="actions">
<%= f.submit 'Create New User', :class => "btn btn-large btn-success" %>
<%= cancel %>
</div>
</fieldset>
The ApplicationController scopes everything to the current_account like so:
def current_account
#current_account ||= Account.find_by_subdomain(request.subdomain) if request.subdomain
end
The UsersController
def new
#user = User.new
#user.build_profile()
#current_account.accounts_users.build() #Edit2: This line was removed
respond_to do |format|
format.html # new.html.erb
format.json { render json: #user }
end
def create
#user = User.new(params[:user])
#user.accounts_users.build(:account_id => current_account.id) #Edit2: This line was added
if #user.save
# Send Email and show 'success' message
flash[:success] = 'An email has been sent to the user'
else
# Render form again
render 'new'
end
end
Models look like this:
class Account < ActiveRecord::Base
attr_accessible :name, :subdomain, :users_attributes
has_many :accounts_users
has_many :users, :through => :accounts_users
accepts_nested_attributes_for :users
end
class User < ActiveRecord::Base
attr_accessible :email, :password, :password_confirmation, :profile_attributes
has_many :accounts_users
has_many :accounts, :through => :accounts_users
has_one :profile
accepts_nested_attributes_for :profile
end
class AccountsUser < ActiveRecord::Base
belongs_to :account
belongs_to :user
end
class Profile < ActiveRecord::Base
belongs_to :user
attr_accessible :first_name, :last_name, :phone
end
Edit2: It turns out that I had required a password + password_comfirmation validation in the User model which prevented me from adding another user without these fields. I commented out these validations plus removed the line: current_account.accounts_users.build() in the 'new' action and added the line: #user.accounts_users.build(:account_id => current_account.id) in the 'create' action.
"I want the users to be able to add more users to their account using the form below." I assume you mean profiles (since your nested form is on profiles)?
If that's the case, I think your UsersController's create action isn't associating the profiles with users by using new.
Try this...
def new
#user = User.build
#profile = #user.profiles.build #build adds the profile to user's associated collection of profiles, but new doesn't
...
end
def create
#user = User.build(params[:user])
if #user.save
....
end
end
If you want the user to be associated with account, then you need to put the new and create actions in the AccountsController and do something similar to nest association of the users and profiles records.
Btw, the reason that it went back to new is because you render new at the end of the create, in case that's also part of the question. Hope that helps!

Resources