Devise + CanCan: Admin manages Users - ruby-on-rails

With my set up I have 2 types of Devise users Admins and Users I would like to be able to have the admins manage the users.
I have found some tutorials about this but they approach the problem from the perspective of a single User model with roles.
So far I've gotten to the point where, when I'm logged in as an admin, I can list the users, destroy the users and create new users, however, when I try to edit a user I get a blank form (as opposed to one that's populated by user information)
Any advice would be appreciated.
Below are the relevant files. Let me know if you need to see anything else.
/config/routes.rb
TestApp::Application.routes.draw do
devise_for :admins
devise_for :users
root to: 'pages#home'
# Admin Routes
if Rails.env.production?
devise_for :admins, :skip => [:registrations]
else
devise_for :admins
end
namespace :admins do
resources :users
end
authenticated :admin do
# For production because of skip registrations
get 'admins/edit' => 'devise/registrations#edit', as: :edit_admin_registration
put 'admins' => 'devise/registrations#update', as: :admin_registration
get 'admins/dashboard' => 'admins#dashboard', as: :admin_dashboard
devise_scope :admin do
get 'admins/list' => 'admins/users#index', as: :manage_users
get 'admins/users/new' => 'admins/users#new', as: :new_admins_user
get 'admins/users/:id/edit' => 'admins/users#edit', as: :edit_admins_user
post 'admins/users' => 'admins/users#create', as: :users
delete 'admins/users/:id' => 'admins/users#destroy', as: :destroy_admins_user
end
# Manage Content Routes
get '/pages/manage' => 'pages#manage', as: :manage_pages
get '/products/manage' => 'products#manage', as: :manage_products
end
authenticated :user, :admin do
get '/products' => 'products#index'
get '/pages/4' => 'products#index'
get '/gallery' => 'products#index'
end
unauthenticated do
devise_scope :users do
get '/pages/4' => 'devise/registrations#new'
get '/gallery' => 'devise/registrations#new'
end
end
resources :pages
resources :products
end
/controllers/admins_controller.rb
class AdminsController < ApplicationController
load_and_authorize_resource
def dashboard
render "admins/dashboard"
end
def index
respond_to do |format|
format.html
end
end
def destroy
#admin.destroy
redirect_to manage_admins_path
end
end
/controllers/admins/users_controller.rb
class Admins::UsersController < ApplicationController
load_and_authorize_resource
def index
#users = User.all
respond_to do |format|
format.html
end
end
def new
#resource = User.new
respond_to do |format|
format.html
end
end
def edit
#user = User.find(params[:id])
end
def create
#user = User.new(params[:user])
respond_to do |format|
if #user.save
format.html { redirect_to manage_users_path, notice: 'User was successfully created.' }
else
format.html { render new_admin_user_path }
end
end
end
def update
#user = User.find(params[:id])
if params[:user][:password].blank?
params[:user].delete(:password)
params[:user].delete(:password_confirmation)
end
respond_to do |format|
if #user.update_attributes(params[:user])
format.html { redirect_to manage_users_path, notice: 'User was successfully updated.' }
else
format.html { render action: "edit" }
end
end
end
def destroy
#user = User.find(params[:id])
#user.destroy
redirect_to manage_users_path
end
# private
# def check_permissions
# authorize! :create, resource
# end
end
/views/admins/users/edit.html.haml
.input-form
%h2
Edit #{resource_name.to_s.humanize}
= form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => { :method => :put }) do |f|
= devise_error_messages!
%fieldset{id: "edit-your-account"}
.field.required
= f.label :first_name
= f.text_field :first_name, :autofocus => true
.field.required
= f.label :last_name
= f.text_field :last_name
.field.required
= f.label :company
= f.text_field :company
.field.required
= f.label :phone
= f.text_field :phone
.field.required
= f.label :email
= f.email_field :email
.field.required
= f.label :password
= f.password_field :password
%span.instructions
(leave blank if you don't want to change it)
- if devise_mapping.confirmable? && resource.pending_reconfirmation?
%br
Currently waiting confirmation for:
= resource.unconfirmed_email
.field.required
= f.label :password_confirmation
= f.password_field :password_confirmation
.field.required
= f.label :current_password
= f.password_field :current_password
%span.instructions
(we need your current password to confirm your changes)
.field
= f.submit "Update"
= link_to "Back", :back
/helpers/admins_helper.rb
module AdminsHelper
# Devise helpers for Admin::UsersController
def resource_name
:user
end
def resource
#resource ||= User.new
end
def devise_mapping
#devise_mapping ||= Devise.mappings[:user]
end
end

Looks like renaming your instance variable should do the trick. Your edit template passes the object resource to the form but the object you've loaded from the database is set to #user:
# Admins::UsersController
def edit
#user = User.find(params[:id])
end
# AdminsHelper
def resource
#resource ||= User.new
end
You could pass this instance variable to the form_for instead, or rename #user as #resource so the helper method will return the proper instance.

Related

Rails undefined method for nil:NilClass

I get this error while clicking in form Log In button:
undefined method `[]' for nil:NilClass
View with signup form is below:
<%= form_for(:session, url: login_path) do |f| %>
<%= f.text_field :login, :placeholder => "login" %>
<%= f.password_field :password, :placeholder => "Password" %>
<%= f.submit "Log in", class: "btn-submit"%>
<% end %>
SessionsController :
class SessionsController < ApplicationController
def new
end
def create
#user = User.find_by_login(params[:login][:password]) ////ERROR LINE
if #user && #user.authenticate(params[:session][:password])
session[:user_id] = #user.id
redirect_to '/'
else
flash.now[:danger] = 'err'
redirect_to '/login'
end
end
def destroy
session[:user_id] = nil
redirect_to '/'
end
end
User controller:
class UsersController < ApplicationController
def new
#users = User.new
end
def create
#user = User.new(user_params)
if #user.save
session[:user_id] = #user.id
current_user = #user.id
redirect_to #user
else
redirect_to '/login'
end
end
private
def user_params
params.require(:user).permit(:first_name, :last_name, :email, :login)
end
end
Routes:
get 'logout' => 'sessions#destroy'
post 'logout' => 'sessions#destroy'
delete 'logout' => 'sessions#destroy'
get 'login' => 'sessions#new'
post 'login' => 'sessions#create'
resources :users, :user_types
Also, I do not want to create 'signup' view - there should be no possibility to create user through website
Looking at your form, I don't think you have params[:login]. So it'll be nil and params[:login][:password] is evaluated to nil[:password]. That's why you have the error.
What you have, according to the form, should be params[:session][:login] and params[:session][:password]. So the only remaining question is how you implemented your User.find_by_login method.

Sidestep Devise "You are already logged in." error with Namespaced custom controllers and views

This article explains how to let users make other users in devise. I've got it almost figured out, thanks to the answer provided in that article by Adam Waselnuk except I can't figure out how to create the same result with namespaced controllers and views. I get one of two errors, depending on how my routes.rb is set up... either Routing Error, No route matches [PUT] "/users/new" or
My code is below.
routes.rb
Rails.application.routes.draw do
namespace :admin do
resources :users, except: :create
end
post 'users/new' => 'admin/users#new', as: :create_user
resources :shows
resources :posts
resources :global_posts
devise_for :users, controllers: {
registrations: "users/registrations",
}
root 'pages#home'
get 'pages/home'
get 'admin' => 'admin#index', as: :admin
end
admin/users_controller.rb
class Admin::UsersController < ApplicationController
before_action :set_user, only: [:show, :edit, :update, :destroy]
# GET /admin/users
# GET /admin/users.json
def index
#all_users = User.all
end
def show
end
# GET /admin/users/new
def new
#user = User.new
end
# GET /admin/users/1/edit
def edit
end
# POST /admin/users
# POST /admin/users.json
def create
#user = User.new(user_params)
end
# PATCH/PUT /admin/users/1
# PATCH/PUT /admin/users/1.json
def update
respond_to do |format|
if #user.update(user_params)
format.html { redirect_to #user, notice: 'User was successfully updated.' }
format.json { render :show, status: :ok, location: #user }
else
format.html { render :edit }
format.json { render json: #user.errors, status: :unprocessable_entity }
end
end
end
# DELETE /admin/users/1
# DELETE /admin/users/1.json
def destroy
#user.destroy
respond_to do |format|
format.html { redirect_to root_path, notice: 'User was successfully destroyed.' }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_user
#user = User.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def user_params
params.require(:user).permit(
:name, :email, :password, :password_confirmation
)
end
end
And finally, the admin/users/new.html.erb
<h2>New User</h2>
<%= form_for #user, url: create_user_path, html: { method: :put } do |f| %>
<div>
<%= f.label :name %><br />
<%= f.text_field :name, autofocus: true %>
</div>
<div>
<%= f.label :email %><br />
<%= f.email_field :email, autofocus: true %>
</div>
<div>
<%= f.label :password %>
<%= f.password_field :password, autocomplete: "off" %>
</div>
<div>
<%= f.label :password_confirmation %><br />
<%= f.password_field :password_confirmation, autocomplete: "off" %>
</div>
<div>
<%= f.submit "Create New User" %>
</div>
<% end %>
Your route should be
post 'users' => 'admin/users#create', as: :create_user
Then you can create the user in
def create
#user = User.new(user_params)
#user.save
end

Rails 4 - Input User data into Model

I'm building a marketplace app where sellers can list items to sell. I use devise for authentication and also have user routes to collect user inputs after they sign up.
I'm trying to set this form up to collect two seller profile inputs from users and save it in the user model. But the controller does not read the user I get an error param not found: user error.
my routes: the match line was added for the profile form I'm posting about.
devise_for :users
resources :users, only: [:update, :edit]
match "/users/:id/sellerprofile", to: "users#sellerprofile", via: [:get, :put], as: :sellerprofile
my user controller method: note that I'm using the update process for a different form so I don't think I can use it for this form.
def sellerprofile
#user = User.find(params[:id])
# if params[:user]
respond_to do |format|
if #user.update(user_params)
format.html { redirect_to root_url, notice: 'Your profile was successfully updated.' }
else
format.html { render action: 'edit' }
end
end
#end
end
def update
#user.attributes = user_params
Stripe.api_key = ENV["STRIPE_API_KEY"]
token = params[:stripeToken]
recipient = Stripe::Recipient.create(
:name => user_params["bankaccname"], #if i want to save to db, use this: #user.bankaccname. not saving it
:type => "individual", #gives you only one set of error messages. i don't need the respond
:bank_account => token #to block either i think. eg. when i enter firstname only, i get two error
) #msgs
#user.recipient = recipient.id
respond_to do |format|
if #user.save
format.html { redirect_to edit_user_url, notice: 'Your account was successfully updated.' }
else
format.html { render action: 'edit' }
end
end
end
def user_params
params.require(:user).permit(:bankaccname, :profileimage, :profilestory)
end
sellerprofile.html.erb
<%= form_for #user, url: sellerprofile_path(#user), html: { method: :put, :multipart => true } do |f| %>
<div class="form-group">
<%= f.label :your_story %><i> (required)</i>
<%= f.text_area :profilestory, class:"form-control" %>
</div>
<div class="form-group">
<%= f.label :profile_image %><i> (required)</i>
<%= f.file_field :profileimage, class:"form-control" %>
</div>
<div class="form-group">
<%= f.submit class:"btn btn-primary" %>
</div>
<% end %>
user.rb:
has_attached_file :profileimage,
:styles => { :profile => "200x200", :p_thumb => "100x100>" },
:default_url => ""
validates_attachment_content_type :profileimage, :content_type => /\Aimage\/.*\Z/
Routes
First things first:
#config/routes.rb
devise_for :users
resources :users, only: [:update, :edit] do
match :sellerprofile, via: [:get, :put], as: :sellerprofile
end
This is a much cleaner way to handle the route.
--
Controller
Next, you need to handle the data-processing capability of the controller. To do this, you need to make sure you're sending the right params (which I believe you are. If you aren't it will be in the url) of the form:
<%= form_for #user, url: sellerprofile_path, method: :put, html: { multipart: true } do |f| %>
You may wish to observe this routing structure inside Rails:
This is the standard routing structure inside Rails (called from the resources directive). This means that if you want to use a route equivalent to update, you will likely be able to omit the #user variable from it (although I've not tested this)
-
In order to make this work as correctly as you need, you need to ensure you're passing the parameters as follows:
params => {
"user" => {
"id" => "1",
"other" => "value",
}
This will be handled by the form_for method. When you call the user_params in your controller, the params.require block looks out for the user param; the permit block looking for the individual values contained within it.
This will allow you to do the following:
#app/controllers/users_controller.rb
def UsersController < ApplicationController
def sellerprofile
#user = User.find params[:id]
#user.update user_params
end
private
def user_params
params.require(:user).permit(:x, :y, :z)
end
end
This should work as you need
--
Update
Personally, I don't see the difference between what you're doing here, and with the update method?
If you want to use Stripe details in the update method, why not just have a separate method for the stripe processing, and call it if you have certain params set:
#app/controllers/users_controller.rb
class UsersController < ApplicationController
def update
#user.attributes = user_params
Stripe.api_key = ENV["STRIPE_API_KEY"]
token = params[:stripeToken]
if token. present?
recipient = stripe
#user.recipient = recipient.id
end
respond_to do |format|
if #user.update user_params
format.html { redirect_to edit_user_url, notice: 'Your account was successfully updated.' }
else
format.html { render action: 'edit' }
end
end
end
end
private
def stripe
return Stripe::Recipient.create(
:name => user_params["bankaccname"], #if i want to save to db, use this: #user.bankaccname. not saving it
:type => "individual", #gives you only one set of error messages. i don't need the respond
:bank_account => token #to block either i think. eg. when i enter firstname only, i get two error
)
end
def user_params
params.require(:user).permit(:x, :y, :z)
end
end

Param in URL not passed to a form - Rails 4

When accessing URL localhost:3000/buyers/sign_up?email=abc%40abc.com , the sign-up form is displayed but email field stays empty and is not auto-filled with abc#abc.com.
Could this be related to the implemented Single Table Inheritance (STI) ? -> Buyer inherits from User.
routes.rb
devise_for :users, :controller => {:sessions => 'sessions'}, :skip => :registrations do
delete '/logout', :to => 'sessions#destroy', :as => :destroy_user_session
get '/login', :to => 'sessions#new', :as => :new_user_session
post '/login', :to => 'sessions#create', :as => :new_user_session
end
devise_for :sellers, :skip => :sessions
devise_for :buyers, :skip => :sessions
resources :users
devise/registrations/new.html.erb
<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
<%= devise_error_messages! %>
<div><%= f.label :email %><br />
<%= f.email_field :email, autofocus: true %></div>
<div><%= f.submit "Sign up" %></div>
<% end %>
users_controller
class UsersController < ApplicationController
before_action :set_user, only: [:show, :edit, :update, :destroy]
def create
#user = User.new(user_params)
respond_to do |format|
if #user.save
format.html { redirect_to #user, notice: 'User was successfully created.' }
format.json { render action: 'show', status: :created, location: #user }
else
format.html { render action: 'new' }
format.json { render json: #user.errors, status: :unprocessable_entity }
end
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_user
#user = User.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def user_params
params.require(:user).permit(:first_name, :last_name, :name, :type, :email)
end
end
Just found the answer.
Simply need to add value in the form + a condition to check presence of the e-mail param:
<%= f.email_field :email, value: params[:email] %>
Params
The problem isn't to do with STI - it's to do with how you're passing variables / parameters to your form.
You must remember that when you use form_for, Rails will automatically populate the provided HTML elements with data from the ActiveRecord object - in that if your object has the attribute email, the form will have that data populated
If you want to populate the form with new data (from a URL), you'll either have to be able to populate the #user.email attribute (as suggested by JTG), or take the params from the URL & pass them through to your form
--
The ways I would look at doing this would include:
Controller-based (the best)
Front-end "params" based
The controller method can be accessed as follows:
#app/controllers/users_controller.rb
Class UsersController < ApplicationController
def new
#user = User.new(email: params[:email])
end
end
This should work, as the param should be made available to the controller during the request.
-
The alternative, as you've discovered, is to populate the value of the element on your page:
<%= f.text_field :email, value: params[:email] %>
This will leave the :email field blank if no email parameter was / is defined.
Making a request to new#User (which is what you are requesting with /buyers/sign_up) won't autofill any of the sign_up form by default
However, you can put
def new
#user = User.new
#user.email = params[:email] if params.has_key? "email"
end
for the url
/buyers/sign_up?email=abc%40abc.com
And it should auto complete the email textfield of your form.
if your url was structured like so...
/buyers/sign_up?user[email]=abc%40abc.com
Then you should be able to use the much more elegant
def new
#user = User.new(params[:user])
end
in your new call. That way you can send in more than one parameter

First argument in form cannot contain nil or be empty (acts_as_commentable error)

So I'm trying to get the acts_as_commentable_with_threading gem working in my app. I would like to allow the users to comment on specific events (events/show.html). I don't think I've set it up properly as I'm getting the below error.
error:
Showing /Users/user/Sites/new_work/app/views/events/show.html.erb where line #36 raised:
First argument in form cannot contain nil or be empty
</div>
<div class="name"></div>
<div id="comments">
<%= form_for #comment do |f| %> <---- it's referring to this line
<div><%= f.hidden_field :event_id, value: #event.id %></div>
<div><%= f.text_field :body, row: 20, placeholder: "Leave a comment" %></div>
<%= f.submit "Post Comment" %>
comments_controller.rb
class CommentsController < ApplicationController
before_filter :load_commentable
def index
#comments = #commentable.current_user.comments
end
def new
#comment = #commentable.current_user.comments.new
end
def create
#comment = #commentable.current_user.comments.new(params[:comment])
if #comment.save
redirect_to #commentable, notice: "Comment created."
else
render :new
end
end
private
def load_commentable
resource, id = request.path.split('/')[1, 2]
#commentable = resource.singularize.classify.constantize.find(id)
end
end
comment.rb snippit only
class Comment < ActiveRecord::Base
attr_accessible :title, :body, :subject
acts_as_nested_set :scope => [:commentable_id, :commentable_type]
validates :body, :presence => true
validates :user, :presence => true
# NOTE: install the acts_as_votable plugin if you
# want user to vote on the quality of comments.
#acts_as_votable
belongs_to :commentable, :polymorphic => true
# NOTE: Comments belong to a user
belongs_to :user
event.rb
class Event < ActiveRecord::Base
attr_accessible :title, :description, :location, :date, :time, :event_date
acts_as_commentable
has_many :comments, as: :commentable
belongs_to :user
after_create :update_event_date
def update_event_date
date = self.date.to_s
time = self.time
hour = Time.parse(time).strftime("%H:%M:%S").to_s
event_date = (date + ' ' + hour).to_time
self.update_attributes(event_date: event_date)
end
end
comments/form.html.erb
<%= form_for [#commentable, #comment] do |f| %>
<% if #comment.errors.any? %>
<div class="error_messages">
<h3>Please correct the following errors.</h3>
<ul>
<% #comment.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
</ul>
</div>
<% end %>
<div class="field">
<%= f.text_field :body, rows: 10, placeholder: "Leave a comment" %>
</div>
<div class="actions">
<%= f.submit "Post comment", class: "btn" %>
</div>
<% end %>
events_controller.rb
class EventsController < ApplicationController
before_filter :authenticate_user!
def index
#user = current_user
#events = Event.all
end
def new
#event = Event.new
end
# def create
# #event = Event.new(params[:event])
# if #event.save
# redirect_to :action => 'index'
# else
# #events = Event.find(:all)
# render :action => 'new'
# end
# end
def create
#event = current_user.events.new(event_params)
respond_to do |format|
if #event.save
format.html { redirect_to :back, notice: 'Event was successfully created.' }
format.json { render action: 'show', status: :created, location: #event }
format.js
else
format.html { render action: 'new' }
format.json { render json: #event.errors, status: :unprocessable_entity }
format.js
end
end
end
def show
#event = Event.find(params[:id])
end
def edit
#event = Event.find(params[:id])
end
def update
#event = Event.find(params[:id])
if #event.update_attributes(params[:event])
flash[:success] = "Event updated."
redirect_to #event
else
render 'edit'
end
end
def destroy
#event = Event.find(params[:id])
#event.destroy
respond_to do |format|
format.html {redirect_to :back}
format.js
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_event
#event = Event.find(params[:id])
end
routes.rb
New_app::Application.routes.draw do
# get 'auth/:provider/callback', to: 'sessions#create'
# get 'auth/failure', to: redirect('/')
root to: "home#landing"
devise_for :users, :controllers => {:registrations => "users/registrations",
:sessions => "users/sessions",
:passwords => "users/passwords",
:omniauth_callbacks => "users/omniauth_callbacks"
}
get "welcome", to: "home#welcome", as: 'welcome'
devise_scope :user do
# get "edit/edit_account", :to => "devise/registrations#edit_account", :as => "account_registration"
get 'edit/edit_account' => 'users/registrations#account_registration', as: :edit_account
end
# patch '/users/:id', to: 'users#update', as: 'user'
get 'profile/:id' => "users#show", as: :profile
get 'disconnect' => 'users#disconnect'
resources :users do
resources :questions
end
resources :photos
resources :events do
resources :comments
end
post "/events/add_new_comment" => "events#add_new_comment", :as => "add_new_comment_to_events", :via => [:event]
resources :questions
end
rake routes for comments
event_comments GET /events/:event_id/comments(.:format) comments#index
POST /events/:event_id/comments(.:format) comments#create
new_event_comment GET /events/:event_id/comments/new(.:format) comments#new
edit_event_comment GET /events/:event_id/comments/:id/edit(.:format) comments#edit
event_comment GET /events/:event_id/comments/:id(.:format) comments#show
PATCH /events/:event_id/comments/:id(.:format) comments#update
PUT /events/:event_id/comments/:id(.:format) comments#update
DELETE /events/:event_id/comments/:id(.:format) comments#destroy
Is #comment defined in the "show" action in your Events controller? Can you post the Events controller code as well?
One thing to double check is to ensure that the action that renders the view for show.html.erb has the #comment variable defined. You seem to be getting the message because the #comment variable in
<%= form_for #comment do |f| %>
Is currently nil when you render the view.
In the "show" action for the events controller, try setting the #comment variable by adding:
#comment = #event.comments.new
Edit 2: Make sure you've setup your routes.rb file to handle comments on events, so assuming youre using RESTful routes, something like this below in your routes.rb. If you could post the routes file that would be helpful too.
resources :events do
resources :comments
end
The error is on app/views/events/show.html.erb which means that your Events controller's show action is missing the #comment variable
def show
#event = Event.find(params[:id])
#comment = ....what ever you need to pull in the comments....
end

Resources