So, I'm trying to implement simple role based authentication system using Rails and I'm having a problem with final step - changing roles.
role is attribute in user table and it has a string type.
Idea is that some users with some privilages have ability to change roles.
Code in view looks like this:
<div>
<%= f.label :role, 'Role' %>
<%= f.collection_select :role, User::ROLES, :to_s, :humanize,
prompt: 'Select role' %>
</div>
update method in users_controller looks like this:
def update
#user = User.find(params[:id])
if #user.update(update_params) #update_params is method that returns permitted parameters
redirect_to #user
else
render :edit
end
end
Problem is that user[role] is empty after submitting a form.
Everything is pretty much made "by the book". Also, I am using Cancan but it's turned off for edit and update with load_and_authorize_resource :except => [:update, :edit].
So, I've found a solution. Since I'm using CanCan and user_params method so CanCan itself can set permitted parameters, my update_params method is ignored.
Solution is to add :role to user_params method.
Related
At the beginning let me excuse for easy question for those who are experienced but for me it is difficult right now. In the Rails in one of the views/_form.html.erb, I want to change line below (it works):
<%= f.collection_select :user_id, User.all, :id, :email %>
into hidden field that will hold the id of the user that is logged in. I try to change it into:
<% f.hidden_field :user_id, :id %>
but it throws an error:
NoMethodError in Orders#new
undefined method `merge' for :id:Symbol
Can sb help me to solve that?
Use this (if you already have current_user method available):
<%= f.hidden_field :user_id, :value => current_user.id %>
If you don't have current_user method implemented, in your corresponding controller, you can have something like this:
#current_user = User.find(params[:user_id])
Then, in your view, you can do:
<%= f.hidden_field :user_id, :value => #current_user.id %>
Update
After the above conversation, if you want to use session to store the user_id, then you can do something like this.
You can create a SessionsHelper module (which you can include in your ApplicationController) where you can define a log_in method:
# Logs in the given user.
def log_in(user)
session[:user_id] = user.id
end
(You can also put this: session[:user_id] = user.id in the create action where you create an user.)
You can also define a current_user method in this module:
# Returns the current logged-in user (if any).
def current_user
#current_user ||= User.find_by(id: session[:user_id])
end
Here are some other useful helper methods that you can add in this module:
# Returns true if the given user is the current user.
def current_user?(user)
user == current_user
end
# Returns true if the user is logged in, false otherwise.
def logged_in?
!current_user.nil?
end
# Logs out the current user.
def log_out
session.delete(:user_id)
#current_user = nil
end
Finally, I would suggest you to take a look at Devise gem which is a very popular authentication solution for Rails application.
This error means that it expects a hash, but you are putting an empty symbol.
You need to send a hash
If you have the current_user method:
<%= f.hidden_field :user_id, :value => current_user.id %>
If you don't, you may use this in your controller
id = User.find(someid)
And keep the same code in the view.
Look # the documentation for more info
The first two answers are correct. Here's why
f.hidden_field is a method on the form object you created somewhere either in that partial or in a file/partial that includes it. The documentation linked by Hristo Georgiev is for the hidden_field method from ActionView::Helpers::FormHelper (link). The one you're trying to call is from ActionView::Helpers::FormBuilder (link)
hidden_field works just as you seem to be expecting it to, ie
hidden_field(:user_object, :id)
these are two different methods
f.hidden_field works a bit differently, because it already has access to some of the information used to build the hidden field. It expects a method to call on the form object and an optional hash which it converts to attributes on the hidden field (thus the :value => user.id hash)
Assuming you had the following form
form_for(#user) do |f|
...
end
And you wanted the id to be a hidden field, you would put this within that block
f.hidden_field(:id)
That would generate the following HTML
<input type="hidden" id="user_id" name="user[id]" value="1" />
See this line from ActionView
TL;DR
You're calling the FormBuilder#hidden_field method with the arguments expected by FormHelper#hidden_field
I'm trying to make simple app. I input my first name and last name to simple <%= form_for #data do |f| %> rails form and after submitting it, app should render simple text like this. My first name is <%= data.first_name %> and my last name is <%= data.last_name %>. I don't know why but my app is saying this error:
undefined local variable or method `data' for
It's probably saying it because no params are passed to view.
Here is my code.
routes.rb
resources :data, only: [:new, :create, :index]
data_controller.rb
class DataController < ApplicationController
def new
#data = Data.new
end
def index
end
def create
#data = Data.new(data_params)
if #data.valid?
redirect_to #data
else
render :new
end
end
private
def data_params
params.require(:data).permit(:first_name, :second_name)
end
end
/views/data/new.html.erb
<%= form_for #data do |f| %>
<%= f.label :first_name %>
<%= f.text_field :first_name %>
<%= f.label :second_name %>
<%= f.text_field :second_name %>
<%= f.submit 'Continue', class: 'button' %>
<% end %>
/views/data/index.html.erb
<h2>Coolest app ever :D</h2>
<p>My first name is: <%= data.first_name %>.</p>
<p>And my second name is: <%= data.second_name %>.</p>
/models/data.rb
class Data
include ActiveModel::Model
attr_accessor :first_name, :second_name
validates :first_name, :second_name, presence: true
end
Please help to find out why params are not passing to next page. Thanks anyways :D
Your view should look like this:
<h2>Coolest app ever :D</h2>
<p>My first name is: <%= #data.first_name %>.</p>
<p>And my second name is: <%= #data.second_name %>.</p>
Also, I would suggest that calling a model something generic like Data is not a very Rails-y approach. Generally, domain models correspond to real-world things like User and Article, which are easy to understand and relate to. It'll get confusing quite fast if you use need to make another model and want to call it Data2 or something :)
Edit:
Since you specified that you do not wish to use the database, I would recommend passing in the object params through the redirect:
redirect_to(data_path(data: #data))
and in your controller's index method:
def index
#data = Data.new(params[:data])
end
Now your view should render properly, since you're passing the in-memory #data object attributes as params within the redirect. You then recreate this object in the index page or wherever you wish to redirect to.
To expand on Matt's answer, the reason you're getting NilClass errors is because:
You're redirecting to a data#show action when no show action has been enabled within your routes file. Since you've set your views up for the index, I'm assuming you want to redirect there when the #data object has been verified as valid:
redirect_to data_path
However I would recommend you follow Rails conventions and specify the data#show route within your routes.rb:
resources :data, only: [:index, :new, :create, :show]
and in your data_controller.rb:
def show
#data = Data.find(params[:id])
end
Another problem is that you're not actually saving the #data object upon creating it. The new method populates the attributes, and valid? runs all the validations within the specified context of your defined model and returns true if no errors are found, false otherwise. You want to do something like:
def create
#data = Data.new(data_params)
if #data.save
redirect_to data_path
else
render :new
end
end
Using save attempts to save the record to the database, and runs a validation check anyways - if validation fails the save command will return false, the record will not be saved, and the new template will be re-rendered. If it is saved properly, the controller will redirect to the index page, where you can call upon the particular data object you want and display it within your view.
I have an active admin resource like this:
ActiveAdmin.register Snippet do
menu label: "Text Snippets"
config.clear_sidebar_sections!
index download_links: false do
column :key if current_admin_user.developer?
column :description
column :contents
default_actions
end
form do |f|
f.inputs do
f.input :description
f.input :contents
end
f.buttons
end
end
Notice in the index block, I'm only adding the key column if the current admin user is a developer. I want to apply this kind of filtering to the available actions.
I tried added this at the top of the resource definition:
actions current_admin_user.developer ? :all : :index, :edit
But I get a NameError on current_admin_user. For some reason, outside of the configuration blocks, the active admin current_admin_user helper doesn't exist.
So, how could I go about filtering actions based on the current user's priviliges?
you have to use a proc... current_admin_user works only when the app it's running, not when you declare your class..
example..
action_item :only => [:edit], :if => proc { current_admin_user.developer? } do
#enter code here
end
You can also use CanCan for this.. and place controller.authorize_resource at the beginning. Check the activeadmin documentation for this.
Another way to do it is overriding the action_methods in the ActiveAdmin controller.. like this
actions :all
controller do
def action_methods
if current_admin_user.developer?
super
else
super - ['show', 'destroy', 'new', 'create']
end
end
end
this works cool if you have multiple roles.
btw you should use developer? instead of developer (off course if the developer method returns a boolean as I suspect)
UPDATE
in version 0.6+ you should stick with CanCan, for more information check the activeadmin documentation http://www.activeadmin.info/docs/13-authorization-adapter.html#using_the_cancan_adapter
I have a Rails app with a user model that contains an admin attribute. It's locked down using attr_accessible. My model looks like this:
attr_accessible :name, :email, :other_email, :plant_id, :password, :password_confirmation
attr_accessible :name, :email, :other_email, :plant_id, :password, :password_confirmation, :admin, :as => :admin
And here's what my update method in my users controller looks like:
def update
#user = User.find(params[:id])
if #user.update_attributes(params[:user], :as => current_user_role.to_sym)
flash[:notice] = "Profile updated"
redirect_to edit_user_url(#user)
else
render 'edit'
end
end
I have a helper method in my application controller that passes back the role as a string:
def current_user_role
#current_user_role ||= current_user.admin? ? "admin" : "default"
end
helper_method :current_user_role
I've also set config.active_record.whitelist_attributes = true in config/application.rb.
I've verified that the current_user_role method is returning the proper value based on the current user's admin status. Rails isn't throwing a mass-assignment error. But when I try to update a user's admin status while logged in as an admin, Rails performs the update and silently ignores the admin attribute. Pulling up the user's record in the Rails console shows that the record hasn't been modified.
I have a feeling there's a Ruby- or Rails-specific issue at play that I'm not aware of. I can't locate any info on making the role dynamic. The best I could find was this.
There was an errant attr_accessor :admin in my model that was left in from a prior attempt at getting this to work. I overlooked it. Removing it fixed it.
So, the upshot is that this is a pretty simple way to get dynamic roles working in Rails 3.2.
Looks like it could be a bug in Rails 3.2
https://github.com/stffn/declarative_authorization/issues/127
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.