Custom fields in form_for - ruby-on-rails

Trying to create a profile form in which a user can add information about their hobbies to their user profile. But I get the following error when trying to do so.
NoMethodError in Users#edit_profile
undefined method `hobbies' for #<User:0x007ff1a8a1f198>
Does anyone know what the problem could be, or a potential solution? I'm new to rails but I was under the impression that 'text_field' was a safe bet to make any custom input work. Would installing the strong parameters gem help this out at all?
edit_profile.html.erb
<h2>Tell us about yourself</h2>
<%= form_for #user do |f| %>
<%= f.label :first_name %><br />
<%= f.text_field :first_name, autofocus: true %>
<%= f.label :last_name %><br />
<%= f.text_field :last_name %>
<%= f.label :hobbies %><br />
<%= f.text_field :hobbies %>
<div><%= f.submit "Update" %></div>
<% end %>
user_controller.rb
class UsersController < ApplicationController
before_filter :authenticate_user!
def index
#users = User.all
end
def show
#user = User.find(params[:id])
end
def new
#user = User.new
end
def create
end
def edit
end
def update
#user = User.find(params[:id])
#user.update!(user_params)
redirect_to #user
end
def destroy
end
def edit_profile
#user = User.find(params[:id])
end
def user_params
params.require(:user).permit(:first_name, :last_name, :email, :password, :password_confirmation, :hobbies)
end
end
user.rb
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
attr_accessible :first_name, :last_name, :email, :password, :password_confirmation, :remember_me, :hobbies
#validates :first_name, presence: true, length: { maximum: 50 }
#validates :last_name, presence: true, length: { maximum: 50 }
end

You don't mention it, but I will assume you're running Rails 4.x.
Rails 4.x introduced strong parameters, so in your controller you need to add a private method to set the allowed parameters and remove the attr_accessible from your model.
So in you case it will be:
def user_params
params.require(:first_name, :last_name).permit(:email, :password, :password_confirmation, :remember_me, :hobbies)
end
If you still have trouble to understand the concept, or came from a previous Rails version, take a look at this blog post.

Does anyone know what the problem could be, or a potential solution?
Sure - the problem is you don't have a hobbies attribute in your User model :)
Since you're new, I'll explain a bit about Rails after I answer the question. However, let me explain the value of creating the right attributes:
#app/models/user.rb
class User < ActiveRecord::Base
attr_accessor :hobbies
end
This is what you'll need to create a single attribute for your User model. The attr_accessor directive is a Ruby method which sets a getter and setter in your User model
This will give Rails the ability to populate the hobbies attribute of your Model. However, the data will not be persistent, as it will only be set on a per-instance basis in the model, meaning it will be lost when you refresh the page etc
Using the code above should get your form working, regardless of whether you're using Rails 3 or 4.
Models
Rails is famously an MVC framework - which means it has 3 core components - models (builds data from the database), controllers (configure data for the view) & views (displays the data from the controller & model).
When you load a Rails application, you're sending a request, which will be routes to a particular controller action. This will then call data from your database, allowing you to manipulate it in your view.
Models, therefore have to be populated from your database. They do this by taking the various attributes you have in your datatables, and creating a series of getters and setters for them. These give you the ability to access the data within the attributes, or set new ones.
Your error occurs because you don't have the relevant attribute set in your datatable. You'll need to create a migration to add the hobbies attribute to your User model:
> $ rails g migration AddHobbiesToUser hobbies:string
> $ rake db:migrate
The migration should create something like:
class AddPartNumberToProducts < ActiveRecord::Migration
def change
add_column :users, :hobbies, :string
end
end

Related

Two models with one controller using devise, problems with validations and error messages

I have two models in my app, one 'user' model with devise, one 'employee' model made by scaffolding.
I need a way to have the employee table populated as soon as a new user registers, both tables share some params, some are exclusive. The employee belongs to the user model, each user has one employee.
The view I use is the devise user registration form with nested attributes to allow for the employee params. Creation is handled by the user controller.
Problems that occur:
undefined method 'email' for empty password field until the first user is created
cannot make the error message for the exclusive 'name' parameter of the employee model show up in the same place as the error messages for the user model
employee-model:
class Employee < ApplicationRecord
audited
validates :email, presence: true, uniqueness: true
validates :name, presence: true
belongs_to :user, inverse_of: :employee, dependent: :destroy, optional: true
user model
class User < ApplicationRecord
audited
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable
has_one :employee, inverse_of: :user
accepts_nested_attributes_for :employee
validates_presence_of :password
validates :password, format: { with: /(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[["§#?!`´~#'._,;<>|°()"{}="#$%^&*+-]]).{8,}/}, if: -> {password.present?}
validates_presence_of :password_confirmation, if: -> {password.present? and password =~ /(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[["§#?!`´~#'._,;<>|°()"{}="#$%^&*+-]]).{8,}/}
validates_confirmation_of :password, if: -> {password.present? and password_confirmation.present? and password =~ /(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[["§#?!`´~#'._,;<>|°()"{}="#$%^&*+-]]).{8,}/}
validates_presence_of :email
validates :email, format: { with: /\A([^#[0-9]\s]+\.)+([^#[0-9]\s]+)#((thisapp+\.)+com)\z/i}, uniqueness: true, if: -> {email.present?}
end
user controller
class Users::RegistrationsController < Devise::RegistrationsController
# before_action :configure_sign_up_params, only: [:create]
# before_action :configure_account_update_params, only: [:update]
# GET /resource/sign_up
def new
super
end
def new_employee
#employee = Employee.new
end
# POST /resource
def create
#employee = Employee.new(employee_params)
super
#employee.email = User.last.email
#employee.user_id = User.last.id
#employee.created_by_id = User.last.id
#employee.save
end
user registration view
.card style='text-align:center;'
.card-body
h2.card-title style='text-align:center;' = t('.sign_up')
= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f|
= render "users/shared/error_messages", resource: resource
br
.field
= f.label :email, t('users.email_field')
br
= f.text_field :email, autofocus: true, autocomplete: "email"
.field
= f.label :password, t('users.password_field')
/- if #minimum_password_length
em
= t('devise.shared.minimum_password_length', count: #minimum_password_length)
br
= f.password_field :password, autocomplete: "new-password"
.field
= f.label :password_confirmation, t('users.password_confirmation_field')
br
= f.password_field :password_confirmation, autocomplete: "new-password"
= fields_for :employee do |e|
= e.label :name
= e.text_field :name
So, when I use #employee.email = User.last.email in the controller I get a 'unknown method 'email' error when not filling out the password field, unless I have a preexisting user, then I get my custom error messages for not filling out the email. I guess it is because I am looking for a last user who does not exist at this point. Could seed a user, but that seems hackish. Tried using #employee.email = User.last(params[:email]) which leads to the email being saved as some hash value, but at least I get my error messages. Is there a way to convert that hash to the real email address again?
The other issue is the validation of the name field. Validation is asked for in the employee model, and user model accepts nested attributes, but that does not seem to be enough.
I did
if params[:employee][:name].blank?
flash[:notice] = t('.noname')
which works insofar as that the form cannot be submitted without some value in the name field, but messes up my error messages. Shows a flash message where all other errors (no email/password/pw confirmation) are handled by the devise's shared error messages as non-flash messages:
- resource.errors.full_messages.each do |message|
li
= message
So having the blank name as a flash message would look inconsistent, and the spot for the flash message is already reserved.
Flash message is on top, 'following errors prevent..', actual error messages are below 'Registrieren', and that is also where the error message for blank name would need to be.
Any ideas on how to approach this or maybe a better solution than handling this stuff in the user controller?
class Users::RegistrationsController < Devise::RegistrationsController
# GET /resource/sign_up
def new
super
end
def new_employee
#employee = Employee.new
end
def sign_up_params
params.require(:user).permit(:email, :password, :password_confirmation, employee_attributes: %i[name])
end
This signup param will be used while creating a user from devise registration controller. Since we used nested attributes, passing the arguments along with the parent object will handle employee creation(https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html)
For assigning email for employee
The one way is to merge the email from the user params.
def sign_up_params
sign_up_param = params.require(:user).permit(:email, :password, :password_confirmation,employee_attributes: %i[name])
sign_up_param[:employee_attributes].merge!(email: params[:user][:email])
sign_up_param
end
Or maybe you could assign the email of the employee from the user before the validation.
class Employee < ApplicationRecord
audited
validates :email, presence: true, uniqueness: true
validates :name, presence: true
belongs_to :user, inverse_of: :employee, dependent: :destroy, optional: true
# Callbacks
before_validation :set_email
# Methods
# Set email of the employee from the user.
def set_email
self.email = self.user.email
end

Rails - Devise - Add profile information to separate table

I am using Devise to build a registration/authentication system into my application.
Having looked at quite a few resources for adding information to the devise model (e.g. username, biography, avatar URL, et cetera..) [resources include Jaco Pretorius' website, this (badly formed) SO question, and this SO question.
That's all fine and well -- it works. But my problem is that it's saving to the User model, which, according to database normalizations (also referencing this SO question), it should in fact be saving to a sub-model of User which is connected via has_one and belongs_to.
Thus far, I have created a User model via Devise. I have also created a UserProfile model via the rails generate script.
user.rb (for reference)
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable, :confirmable, :recoverable, :rememberable, :trackable, :validatable
has_one :user_profile, dependent: :destroy
end
user_profile.rb
class UserProfile < ActiveRecord::Base
belongs_to :user
end
timestamp_create_user_profiles.rb
class CreateUserProfiles < ActiveRecord::Migration
def change
create_table :user_profiles do |t|
t.string :username, null: false
t.string :biography, default: ""
t.references :user, index: true, foreign_key: true
t.timestamps null: false
end
add_index :user_profiles, [:user_id, :username]
end
end
My question, now, is, how does one collect the information for both of these models and ensure, via the devise registration form, that it all ends up in the right places?
I've seen resources about creating state machines (AASM, and the answer to this SO question. I've also seen information about creating a wizard with WICKED, and an article on the same topic.
These all seem too complicated for my use-case. Is there some way to simply separate the inputs with devise and make sure the end up in the right place?
I think, instead of simply commenting on an answer that led me to the final answer, I'll archive the answer here in case someone in the future is trying to also find this answer:
I will be assuming that you have some sort of setup as I do above.
First step is you need to modify your User controller to accept_nested_attributes_for the profile reference as well as add a utility method to the model so when requested in code, the application can either retrieve the built profile model or build one.
The user model ends up looking like so:
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable, :confirmable, :recoverable, :rememberable, :trackable, :validatable
has_one :user_profile, dependent: :destroy
accepts_nested_attributes_for :user_profile
def user_profile
super || build_user_profile
end
end
Secondly, you will need to modify your sign up/account_update form to be able to pass the attributes for this secondary model into the controller and eventually to be able to build the profile for the parent model.
You can do this by using f.fields_for.
Add something like this to your form:
<%= f.fields_for :user_profile do |user_profile_form| %>
<%= user_profile_form.text_field :attribute %>
<% end %>
An example of this in my specific case is:
<%= f.fields_for :user_profile do |user_profile_form| %>
<div class="form-group">
<%= user_profile_form.text_field :username, class: "form-control", placeholder: "Username" %>
</div>
<% end %>
Finally, you will need to tell Devise that it should accept this new hash of arguments and pass it to the model.
If you have created your own RegistrationsController and extended Devise's, it should look similar to this:
class RegistrationsController < Devise::RegistrationsController
private
def sign_up_params
params.require(:user).permit(:email, :password, user_profile_attributes: :username)
end
end
(Of course, make the proper changes for your specific use-case.)
If you have simply added the Devise sanitization methods to your application controller, it should look similar to this:
class ApplicationController < ActionController::Base
before_filter :configure_permitted_parameters, if: :devise_controller?
protected
def configure_permitted_parameters
devise_parameter_sanitizer.for(:sign_up) {|u|
u.permit(:email, :password, user_profile_attributes: :username)}
end
end
(Again, make the proper changes for your specific use-case.)
A small note on user_profile_attributes: :username:
Note this is a hash, of course. If you have more than one attribute you are passing in, say, as an account_update (hint hint), you will need to pass them like so user_profile_attributes: [:attribute_1, :attribute_2, :attribute_3].
Please check out the RailsCasts.com web-site.
There are a couple of interesting railscasts about nested model forms:
http://railscasts.com/episodes/196-nested-model-form-part-1
http://railscasts.com/episodes/197-nested-model-form-part-2
http://railscasts.com/episodes/196-nested-model-form-revised
Also check out accepts_nested_attributes_for
Or check out this question:
Profile model for Devise users?
Also note that for Devise 4.2 the '.for' method for the devise_parameter_sanitizer is deprecated in favor of '.permit'
From the documentation:
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_in) do |user_params|
user_params.permit(:username, :email)
end
end

Using a model in the controller for a different model?

I have a User model (generated by devise) and a Submission model in a Rails project. I've added a field called 'full_name' to the user model. Also, there is a field for 'user_id' in the submission model.
I want to show the the full_name of the user associated with the submission on the submission show page. Right now it shows the user_id from the submission model just fine.
The User model I have this:
class User < ActiveRecord::Base
has_many :submissions
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable, :confirmable
attr_accessible :email, :password, :password_confirmation, :remember_me, :full_name, :role_id
validates_presence_of :full_name
end
This is the model for submissions:
class Submission < ActiveRecord::Base
belongs_to :user
attr_accessible :description, :title, :user_id
end
This is in the controller:
def show
#submission = Submission.find(params[:id])
#user = User.find(params[#submission.user_id])
respond_to do |format|
format.html # show.html.erb
format.json { render json: #submission }
end
end
What I get when I try to use this line in the view:
<%= #user.full_name %>
I get this error:
Couldn't find User without an ID
I've tried several variations and can't quite figure out what should be in the place of:
#user = User.find(params[#submission.user_id])
Assuming that you're populating the data correctly, you only need the following once you have looked up the submission.
#submission.user
If you think that the data is OK, then try the following in the Rails console.
> Submission.first.user
What do you see there?
The specific error that you are seeing is because this:
params[#submission.user_id]
is unlikely to ever have anything in it. If the user ID is "1" (for example) then you're asking for the value in the params hash that corresponds to the key "1".

devise and multiple "user" models

I'm using rails 3.2 and devise 2.0 and I'm quite new to Rails.
Requirements
I'd like to achieve the following:
have 2 or more "user" models, eg. Member, Customer, Admin
all models share some required fields (eg. email and password)
each model may have some unique fields (eg. company for Customer only)
some fields may be shared but not have the same validation (eg. name is required for Customer but optional for Member)
all fields must be filled during the registration process, so the forms are different
the login form should be unique
Possible solutions
I googled and searched StackOverflow for quite a long time, but nothing seems right to me (I'm a Java guy, sorry :) and now I'm quite confused. Two solutions came up:
Single devise user
That's the most frequent answer. Just create the default devise User and create relations between Member-->User and Customer-->User.
My concern here is how can I achieve a customized registration process for each model? I tried different things but all ended as a mess!
Multiple devise users
This solves the custom registration process, and seems right to me, but the unique login form is a blocker. I found an answer on SO (Devise - login from two model) which suggests to override Devise::Models::Authenticatable.find_for_authentication(conditions).
That seems complicated (?) and since I'm new to rails, I'd like to know if that could work?
Thanks for your advice!
Welcome aboard Java guy =), I hope you'll enjoy the Rails world.
Simply, to solve your issue you have 2 solutions:
For each user create a table in the database and corresponding model.
Create a single table in the database and for each user type create a model. This is called single table inheritance (STI).
Which one to choose?
It depends on the common attributes of the roles. If they are almost common (for example all have a name, email, mobile, ...) and a few attributes are different, I highly recommend the STI solution.
How to do the STI?
1. Simply create the the devise user model and table using the command rails generate devise User
2. Add a column named type with string datatype to the user table in the database using a migration.
3. For each user type create a model (for example rails g model admin)
4. Make the Admin class inherits from user model
class Admin < User
end
That's it you are done =) ... Yupeee
To create an admin run the command Admin.create(...) where the dots is the admin attributes for example the email, name, ...
I think this question could help you too
I'm in similar shoes as you, after trying all sorts of approaches I went with a single User model, which would belong to polymorphic roles. This seems like the simplest way to achieve single-login.
The User model would contain the information specific to log-in only.
The Role model would store fields specific to each role, as well as other associations specific to the role.
New registrations would be customized for each user type (roles) via individual controllers, and then building nested attributes for the User.
class User < ActiveRecord::Base
#... devise code ...
belongs_to :role, :polymorphic => true
end
class Member < ActiveRecord::Base
attr_accessible :name, :tel, :city #etc etc....
attr_accessible :user_attributes #this is needed for nested attributes assignment
#model specific associations like
has_many :resumes
has_one :user, :as => :role, dependent: :destroy
accepts_nested_attributes_for :user
end
Routes -- just regular stuff for the Member model.
resources :members
#maybe make a new path for New signups, but for now its new_member_path
Controller -- you have to build_user for nested attributes
#controllers/members_controller.rb
def new
#member = Member.new
#member.build_user
end
def create
#... standard controller stuff
end
views/members/new.html.erb
<h2>Sign up for new members!</h2>
<%= simple_form_for #member do |f| %>
# user fields
<%= f.fields_for :user do |u| %>
<%= u.input :email, :required => true, :autofocus => true %>
<%= u.input :password, :required => true %>
<%= u.input :password_confirmation, :required => true %>
<% end %>
# member fields
<%= f.input :name %>
<%= f.input :tel %>
<%= f.input :city %>
<%= f.button :submit, "Sign up" %>
<% end %>
I would like to point out that there is NO NEED to reach for nested_form gem; since the requirement is that User can only belong_to one type of Role.
I found a way to go and I'm quite happy with it so far. I'll describe it here for others.
I went with the single "user" class. My problem was to achieve a customized registration process for each pseudo model.
model/user.rb:
class User < ActiveRecord::Base
devise :confirmable,
:database_authenticatable,
:lockable,
:recoverable,
:registerable,
:rememberable,
:timeoutable,
:trackable,
:validatable
# Setup accessible (or protected) attributes for your model
attr_accessible :email, :password, :password_confirmation, :remember_me, :role
as_enum :role, [:administrator, :client, :member]
validates_as_enum :role
## Rails 4+ for the above two lines
# enum role: [:administrator, :client, :member]
end
Then I adapted http://railscasts.com/episodes/217-multistep-forms and http://pastie.org/1084054 to have two registration paths with an overridden controller:
config/routes.rb:
get 'users/sign_up' => 'users/registrations#new', :as => 'new_user_registration'
get 'clients/sign_up' => 'users/registrations#new_client', :as => 'new_client_registration'
post 'clients/sign_up' => 'users/registrations#create', :as => 'client_registration'
get 'members/sign_up' => 'users/registrations#new_member', :as => 'new_member_registration'
post 'members/sign_up' => 'users/registrations#create', :as => 'member_registration'
controllers/users/registrations_controller.rb:
I created a wizard class which knows the fields to validate at each step
class Users::RegistrationsController < Devise::RegistrationsController
# GET /resource/sign_up
def new
session[:user] ||= { }
#user = build_resource(session[:user])
#wizard = ClientRegistrationWizard.new(current_step)
respond_with #user
end
# GET /clients/sign_up
def new_client
session[:user] ||= { }
session[:user]['role'] = :client
#user = build_resource(session[:user])
#wizard = ClientRegistrationWizard.new(current_step)
render 'new_client'
end
# GET /members/sign_up
def new_member
# same
end
# POST /clients/sign_up
# POST /members/sign_up
def create
session[:user].deep_merge!(params[:user]) if params[:user]
#user = build_resource(session[:user])
#wizard = ClientRegistrationWizard.new(current_step)
if params[:previous_button]
#wizard.previous
elsif #user.valid?(#wizard)
if #wizard.last_step?
#user.save if #user.valid?
else
#wizard.next
end
end
session[:registration_current_step] = #wizard.current_step
if #user.new_record?
clean_up_passwords #user
render 'new_client'
else
#session[:registration_current_step] = nil
session[:user_params] = nil
if #user.active_for_authentication?
set_flash_message :notice, :signed_up if is_navigational_format?
sign_in(:user, #user)
respond_with #user, :location => after_sign_up_path_for(#user)
else
set_flash_message :notice, :"signed_up_but_#{#user.inactive_message}" if is_navigational_format?
expire_session_data_after_sign_in!
respond_with #user, :location => after_inactive_sign_up_path_for(#user)
end
end
end
private
def current_step
if params[:wizard] && params[:wizard][:current_step]
return params[:wizard][:current_step]
end
return session[:registration_current_step]
end
end
and my views are:
new.rb
new_client.rb including a partial according to the wizard step:
_new_client_1.rb
_new_client_2.rb
new_member.rb including a partial according to the wizard step:
_new_member_1.rb
_new_member_2.rb
So what's wrong? Just run rails g devise:views [model_name], customize each registration forms and in config/initializer/devise.rb just put config.scoped_views = true.

Rails: Combining last name and first name in Rails model

FINAL SOLUTION:
I have a Rails 3 app that uses Devise to handle authentication. In the signup form i have the following fields:
<p><%= f.label :first_name %><br />
<%= f.text_field :first_name %></p>
<p><%= f.label :last_name %><br />
<%= f.text_field :last_name %></p>
I need to capitalize first and last names and combine them in the User model in a database field called 'login' (e.g. Lastname, Firstname). Here is the complete user model:
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
attr_accessible :first_name, :last_name, :email, :password, :password_confirmation, :remember_me, :login
validates :first_name, :last_name, :email, :password, :password_confirmation, :presence => true
before_create :create_login
def create_login
self.login = "#{last_name.capitalize}, #{first_name.capitalize}"
end
end
Thanks.
I genuinely, honestly, and truly applaud your use of the power of Ruby, but since this is such a straightforward and static concatenation of two strings, I'd go with:
def create_login
login = "#{last_name.capitalize}, #{first_name.capitalize}"
end
As for the nil:NilClass issue, are you adding first_name and last_name columns to your users table in your associated migration?
before_create :create_login
validates :first_name, :presence => true
validates :last_name, :presence => true
def create_login
login = [last_name, first_name].map(&:capitalize).join(", ")
end
Short explanation
I think it is good to get first_name and last_name on registration: so we will ad validation to it. Also it is good idea to validate length and match it with some regexp.
Then, as far as login is creates only once, we will add before_create callback, which will be executed only when object is creating (not updating). before_create callback will be run only if validation is passed, so if first_name or last_name is blank - validation won't be passed and callback won't be executed till first_name and last_name is filled.
UPD
Ok, as far as you get your error:
def create_login
login = [last_name, first_name].compact.map(&:capitalize).join(", ")
end

Resources