In the Sign up form of my Rails 6 application an Account with a nested User can be created.
class AccountsController < ApplicationController
def new
#account = Account.new
#account.users.build(
:owner => true,
:language => "FR"
)
end
def create
#account = Account.new(account_params)
if #account.save
redirect_to root_url, :notice => "Account created."
else
render :new
end
end
private
def account_params
safe_attributes = [
:name,
:users_attributes => [:first_name, :last_name, :email, :password, :owner, :language]
]
params.require(:account).permit(*safe_attributes)
end
end
What is the best way to define default values on the new user here?
Right now, I use hidden_fields for the default values in my sign up form thus making them publicly available. This is of course not what I want because it's very insecure.
Is there a better way to deal with this?
I know that there's Rails' with_defaults method but I couldn't get it to work on nested items so far.
try with:
account_params[:users_attributes] = account_params[:users_attributes].with_defaults({ first_name: 'John', last_name: 'Smith'})
in first line of create action
Related
I'm learning Rails 6 implementing a blog and I want to let users comment on the post even if they are not users in the platform.
I'm not sure how to do this, because currently I'm creating the comment like this
def create
#comment = Comment.new(
guest_user: params[:guest_user],
post_id: params[:post_id],
user_id: current_user.id,
)
...
end
And Comments belong_to User.
I have created a GuestUser model to use it as a placeholder but I'm not sure what to do with the id
class GuestUser < User
attr_accessor :name, :first_name, :last_name, :email
end
What do you think is the best approach for this?
There's a few different ways I believe you could do it, here's one possibly way. Let's say your comment form looked something like this (lots of assumptions given the below, but it should be not too bad to modify this to your needs):
= simple_form_for :comment do |f|
= f.input :comment_body # Not sure what your comment model looks like here, just a guess
- unless current_user.present? # Don't show the GuestUser fields unless no current_user, will also validate this on the backend
= f.input :first_name
= f.input :last_name
= f.input :email
# ...
In your controller, you could then do something like:
def create
user = current_user || GuestUser.create(guest_params) # Take the current logged in user, or generate a new GuestUser
redirect_to(#bad_user_path, notice: 'User invalid') unless user.valid? # Mostly a check on GuestUser here
#comment = user.comments.new(comment_params)
if #comment.save!
redirect_to (#wherever), notice: 'Saved comment!'
else
redirect_to (#wherever), notice: 'Comment invalid'
end
end
private
def comment_params
params.require(:comment).permit(:comment_body)
end
def guest_params
params.require(:comment).permit(:first_name, :last_name, :email)
end
end
Again there'd be a few assumptions here, like that your Comment model held some attr_accessors for :first_name, :last_name, :email, just so you could use f.input in the simple form vs f.text_field, but that would be one way.
While implementing what I thought was a simple signup/login system for a Ruby on Rails app, results haven't matched what tutorials have shown.
I'm trying to use bcrypt for authentication and PostgreSQL for the database.
I continually get 'ActionController::ParameterMissing (param is missing or the value is empty: name): ', even though it will show name as being input. '"users"=>{"name"=>"asdf", "password"=>"Qq!1asdfasdf", "password_confirmation"=>"Qq!1asdfasdf"}, "commit"=>"Submit"} (0.1ms)
output from the console when attempting to sign in
users controller
class UsersController < ApplicationController
def new
end
def create
user = User.new(
name: params[:name],
password: params[:password],
password_confirmation: params[:password_confirmation])
if user.save
session[:user_id] = user.id
redirect_to '/'
else
redirect_to '/signup'
end
end
private
end
The table
class UsersController < ApplicationController
def new
end
def create
user = User.new(
name: params[:name],
password: params[:password],
password_confirmation: params[:password_confirmation])
if user.save
session[:user_id] = user.id
redirect_to '/'
else
redirect_to '/signup'
end
end
private
end
and the signup form
class UsersController < ApplicationController
def new
end
def create
user = User.new(
name: params[:name],
password: params[:password],
password_confirmation: params[:password_confirmation])
if user.save
session[:user_id] = user.id
redirect_to '/'
else
redirect_to '/signup'
end
end
private
end
the user model
class User < ActiveRecord::Base
PASSWORD_FORMAT = /\A
(?=.{10,}) # Must contain 10 or more characters
(?=.*\d) # Must contain a digit
(?=.*[a-z]) # Must contain a lower case character
(?=.*[A-Z]) # Must contain an upper case character
(?=.*[[:^alnum:]]) # Must contain a symbol
/x
#formatting for password
USERNAME_FORMAT = /\A[a-z0-9A-Z\-_]{2,15}\z/ #Can contain lowercase and upercase letters, numbers, - and _, must be between 2 and 15 length
#username formatting
validates :name,
:presence => true,
:uniqueness => true,
:format => USERNAME_FORMAT
validates :password,
:presence => true,
:format => PASSWORD_FORMAT,
:confirmation => true,
:on => create
has_secure_password
end
I've tried troubleshooting, all similar questions haven't yielded an answer or fix.
EDIT: More clarity on issue
You need to use rails Strong Parameter like the following
class UsersController < ApplicationController
def new
end
def create
user = User.new(user_params)
if user.save
session[:user_id] = user.id
redirect_to root_path
else
redirect_to new_user_path
end
end
private
def user_params
params.require(:user).permit(:name, :password, :password_confirmation)
end
end
I think it's a problem of passing data between your form and your controller.
In your logs your parameters for user looks like: "users"=>{"name"=> ...} but it should be "user"
To pass data between your controller and your view, you need to use instance variable such as #user to make the new instance of User available in the view. (source)
In that way your controller should be:
def new
#user = User.new
end
def create
#user = User.new(user_params)
if #user.save
session[:user_id] = #user.id
redirect_to '/'
else
redirect_to '/signup'
end
end
private
def user_params
params.require(:user).permit(:name, :password, :password_confirmation)
end
(with strong parameters like #fool-dev explained)
Then in your view, use this #user to pass the parameters to the controller:
<%= form_for #user do |f| %>
//...the form
<% end %>
I am making a basic account setup and to try to learn how the database stuff works. I have been running into this error constantly, and I have no idea how to make it disappear. I have my stuff named as U, so the URL will be easier to type a username like Reddit has it example.com/u/username
The Error is uninitialized constant UController::User_param
It highlights this code: #user = U.new(User_param)
Controller:
class UController < ApplicationController
def index
#users = U.all
end
def show
end
def create
#user = U.new(User_param)
if #user.save
redirect_to :action => 'list'
else
#user = U.all
render :action => 'new'
end
end
def User_param
params.require(:Us).permit(:id, :email, :password, :created_at, :updated_at)
end
def new
#user = U.new
end
def edit
end
end
Routes:
resources :u
U Model:
class U < ActiveRecord::Base
end
In Rails you don't capitalize methods, only constants and classes. change User_param to user_params along with the method and that should work. I made params plural since it is clearer and easier to understand
Also, change the user_param method to this:
def user_params
params.require(:u).permit(:id, :email, :password, :created_at, :updated_at)
end
The .require(:u) doesn't need to be plural as you had it.
When an employee is created, he is given a title. If the title is unique, the record saves normally. If the title is not unique, I want to find the existing title, and use that instead. I can't figure out how to do this in the create action.
employer.rb
class Employee < ActiveRecord::Base
belongs_to :title, :class_name => :EmployeeTitle, :foreign_key => "employee_title_id"
accepts_nested_attributes_for :title
end
employer_title.rb
class EmployerTitle < ActiveRecord::Base
has_many :employees
validates :name, presence: true, length: { maximum: 50 },
uniqueness: { case_sensitive: true }
end
new.html.erb
<%= f.simple_fields_for :title do |title| %>
<%= title.input :name, label: "Title" %>
<% end %>
employees_controller.rb
def create
if EmployeeTitle.exists?(name: employee_params[:title_attributes][:name])
# find title and use it?
else
#employee = current_user.employee.build(employee_params)
end
if #employee.save
flash[:success] = "Employee #{#employee.title.name} created."
redirect_to #employee
else
render 'new'
end
end
Edit: Using first_or_create
def create
EmployeeTitle.where(name: employee_params[:title_attributes][:name]).first_or_create do |title|
#employee = current_user.employees.build(employee_params, :title => title)
end
if #employee.save
flash[:success] = "Employee #{#employee.title.name} created."
redirect_to #employee
else
render 'new'
end
end
This makes the #employee go out of scope. Error: Undefined method `save' for nil:NilClass.
In addition, if I do this, won't the title be created regardless of whether the rest of the employee data is valid?
Using private method
employee.rb
private
def title_attributes=(attributes)
self.title = EmployeeTitle.find_or_create_by_name(name: attributes[:name])
end
The value is not being set. I get a "cannot be blank" validation error. The parameters include
employee: !ruby/hash:ActionController::Parameters
title: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
name: Consultant
The !ruby/hash:ActiveSupport::HashWithIndifferentAccess was not there before.
employee_params
private
def employee_params
params.require(:employee).permit(
title_attributes: [:id, :name],
)
end
What you need to do is to change this:
def create
if EmployeeTitle.exists?(name: employee_params[:title_attributes][:name])
# find title and use it?
else
#employee = current_user.employee.build(employee_params)
end
if #employee.save
flash[:success] = "Employee #{#employee.title.name} created."
redirect_to #employee
else
render 'new'
end
end
with this:
def create
#employee = current_user.employee.build(employee_params)
if #employee.save
flash[:success] = "Employee #{#employee.title.name} created."
redirect_to #employee
else
render 'new'
end
end
Now, override title_attributes method by putting this code in your app/models/employee.rb file:
def title_attributes=(attributes)
self.title = EmployeeTitle.find_or_create_by_name(attributes[:name])
end
Now, every time you'll create an employee whose name already exists with the particular name, it'll be used by default for associating it as title. Let the controller be skinny as it used to be.
Read more about find_or_create_by method here.
However, your question's title says: Rails 4, but you have tagged ruby-on-rails-3.2. If you're using Rails 4 then you can use this instead:
EmployeeTitle.find_or_create_by(name: attributes[:name])
In my Rails app I have users who can have many projects which in turn can have many invoices.
How can I make sure that a user can only create an invoice for one of his projects and not for another user's projects?
class Invoice < ActiveRecord::Base
attr_accessible :number, :date, :project_id
validates :project_id, :presence => true,
:inclusion => { :in => ????????? }
end
Thanks for any help.
class InvoicesController < ApplicationController
def new
#invoice = current_user.invoices.build(:project_id => params[:project_id])
end
def create
#invoice = current_user.invoices.build(params[:invoice])
if #invoice.save
flash[:success] = "Invoice saved."
redirect_to edit_invoice_path(#invoice)
else
render :new
end
end
end
I think that shouldn't be on a validation. You should ensure the project the user selected is one his projects.
You could do something on your controller like:
project = current_user.projects.find params[:project_id]
#invoice = Invoice.new(project: project)
# ...
Your create action could look something like this.
def create
#invoice = current_user.invoices.build(params[:invoice])
#invoice.project = current_user.projects.find params[:invoice][:project_id]
if #invoice.save
flash[:success] = "Invoice saved."
redirect_to edit_invoice_path(#invoice)
else
render :new
end
end
project_id is "sensitive" attribute - so remove it from attr_accessible. You are right that you should not believe params from the form and you must check it.
def create
#invoice = current_user.invoices.build(params[:invoice])
# #invoice.project_id is nil now because this attr not in attr_accessible list
#invoice.project_id = params[:invoice][:project_id] if current_user.project_ids.include?(params[:invoice][:project_id])
if #invoice.save
flash[:success] = "Invoice saved."
redirect_to edit_invoice_path(#invoice)
else
render :new
end
end
If user tries to hack your app and change project_id to not owned value then method create render partial new with invalid #invoice. Do not forget to leave the validation of project_id on presence.
If you get exception Can't mass-assign protected attributes... there are several ways what to do. The simplest ways are:
1. remove line from environment configs (development, test, production)
# Raise exception on mass assignment protection for Active Record models
config.active_record.mass_assignment_sanitizer = :strict
2. Reject sensitive parameters from params before assigning.
# changes in method create
def create
project_id = params[:invoice].delete(:project_id)
#invoice = current_user.invoices.build(params[:invoice])
#invoice.project_id = project_id if current_user.project_ids.include?(project_id)
...
end
OK, luckily I managed to come up with a solution of my own this time.
I didn't make any changes to my controller ("let's keep 'em skinny"), but added a validation method to my model instead:
class Invoice < ActiveRecord::Base
attr_accessible :number, :date, :project_id
validates :project_id, :presence => true,
:numericality => { :only_integer => true },
:inclusion => { :in => proc { |record| record.available_project_ids } }
def available_project_ids
user.project_ids
end
end
I am not sure if this is good or bad coding practice. Maybe someone can shed some light on this. But for the moment it seems pretty safe to me and I haven't been able to hack it in any way so far.