Rails custom validation for specific value - ruby-on-rails

I have a user signup form where I am trying to restrict the ability to create an account to users who have a specific access code. I'm running into a problem with being able to validate for that specific code. I've included the code that I've been able to piece together, but isn't working for me.
#new.html.erb
<%= f.text_field :access, placeholder: "Access Code", required: "required" %>
#user.rb
validate :check_code
def check_code
errors.add(:base, 'Try another code') unless User.where(:access == "theAccessCode")
end
Can someone suggest an alternative to this validation?

If there's a single correct access code, I think you want something like this instead:
ACCESS_CODE = 'secretCode'
def check_code
errors.add(:base, 'Try another code') unless self.access == ACCESS_CODE
end
Alternatively, your code would probably need to look like this to work (although querying for an existing user still seems wrong):
def check_code
errors.add(:base, 'Try another code') unless User.where(:access => "theAccessCode").exists?
end

Related

Rails validation is still firing despite unless option evaluating to true

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

How do I refactor this case statement to work?

So what I would like to do is to do redirects based on the role of the current_user.
This is what I have:
path = case current_user.roles.where(:name => "vendor")
when :vendor
dashboard_path
when :guest
home_path
else
home_path
end
redirect_to path
I am using cancan and the only way to figure out the role of a user, that I know of, is to either do current_user.has_role? :admin or current_user.roles.where(:name => role_name).
Given those constraints (or tell me another way to figure out the role of a user) how do I get this case statement to work?
Edit 1
Assume that I am checking for multiple roles, not just the 2 I have here - could be 4 or 5.
Edit 2
To be clear, this is my current setup.
I am using Devise, CanCan & Rolify. Rolify allows a user to have multiple roles, but my application won't have that use case. A user will just have one role. They can either be a vendor, buyer, guest, superadmin.
If they are a vendor, they can only see the dashboard_path that belongs to them. They can't see any other vendor storefront that belongs to anyone else. They also should not be able to see products from other vendors. So, once they login, their root_path should be dashboard_path not home_path which is what every other role's root_path will be.
If they are a guest, they can see everything except the prices - I already have this logic working. I achieved this like this:
if user.has_role? :guest
can :read, [Product, Vendor, Banner]
cannot :view_prices, Product
end
Then in my view, I just did something like this:
<% if can? :view_prices, Product %>
<div class="price pull-right">
<%= number_to_currency(#product.price) %> ea
</div>
<% else %>
<span class="no-price pull-right"><%= link_to "Log In To See Price", login_path %></span>
<% end %>
So, basically...my real goal is to try and change the root_path depending on the role the user has. I am basically trying to implement the answer on this question.
FINAL ANSWER (for earlier anwsers, see below)
If your users can only have one role, I'd say your current implementation is not exactly appropriate. However, if you really need to keep this implementation, you can do something like this :
class User < ActiveRecord::Base
# this will return the name of the first (so the only one)
# role that your user has, or nil.
def role_name
roles.first.try( :name )
end
end
so now your case statement would work :
path = case current_user.role_name
when 'vendor' ; dashboard_path
when 'guest' ; home_path
when 'foo' ; bar_path
else home_path
end
I still encourage you to wrap your case statement in a helper for reusability and easier maintainance.
EARLIER ANSWER
I'm not sure i understand your question, but i think you don't need a case statement here :
redirect_to (current_user.has_role? :vendor ? dashboard_path : home_path)
another way is to push part of the responsability to the user class (or a presenter):
class User < ActiveRecord::Base
def home_page
# here some logic to return a symbol like :home or :dashboard,
# depending on your roles implementation. Your User could even use
# a state machine to do this, or have multiple decorators.
end
end
and then with a helper
def home_path_for( user )
case user.home_page
when :home ; home_path
when :dashboard ; dashboard_path
when :foo ; bar_path
else home_path
end
end
FIRST EDIT
if your user can have multiple roles at a time, i'd say a case statement is not appropriate. case is a branching statement that is appropriate when you only have one and only one input and one and only one outcome out of a set of possible outcomes.
So you have to reduce your list of roles to an intermediate state, for instance :
DASHBOARD_ROLES = [:vendor, :admin]
CLIENT_ROLES = [:client, :prospect]
BASIC_ROLES = [:logged_visitor]
if (DASHBOARD_ROLES & user_roles).any?
:admin_dashboard
else if (CLIENT_ROLES & user_roles).any?
:client_dashboard
# additionnal, different logic is possible
else if (BASIC_ROLES & user_roles).any? && user_logged_in? && user_roles.first != :prospect
:logged_dashboard
else
:home
end
this is a completely different kind of logic.
First of all, you probably need to address the case when user has multiple roles, if that is possible.
Assuming a user has one role (though we can add more conditions if need be) Can you consider a hash?
Something like -
path = {:vendor => dashboard_path, :guest => home_path} [current_user.active_role] || default_path
I have made some assumptions -
current_user.active_role could be the current role of the user based on which you can redirect the answer.
default_path is self explanatory.

Get a list of the five last logins for x user

So I've got this task where I'm supposed to output a list of the last five logins by the currently logged in user.
So like this:
Person A logs in at 12.00 12/12/12.
Person A logs in again at 12.00 12/01/13.
And so on.
This needs to work up to a list of five, once that limit is reached the first login (12.00 12/12/12 in this case) is to be taken out of view.
What I've got so far is a Login system with remember tokens and some simple authorization for the login itself (doing it from scratch and going back to some details fom Michael Hartl's tutorial). Right now, I can list the current time that the user logged in, but not the previous 'versions'.
So, this is my view:
<% if signed_in? %>
<h1>Welcome, <%= current_user.name %>!</h1>
<p>You were last logged in on:</p>
<ul>
<li><%= current_user.lastlogin %></li>
</ul>
<%= link_to "Sign out", signout_path, method: "delete", class: "btn btn-large" %>
<% else %>
... (state for non logged in users)
<% end %>
This is my user class:
class User < ActiveRecord::Base
attr_accessible :name, :email, :password, :password_confirmation, :lastlogin
has_secure_password
before_save { |user| user.email = email.downcase }
before_save :create_remember_token
...(validation following this)
And this is my sessions_controller
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by_email(params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
sign_in user
redirect_to root_path
else
flash.now[:error] = 'Invalid email/password combination'
render 'new'
end
end
And here we have the sign_in function
module SessionsHelper
def sign_in(user)
# user.update_attribute(:lastlogin, Time.now)
user.touch(:lastlogin)
cookies.permanent[:remember_token] = user.remember_token
self.current_user = user
end
end
I'm looking for ideas on how to tackle this problem.
Another thing that I've forgotten to mention is that the view layer needs to be modified to some extent so as to loop through the five login times and output them in order, as opposed to hard coding in five list items.
One thing that I'm thinking of is setting up a relationship between a table with five fields corresponding to one user. And then upon sign in (or sign out) take the :lastlogin value and push it into a field in the table.
Then when all fields have been filled, look at the first date and replace it with the new.
So what I'm thinking is something like
def sign_in
user.touch(:lastlogin)
# push :lastlogin value into field in LoginsTable
cookies.permanent[:remember_token] = user.remember_token
self.current_user = user
end
I am quite new to Ruby on Rails and might be way off here. I've tried searching for functionality like the one I'm explaining here, and all I've gotten is various plugins/full-out gems that handle this and everything else when it comes to Authentication and so forth, which isn't really 'needed' in this case as it's just a small time project for myself.
Cheers!
Edit:
The futile attempt at pushing into the user_logins table.
module SessionsHelper
def sign_in(user)
user.update_attribute(:lastlogin, Time.now)
cookies.permanent[:remember_token] = user.remember_token
self.current_user = user
login_time = UserLogins.create(:login_time Time.now, :user_id current_user)
end
end
Now this is obviously not working. I'm not too sure on exactly how to loop through the login_times either in the view layer. But I reckon I'll get it working eventually, now that you put me on the right track :)
You'll get yourself into a messy pickle if you hardcode 5 fields to hold the last 5 login dates, instead consider having a table to store login history for all users. A simple table "user_logins" with id, user_id and login_time would suffice.
Write to this table every time the user logs in, then in your view just select the top 5 results ordered by login_time, descending.
Hope that makes sense!
on this line
login_time = UserLogins.create(:login_time Time.now, :user_id current_user)
You're creating the hash object wrong. Ruby 1.9 hash syntax is
some_var: "some value"
not
:some_var "some value"
Good luck!
Also decompose that UserLogins object a little. Since you're using Time.now have the UserLogins object set the time which reduces your method to a monodic one.
Side notes:
You shouldn't name a Model object plural as that'll get confusing in Rails.
I prefer to use devise gem as it maintains authuntication and also history of user logins So you can easily get Last five login user applying query on user model and order desc by last login

rails 3: How would I write a link_to for a ActionMailer to a just created object

I have Recommendations has_many Approvals.
When one approval is made, the user provides an email address for the next user who needs to approve.
In my Approval Model
after_save :create_next_approval, :approval_notification
attr_accessor :next_approver_email
def recently_approved?
self.approved_changed? && self.approved?
end
def create_next_approval
#self.recommendations.create :email => self.next_approver_email if next_approver_email.present? && recently_approved?
next_approval = self.recommendation.approvals.build(:email => self.next_approver_email)
next_approval.save if next_approver_email.present? && recently_approved?
end
private
def approval_notification
ApprovalMailer.needs_approval(self).deliver
end
In the create_next_approval method, I am saving the next_approval. I am then sending an email to the next_approver_email address asking them to come approve the recommendation.
I am saving this approval here and I need to link to it in the email being sent out... any ideas?
If I follow you correctly. You need to link to the Approval that you just saved. So this would be self.
In that case something like this would work in your email if you have normal routes setup: <%= link_to "approval link", approval_path(self) %>
Let me know if I'm following correctly.

Rails form validation

I have a Rails app that lets a user construct a database query by filling out an extensive form. I wondered the best practice for checking form parameters in Rails. Previously, I have had my results method (the one to which the form submits) do the following:
if params[:name] && !params[:name].blank?
#name = params[:name]
else
flash[:error] = 'You must give a name'
redirect_to :action => 'index'
return
end
But for several form fields, seeing this repeated for each one got tiresome. I couldn't just stick them all in some loop to check for each field, because the fields are set up differently:
a single key: params[:name]
a key and a sub-key: params[:image][:font_size]
only expect some form fields to be filled out if another field was set
Etc. This was also repetitive, because I was setting flash[:error] for each missing/invalid parameter, and redirecting to the same URL for each one. I switched to using a before_filter that checks for all necessary form parameters and only returns true if everything's okay. Then the my results method continues, and variables are just assigned flat-out, with no checking involved:
#name = params[:name]
In my validate_form method, I have sections of code like the following:
if (
params[:analysis_type][:to_s] == 'development' ||
params[:results_to_generate].include?('graph')
)
{:graph_type => :to_s, :graph_width => :to_s,
:theme => :to_s}.each do |key, sub_key|
unless params[key] && params[key][sub_key]
flash[:error] = "Cannot leave '#{Inflector.humanize(key)}' blank"
redirect_to(url)
return false
end
end
end
I was just wondering if I'm going about this in the best way, or if I'm missing something obvious when it comes to parameter validation. I worry this is still not the most efficient technique, because I have several blocks where I assign a value to flash[:error], then redirect to the same URL, then return false.
Edit to clarify: The reason I don't have this validation in model(s) currently is for two reasons:
I'm not trying to gather data from the user in order to create or update a row in the database. None of the data the user submits is saved after they log out. It's all used right when they submit it to search the database and generate some stuff.
The query form takes in data pertaining to several models, and it takes in other data that doesn't pertain to a model at all. E.g. graph type and theme as shown above do not connect to any model, they just convey information about how the user wants to display his results.
Edit to show improved technique: I make use of application-specific exceptions now, thanks to Jamis Buck's Raising the Right Exception article. For example:
def results
if params[:name] && !params[:name].blank?
#name = params[:name]
else
raise MyApp::MissingFieldError
end
if params[:age] && !params[:age].blank? && params[:age].numeric?
#age = params[:age].to_i
else
raise MyApp::MissingFieldError
end
rescue MyApp::MissingFieldError => err
flash[:error] = "Invalid form submission: #{err.clean_message}"
redirect_to :action => 'index'
end
You could try active_form (http://github.com/cs/active_form/tree/master/lib/active_form.rb) - just ActiveRecord minus the database stuff. This way you can use all of AR's validation stuff and treat your form like you would any other model.
class MyForm < ActiveForm
validates_presence_of :name
validates_presence_of :graph_size, :if => # ...blah blah
end
form = MyForm.new(params[:form])
form.validate
form.errors
Looks like you are doing the validation in the controller, try putting it in the model, it's better suited to that sort of thing.
If you were to tackle the problem again today, you could create a model for the query parameter set and use Rails' built in validations, Rails 3 makes this a lot easier with ActiveModel::Validations see this post.
Model
class Person
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Naming
attr_accessor :name
attr_accessor :email
validates_presence_of :name,:message => "Please Provide User Name"
validates_presence_of :email,:message => "Please Provide Email"
end
Note that you don't necessarily need to save/persist the model to validate.
Controller
#person.name= params["name"]
#person.email= params["email"]
#person.valid?
One you called .valid? method on the model, the errors will be populated inside #person object. Hence,
View
<%if #person.errors.any? %>
<%#person.errors.messages.each do|msg| %>
<div class="alert alert-danger">
<%=msg[0][1]%>
</div>
<%end%>
<%end%>

Resources