I want a simple system where users have three types (user, admin and medic) and the ones tagged Admin can "activate" other users' accounts so they can access certain privileges. I tried doing this with a simple button but I haven't found a way to do so.
The button for the code is:
<%= button_to "Change user Type to Medic", :method=> "activate_medic" %>
My activation method is as so:
def activate_medic
#user = User.find(params[:id])
#user.activated = true
if #user.save
flash[:info] = "Success"
end
end
And there's a post 'users/activate_medic' in my routes.rb file.
However, pressing the button brings up:
ActionController::RoutingError (No route matches [POST] "/users/1"):
If I'm trying to edit user 1.
Not exactly what I proposed in the original question, but:
I allowed the Admin users to completely edit a given user's info and by proxy their user type and permissions:
Users controller:
before_action :correct_user, only: [:edit, :update]
def correct_user
#user = User.find(params[:id])
redirect_to(root_url) unless current_user?(#user) || current_user.type == "Admin"
end
However, to avoid any user from just accessing their edit page and giving themselves admin rights, I edited the form to only allow Admins to see the field that edits their type:
<% if #user.type == "Admin" %>
<%= f.label :type, "User Type:" %>
<%= f.text_field :type, class: 'form-control' %>
<% end %>
The method option in link_to is meant to specify an HTTP verb, not the name of a custom method. Based on what you've provided, I would approach the problem like this:
button_to "Change user Type to Medic", activate_user_path(#user), method: :patch
And a dedicated controller:
class ActivateUserController
def update
#user = User.find(params[:id])
if #user.activate
flash[:info] = "Success"
end
redirect_to #user
end
end
And a route:
resources :users do
patch :activate, to: 'activate_user#update', as: :activate_user
end
And finally, move user behavior into the User model:
class User
def activate
self.update_attribute!(:active, true)
end
end
Related
I am trying to delete all task that is linked to logged in user but when I click on delete all button it shows the error
No route matches [POST] "/tasks/destroy_all"
task_controller.rb
class TaskController < ApplicationController
def all_destory
#user = current_user
#user.tasks.destroy_all
redirect_to user_tasks_path
end
end
route.rb
get '/tasks/destroy_all', to: 'task#all_destory', as: :destroy_all
HTML
<% #tasks.each do |task| %>
<%= task.daily_task %>
<%= task.date %>
<% end%>
<%= button_to "delete all", destroy_all_path %>
When destroying records you want to use the DELETE HTTP verb.
GET requests are saved in the browsers history and should not create, modify or destroy anything on the server.
Typically in Rails you just have a route to destroy a single record. But if DELETE /things/1 deltes a single resource then DELETE /things should logically destroy the entire collection:
get '/user/tasks', to: 'users/tasks#index', as: :user_tasks
delete '/user/tasks', to: 'users/tasks#destroy_all'
# app/controllers/users/tasks_controller.rb
module Users
class TasksController < ApplicationRecord
before_action :authenticate_user!
# display all the tasks belonging to the currently signed in user
# GET /user/tasks
def index
#tasks = current_user.tasks
end
# destroy all the tasks belonging to the currently signed in user
# DELETE /user/tasks
def destroy_all
#tasks = current_user.tasks
#tasks.destroy_all
redirect_to action: :index
end
private
# You don't need this if your using Devise
def authenticate_user!
unless current_user
redirect_to '/path/to/your/login',
notice: 'Please sign in before continuing'
end
end
end
end
<%= button_to "Delete all", user_tasks_path, method: :delete %>
Your HTTP verb and your route must match. Currently your button is using POST, but your route accepts GET. You could change them both to POST.
post '/tasks/destroy_all', to: 'task#all_destory', as: :destroy_all
This fixes the problem in the question, but it's not ideal. As #max points out, DELETE would be more communicative of what clicking the button does– delete resources.
DELETE documentation
I am using devise for user management so it let's user sign up with default email and password fields.
I added new fields/columns into the user model say username, designation and company.
So I have a profile view say with route '/users/1' and a link_to helper which would allow me to edit and update my user info.
By default i can only use users/edit route to edit my user info. How can i manage a new or separate edit and update option with different route say '/users/1/edit' from my profile view.
I read some posts before this but didn't help me. If anyone could outline things i should do. Thanks for reading :))
Edit:
routes file
root 'public#index'
devise_for :users
resources :users do
put 'users/:id/edit', to: 'users#edit'
end
user controller
class UsersController < ApplicationController
before_action :authenticate_user!
after_action :verify_authorized
before_action :set_user, only: %i[ show edit update ]
def index
#users = User.all
authorize User
end
def show
authorize #user
end
def edit
if current_user == #user
#user.update()
end
end
def update
authorize #user
if #user.update(secure_params)
redirect_to users_path, :notice => "User updated."
else
render 'edit'
end
end
private
def secure_params
params.require(:user).permit(:designation, :company,
:username)
end
def set_user
#user = User.find(params[:id])
end
end
In my view to go to edit:
<% if current_user.id == #user.id %>
<%= link_to 'Edit My profile', edit_user_path(#user), method: :edit,
class:"btn btn-primary" %>
<% end %>
If you really want to have a route user/:id/edit and not use the Devise default users/edit route(which edits the currently logged-in user). You can do the following:
Let's assume you have a users controller(if you don't have one, create one) and add an edit action to it which will handle the editing logic:
class UsersController < ApplicationController
# other code
def edit
user = User.find_by(id: params[:id]) # this id will be passed through the route
# Now here you need some authorization logic to prevent users from updating others.
# If you use CanCanCan, Pundit or any other authorization gem then write
# this logic there
if current_user == user
user.update() # do your update logic here with params you have
# render some json or whatever you want
else
# render some error messages in format you are using
end
end
end
This is the controller logic, now in your routes.rb file you need to register this route:
put 'user/:id/edit', to: 'users#edit'
This will edit the user with ID specified at :id.
Note again: This is not the approach I would take, I would rather just use the users/edit route and update the currently logged in user, but you wanted an example of this so do as you will
How can you make a user edit action only available if the user is current user? I am using devise.
Devise has this:
before_action :authenticate_user!, only: [:new, :edit, :update, :destroy], notice: 'you must sign in first!'
But all this does is make sure a user is logged in not if a user is equal to current user? I want to make sure other users aren't able to edit other users accounts.
What is the best way to do this? Should I create a new before_filter? I couldn't find any standard way.
You can use the current_user method provided by devise. Here you can read more -current_user method.
def edit
unless current_user
redirect_to home_path, :alert => "Restricted area"
end
end
I highly advise looking into the CanCanCan gem to handle these things. In such a case your code would look something like:
View:
<% if user_signed_in? %>
<% if can? :update, #user %>
# Edit something
<%= link_to edit_profile_path(#user), class: 'user' do %>
Edit your profile
<% end %>
<% end %>
<% end %>
And in your Users controller or such you would add the following line which would take care of the case where a user manually types a url unto the browser:
Controller:
class UsersController < ApplicationController
load_and_authorize_resource
...
More info and docs: https://github.com/CanCanCommunity/cancancan
I have this app where a user can write a review for a school. A user must sign in with Facebook to save a review. The problem is if a user is unsigned and writes a review, then signs in with Facebook they have to write the same review again.
I am trying to fix this by storing the review data form in sessions, but I cant quite make it work.
What is the proper rails way to do this?
ReviewForm:
<%= form_for [#school, Review.new] do |f| %>
<%= f.text_area :content %>
<% if current_user %>
<%= f.submit 'Save my review', :class => "btn" %>
<% else %>
<%= f.submit 'Save my review and sign me into facebook', :class => "btn" %>
<% end %>
<%end %>
ReviewController
class ReviewsController < ApplicationController
before_filter :signed_in_user, only: [:create, :destroy]
def create
#school = School.find(params[:school_id])
#review = #school.reviews.new(params[:review])
#review.user_id = current_user.id
if #review.save
redirect_to #review.school, notice: "Review has been created."
else
render :new
end
end
def new
#school = School.find_by_id(params[:school_id])
#review = Review.new
end
def save_review(school, review, rating)
Review.create(:content => review, :school_id => school,
:user_id => current_user, :rating => rating)
end
private
def signed_in?
!current_user.nil?
end
def signed_in_user
unless signed_in?
# Save review data into sessions
session[:school] = School.find(params[:school_id])
session[:review] = params[:review]
session[:rating] = params[:rating]
# Login the user to facebook
redirect_to "/auth/facebook"
# After login save review data for user
save_review(session[:school], session[:review], session[:rating])
end
end
end
My understanding is that it's not "The Rails Way" to store things in the session besides really tiny stuff like a user token, etc. You can read more about that idea in The Rails 3 Way by Obie Fernandez.
I would recommend that you store reviews in the database right from the start and only "surface" the review after the review has been connected to a Facebook-authenticated user. If you have any curiosities regarding how to accomplish that, I'm happy to elaborate.
Edit: here's a little sample code. First I'd take care of associating users with reviews, for "permanent" storage. You could just add a user_id to the review table, but it would probably be null most of the time, and that seems sloppy to me:
$ rails g model UserReview review_id:references, user_id:references
Then I'd create a user_session_review table with a review_id and a user_session_token. This is for "temporary" storage:
$ rails g model UserSessionReview review_id:integer, user_session_token:string
Then when a user signs up, associate any "temporary" reviews with that user:
class User
has_many :user_reviews
has_many :reviews, through: :user_reviews
has_many :user_session_reviews
def associate_reviews_from_token(user_session_token)
temp_reviews = UserSessionReview.find_all_by_user_session_token(user_session_token)
temp_reviews.each do |temp_review|
user_reviews.create!(review_id: temp_review.review_id)
temp_review.destroy
end
end
end
So in your controller, you might do
class UsersController < ApplicationController
def create
# some stuff
#user.associate_reviews_from_token(cookies[:user_session_token])
end
end
You'll of course have to read between the lines a little bit, but I think that should get you going.
Edit 2: To delete old abandoned reviews, I'd do something like this:
class UserSessionReview
scope :old, -> { where('created_at < ?', Time.zone.now - 1.month) }
end
Then, in a cron job:
UserSessionReview.old.destroy_all
You should save the review in the create sessions action (which is not included in your question). Assuming you are using omniauth, you can add something on the action that handles the callback
# review controller
def signed_in_user
unless signed_in?
# Save review data into sessions
session[:school] = School.find(params[:school_id])
session[:review] = params[:review]
session[:rating] = params[:rating]
# Login the user to facebook
redirect_to "/auth/facebook"
end
end
# callback to login the user
def handle_callback
# do your thing here to login the user
# once you have the user logged in
if signed_in?
if session[:school] && session[:review] && session[:rating] # or just 1 check
Review.create(
content: session.delete(:review),
school_id: session.delete(:school),
user_id: current_user.id,
rating: session.delete(:rating)
)
#redirect_to somewhere
end
end
end
I used delete so the session will be cleared of these values.
UPDATE: since you're using a session controller
class SessionsController < ApplicationController
def create
if user = User.from_omniauth(env["omniauth.auth"])
session[:user_id] = user.id
if session[:school] && session[:review] && session[:rating] # or just 1 check
review = Review.new
review.content = session.delete(:review)
review.school_id = session.delete(:school)
review.user_id = user.id
review.rating = session.delete(:rating)
review.save
end
end
redirect_to :back
end
Edit: Hmm this is interested. I just noticed, my signup route is /signup. But, once I submit the information and the form returns invalid, I'm in the route /users .
I'm building a simple app to learn rails, and I've learned to set up an authentication system.
Today, I added a new plans table, to make different subscriptions for users. The Plan model has_many users, and users belong to plans. After implementing this, I see that if I enter invalid information, error messages do not show up in the view anymore.
I have the following code in the application.html.erb file for it show up -- >
<% flash.each do |key, value| %>
<div class="alert alert-<%= key %>"><%= value %></div>
<% end %>
It works fine on other notices, but it isn't showing the error messages.
One thing to also note, is that if I enter an email incorrectly for example, Rails shows me that it was the problem by highlighting it in red(edited CSS previously to do that). But, the error messages themeselves are nowhere to be found :P
Here's my Users controller :
class UsersController < ApplicationController
before_filter :signed_in_user, only: [:show]
before_filter :correct_user, only: [:show]
def show
#user = User.find(params[:id])
end
def new
plan = Plan.find(params[:plan_id])
#user = plan.users.build
end
def create
#user = User.new(params[:user])
if #user.save
sign_in #user
flash[:success] = "Welcome to the Sample App!"
redirect_to #user
else
render 'new'
end
end
def index
if current_user
redirect_to(user_path(current_user))
else
redirect_to(root_path)
end
end
private
def signed_in_user
unless signed_in?
store_location
redirect_to login_url, notice: "Please sign in."
end
end
def correct_user
#user = User.find(params[:id])
redirect_to(root_path) unless current_user?(#user)
end
end
I did some research, and people have been saying it might because of redirects, but I'm not sure If I'm doing that.
When first visiting the signup though, it's in the form of /signup?plan_id=1, to populate a hidden field with the plan_id in the signup form. When it shows the error screen, the plan_id is no longer there, which I assumed is okay since it already POSTed it. Does that have anything to do with it?
I think the problem is that the error messages you're expecting to see are errors on the User object, not stored in the flash. Try this in your view:
<% #user.errors.full_messages.each do |error_message| %>
<div class="alert"><%= error_message %></div>
<% end %>
See the docs for ActiveModel::Errors for more info.