Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add email_scope option for email uniqueness validator #5094

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/devise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ module Test
mattr_accessor :email_regexp
@@email_regexp = /\A[^@\s]+@[^@\s]+\z/

# Option to scope the email uniqueness validator
mattr_accessor :email_scope
@@email_scope = nil

# Range validation for password length
mattr_accessor :password_length
@@password_length = 6..128
Expand Down
7 changes: 4 additions & 3 deletions lib/devise/models/validatable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module Models
# Validatable adds the following options to devise_for:
#
# * +email_regexp+: the regular expression used to validate e-mails;
# * +email_scope+: option to scope the email uniqueness validator;
# * +password_length+: a range expressing password length. Defaults to 6..128.
#
module Validatable
Expand All @@ -30,10 +31,10 @@ def self.included(base)
base.class_eval do
validates_presence_of :email, if: :email_required?
if Devise.activerecord51?
validates_uniqueness_of :email, allow_blank: true, case_sensitive: true, if: :will_save_change_to_email?
validates_uniqueness_of :email, allow_blank: true, case_sensitive: true, if: :will_save_change_to_email?, scope: email_scope
validates_format_of :email, with: email_regexp, allow_blank: true, if: :will_save_change_to_email?
else
validates_uniqueness_of :email, allow_blank: true, if: :email_changed?
validates_uniqueness_of :email, allow_blank: true, if: :email_changed?, scope: email_scope
validates_format_of :email, with: email_regexp, allow_blank: true, if: :email_changed?
end

Expand Down Expand Up @@ -66,7 +67,7 @@ def email_required?
end

module ClassMethods
Devise::Models.config(self, :email_regexp, :password_length)
Devise::Models.config(self, :email_regexp, :password_length, :email_scope)
end
end
end
Expand Down
3 changes: 3 additions & 0 deletions lib/generators/templates/devise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,9 @@
# to give user feedback and not to assert the e-mail validity.
config.email_regexp = /\A[^@\s]+@[^@\s]+\z/

# Option to scope the email uniqueness validator. Default is nil;
# config.email_scope = [:username]

# ==> Configuration for :timeoutable
# The time you want to timeout the user session without activity. After this
# time the user will be asked for credentials again. Default is 30 minutes.
Expand Down
6 changes: 6 additions & 0 deletions test/models/validatable_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ class ValidatableTest < ActiveSupport::TestCase
assert user.valid?
end

test 'should allow non-unique emails using email_scope attribute' do
existing_user = create_user_with_scope
user = new_user_with_scope(email: existing_user.email, username: "New username")
assert user.valid?
end

test 'should require correct email format if email has changed, allowing blank' do
user = new_user(email: '')
assert user.invalid?
Expand Down
10 changes: 10 additions & 0 deletions test/rails_app/app/active_record/user_with_scope.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

require 'shared_user_with_scope'

class UserWithScope < ActiveRecord::Base
self.table_name = 'users'
include Shim
include SharedUserWithScope
end

31 changes: 31 additions & 0 deletions test/rails_app/app/mongoid/user_with_scope.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

require "shared_user_with_scope"

class UserWithScope
include Mongoid::Document
include Shim
include SharedUserWithScope

field :username, type: String

## Database authenticatable
field :email, type: String, default: ""
field :encrypted_password, type: String, default: ""

## Recoverable
field :reset_password_token, type: String
field :reset_password_sent_at, type: Time

## Rememberable
field :remember_created_at, type: Time

## Trackable
field :sign_in_count, type: Integer, default: 0
field :current_sign_in_at, type: Time
field :last_sign_in_at, type: Time
field :current_sign_in_ip, type: String
field :last_sign_in_ip, type: String

validates :email, presence: true
end
3 changes: 3 additions & 0 deletions test/rails_app/config/initializers/devise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@
# Regex to use to validate the email address
# config.email_regexp = /^([\w\.%\+\-]+)@([\w\-]+\.)+([\w]{2,})$/i

# Option to scope the email uniqueness validator. Default is nil;
# config.email_scope = [:subdomain]

# ==> Configuration for :timeoutable
# The time you want to timeout the user session without activity. After this
# time the user will be asked for credentials again. Default is 30 minutes.
Expand Down
11 changes: 11 additions & 0 deletions test/rails_app/lib/shared_user_with_scope.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

module SharedUserWithScope
extend ActiveSupport::Concern

included do
devise :database_authenticatable, :lockable, :recoverable,
:registerable, :rememberable, :timeoutable,
:trackable, :validatable, email_scope: [:username]
end
end
8 changes: 8 additions & 0 deletions test/support/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ def new_user(attributes = {})
User.new(valid_attributes(attributes))
end

def new_user_with_scope(attributes = {})
UserWithScope.new(valid_attributes(attributes))
end

def create_user(attributes = {})
User.create!(valid_attributes(attributes))
end
Expand All @@ -56,6 +60,10 @@ def create_user_with_validations(attributes = {})
UserWithValidations.create!(valid_attributes(attributes))
end

def create_user_with_scope(attributes={})
UserWithScope.create!(valid_attributes(attributes))
end

# Execute the block setting the given values and restoring old values after
# the block is executed.
def swap(object, new_values)
Expand Down