I have a custom Class in my Rails application, which validates a bunch of settings.
The code:
class UserSettingObject < RailsSettings::SettingObject
validate do
if !/^([0-9]|0[0-9]|1[0]):[0-5][0-9]$/.match(self.time)
redirect_to settings_path, notice: 'Invalid time format'
end
end
end
I check SO posts and found a similar problem (here), the recommendation was include ActionController::Redirecting but it doesn't works, resulting in undefined method `config_accessor' error.
How to use Rails redirects methods in a custom Class?
In addition to what #SergioTulentsev already said, validations are for validating, not for taking actions. What you could do instead is leaving the regexp as a method, and in controller check the time using it and redirect based on the result of the validation.
Short answer is: you can't do that.
redirect_to only makes sense in request context (read: when being called from within controller action). You can't redirect from random objects, because they don't know what request to manipulate.
And yes, what #AndreyDeineko says.
Related
I try to stick to principles of thin controller and thin model. And I believe that business logic related code should be in the service classes.
I write a Rails back-end application accepting JSON requests. And I need to validate, that the one parameter is present. Let's assume that I have:
class UserController
def change_status
user = User.find(params[:id])
render json: UserStatusChanger.new(user, params[:status]).perform!
end
end
class UserStatusChanger
attr_reader :user, :status
def initialize(user, status)
#user = user
#status = status
end
def perform!
# complex logic here
{result: 'ok'}
end
end
And now let's suppose that I need to receive non-blank params[:status]. Sure, in the real world it is much more complex with more parameters. :)
My question is: Where should I put validation of params[:status]?
My thoughts are:
If I put it in the controller, I need an integration test to test the validation. But there are some good looking solutions, as rails_params gem. But I can face with problem of big controller method, having many validations. And also in the unit test my service will work wrong if some input parameter is nil without validation before performing complex logic.
If I put is in the service, the test will be more lightweight. But I should catch exceptions via ApplicationController#rescue_from and this will be not tested.
I think, that you should do it in controller. As for me, i usually do it in before_action method. According to MVC pattern, all params and routing logic must be in controller.
I use devise_invitable in my app to allow users to send invitations. I realized a bad case in which a user has been invited but ignores the invitation and later returns to the app to sign up on their own. Because devise_invitable handles invitations by creating a new user using the provided email address for the invitation, my uniqueness validation on the email field will cause Rails to complain, telling the user that the email address is already taken.
I'm trying to write some logic to handle this case. I see two paths - either figure a way to detect this and destroy the previously created user and allow the new one to be created, or detect the user was invited and execute another flow. I've decided to implement the second option, as I'd like to still utilize the invitation if possible.
My limited experience has me questioning if what I've written will work, but I can't actually fully test it because the Rails validation on the email is triggered. I've made sure Devise's :validatable module is inactive. I created a method that (I think) will detect if a user was invited and in that case the uniqueness validation should be skipped.
#user.rb
...
validates :email, uniqueness: true, unless: :was_invited?
...
def was_invited?
if self.invitation_sent_at.present? && self.sign_in_count == 0
true
else
false
end
end
FWIW, I had originally written this in shorthand rather than breaking out the if/else, but I wanted to be very explicit in an effort to find the bug/failure.
The hope is that once the form passes validation, the create action will do some detection about a user's invitation status and, if they were invited, redirect them to the accept_user_invitation_path. Again, I haven't been able to actually test this yet because I can't get around the validations.
#registrations_controller.rb
def create
if User.find_by_email(params[:email])
#existing_user = User.find_by_email(params[:email])
#existing_user.save(validate: false)
if #existing_user.was_invited?
redirect_to accept_user_invitation_path(:invitation_token => #existing_user.invitation_token)
end
else
super
end
end
In a desperate effort, you'll see I've also added the .save(validate: false) to try to short circuit it there, but it's not even getting that far.
If I comment out the email validation entirely, simply to test the rest of the logic/flow, I get a PG error complaining on uniqueness because of an index on the email address - I don't want to tear all this apart simply to test this method.
I've tried to mess with this for hours and I'm at a loss - any help is appreciated. Let me know if there's any other code you want to see.
Looking at the redirect:
redirect_to accept_user_invitation_path(:invitation_token => #existing_user.invitation_token)
I can see that there is no return which should mean that if that redirect was being called you should be getting an AbstractController::DoubleRenderError error as the parent controller's create method should be trying to render the new view.
From this I would guess that the query you are using to find the existing user is not actually returning a result, possibly because you are using params[:email] whereas if you are using the default views or a properly formatted form it should be params[:user][:email].
Maybe you should give more responsibilities to your controller...
If you find the user, use that, else create a new one. Assuming your form appears with http://yourapp/users/new, change it in your routes to http://yourapp/users/new/:email, making the user input their email before advancing to the form.
def new
#existing_user = User.find_by_email("#{params[:email]}.#{params[:format]}") || User.new
if #existing_user.was_invited? # will only work for existing user
redirect_to accept_user_invitation_path(:invitation_token => #existing_user.invitation_token)
else
render 'new'
end
end
def create
# do maybe something before saving
if #existing_user.save(user_params)
# do your magic
else
render 'new', notice: "Oops, I didn't save"
end
end
I have a form that allows a user to update their profile information, but I would like to prevent some information from being changed. I also would like to keep my controller code very simple. In the update action of my Users Controller, I have the following code:
def update
#user = Users.find params[:id]
if #user.update_attributes(params[:user])
flash[:notice] = 'Update successful.'
redirect_to user_path(#user)
else
render :action => :edit
end
end
This is very clean and simple, and I like that. What I don't like, however, is that a user can add a field to the form, with the same name as an attribute, and use it to modify forbidden attributes. Is there a simple way to do this, or do I need to devise a way to do this myself?
One method I was considering was to generate a hash value, using a hash-based message authentication code, of all the form's element names. This message access code would be a hidden value in the form. Then, once the form is submitted, I would calculate the message access code (MAC) again using the names of the parameter Hash's keys. If the two MACs are different, or if the first MAC is missing from the parameter Hash, I would throw an error. I would rather not spend the time implementing this if there was already and easy solution out there.
Thanks.
On your model you can use attr_protected or attr_accessible to blacklist or whitelist attributes when being set via mass assignment (like when a form is submitted).
Rails will prevent mass assignment if you use attr_protected :protectedcolumn (blacklist) or attr_accessible :safecolumn (whitelist) within your model. More information on this topic can be found in the Ruby on Rails Security Guide (Section 6.1)
I'm working on a JSON-based API for my Rails 3.1 app. I'd like to provide a custom failure response instead of the default, which is:
{"error":"You need to sign in or sign up before continuing."}
My API controller includes a before_filter call to authenticate_user!, which is what is rendering this JSON response.
While searching, I came across this StackOverflow question, which references this Devise wiki entry. Unfortunately, the wiki entry isn't verbose enough for me to understand what it's telling me. Specifically, I have no clue where I'm supposed to put that code such that Devise/Warden knows to render what I want returned.
From the comments on the other SA question, it sounds like I don't need to call custom_failure! since I'm using a version of Devise above 1.2 (1.4.2 to be specific). However, the wiki entry doesn't explain where the render call should go such that authenticate_user! knows to use that instead of its own render call.
Where does this render call go?
Edit: I'm not just trying to change the message itself (a la the devise en.yml config); I'm trying to change the actual format of the response. Specifically, I want to return this:
render :text => "You must be logged in to do that.", :status => :unauthorized
For reference in case anyone else stumbles upon this question when looking for how to customize the json error response when a failed login attempt is made using Devise, the key is to use your own custom FailureApp implementation. (You can also use this approach to override some redirect behavior.)
class CustomFailureApp < Devise::FailureApp
def respond
if request.format == :json
json_error_response
else
super
end
end
def json_error_response
self.status = 401
self.content_type = "application/json"
self.response_body = [ { message: i18n_message } ].to_json
end
end
and in your devise.rb, look for the config.warden section:
config.warden do |manager|
manager.failure_app = CustomFailureApp
end
Some related info:
At first I thought I would have to override Devise::SessionsController, possibly using the recall option passed to warden.authenticate!, but as mentioned here, "recall is not invoked for API requests, only for navigational ones. If you want to customise the http status code, you will have better luck doing so at the failure app level."
Also https://github.com/plataformatec/devise/wiki/How-To%3a-Redirect-to-a-specific-page-when-the-user-can-not-be-authenticated shows something very similar for redirection.
If you're simply wanting to change the text displayed with the error message, I believe you can just edit the locale file (/config/locales/devise.en.yml).
The RailsCast on this topic might be helpful too, if you want more specific details. You can find it at http://railscasts.com/episodes/210-customizing-devise
I have a permission model in my app, that ties (Users, Roles, Projects) together.
What I'm looking to learn how to do is prevent a user for removing himself for their project...
Can you give me feedback on the following?
class Permission < ActiveRecord::Base
.
.
.
#admin_lock makes sure the user who created the project, is always the admin
before_save :admin_lock
def before_save
#Get the Project Object
project = Find(self.project_id)
if project.creator_id == current_user.id
# SOME HOW ABORT OR SEND BACK Not Allowed?
else
#continue, do nothing
end
end
end
Is that look like the right approach?
Also, I'm not sure how to do the following two things above:
How to abort prevent the save, and send back an error msg?
Get the devise, current_user.id in the model, that doesn't seem possible, so how do Rails gurus do stuff like the above?
Thanks for reading through
How to abort prevent the save, and send back an error msg?
return false during the callback chain tells activemodel to stop (similar to how adding errors to the model during a validation tells it to stop at that point)
self.errors.add_to_base "msg" will add an error to the model, which can then be rendered on the view.
Get the devise, current_user.id in the model, that doesn't seem possible, so how do Rails gurus do stuff like the above?
Models shouldn't really know about things like the current request, if at all possible, you should be locking things down at the controller/action level.
EDIT:
So, the role of controllers is to deal with everything involved in getting the correct information together based on the request, and passing it to the view (which becomes the response). People often say "make your models fat and your controllers skinny", but that could be said of any system that embraces object oriented design -- your logic should be in objects when possible.
That being said, the whole point of controllers is to deal with routing the right things to the right places, and authentication is definitely a concern of routing.
You could easily move the line comparing creator_id to user id in the action, and react based on that.
Now, sometimes you genuinely need that stuff in the model and there is no way around it. That becomes a problem, because you need to fight rails to get it there. One way would be to attr_accessor a current_user field on your model, and pass that in on initialize. Another would be to remove the fields from the params hash that a user is not allowed to change in the action. Neither is really that nice though.
Agreed with Matt that you should try to use the controller for the redirect. The model should have the logic to determine if the redirect is appropriate. Maybe something like
class ProjectsController < ApplicationController
def update
redirect_to(projects_url, :alert => "You can't remove yourself from this project.") and return if Role.unauthorized_action?(:update, params[:project])
#project = Project.find(params[:id])
if #project.update_attributes(params[:project])
...
end
class Role
def self.unauthorized_action?(action, params)
# your logic here
end
You should check out CanCan for some ideas.
In permission model take one field project_creater as boolean
In project modelbefore_create :set_project_ownership
def set_project_ownership
self.permissions.build(user_id: User.current.id, project_creater: true)
end
In project controllerbefore_filter :set_current_user
In Application controllerdef set_current_user
User.current = current_user
end