← Back to Upcase

default_scope and client_side_validations gem - SOLVED


(Dan Weaver) #1

I’m using the Client_Side_Validations gem on my sign up form. It’s a mutli-tenant app using default_scope to set the tenant for applicable models.

There is no tenant set on the sign up page so I’m running into an issue where the gem includes the default_scope when running a uniqueness validation.

When the uniqueness validator runs on @user.email the default_scope prevents the validation from running properly (because company_id is nil) and therefore always looks like a unique result.

My user model:

# user.rb
...
default_scope { where(company_id: Company.current_id) }
...

This query is run when validating email:

Started GET "/validators/uniqueness?case_sensitive=true& \
user%5Bemail%5D=me%2B40%40example.com&_=1423897339854" \
for 76.175.168.2 at 2015-02-14 07:02:30 +0000                        
  User Exists (0.4ms)  SELECT 1 AS one FROM "users" WHERE \
  "users"."company_id" IS NULL AND "users"."email" = 'me@example.com' LIMIT 1

When I remove the default_scope from my user model I get the correct result and proper validation:

Started GET "/validators/uniqueness?case_sensitive=true& \
user%5Bemail%5D=me%2B40%40example.com&_=1423897339854" \
for 76.175.168.2 at 2015-02-14 07:02:30 +0000
  User Exists (0.4ms)  SELECT 1 AS one FROM "users" WHERE \
  "users"."email" = 'me@example.com' LIMIT 1

I’m not sure if it’s intended behaviour or not. It seems to me that it shouldn’t use default_scope in a uniqueness validation.

What’s the most practical way for me to ignore the default_scope when this gem runs the email validator?


(Tom Ridge) #2

@weavermedia I would suggest this is an issue with the gem, I did some digging around and found a similar issue with mongoid. In a nutshell unscoped needed to be applied to the validation method when called and wasn’t previously.

I had a quick look through the client side validations gem this morning and couldn’t immediately identify the area for change sorry, I saw your ticket on the gh issues so will see if I can try and track the area down, but agree with the author that trying to pass in the scope may be useful.


(Dan Weaver) #3

Thanks for your input @Tom_Ridge, I’m looking through now to see if I can find where the query is built. My gem debugging skills are sadly lacking. This seems like a perfect opportunity to improve them.

I can’t figure out how to use scope: in the form. I don’t see any change in the query that is run, no matter whether I pass in a named scope defined on the model or an attribute of the model.

At least I’ve managed to rule out Devise involvement. I’m using this on a user but even when I test on a non-user model I get the same results.

I’ll keep digging.


(Dan Weaver) #4

My only idea is that unscoped should be included here:

# lib/client_side_validations/active_record/middleware.rb

8  def self.is_unique?(klass, attribute, value, params)
...
41   !klass.where(relation).exists?
42 end

# should this be > !klass.unscoped.where(relation).exists?

Unfortunately I don’t yet know how to hack a gem and try out my own version of it. I just spent an hour trying to fork, edit and install my own modification but to no avail. Something else to learn :slight_smile:


(Tom Ridge) #5

@weavermedia So I had another dive into the source, and unfortunately came up a little empty, if you add a default scope of active: true to the user.rb test class, you’ll be able to replicate the same behaviour that you’re currently seeing.

Unfortunately I’m pretty rubbish with the arel aspects of the code in order to properly diagnose where to call unscoped or even if thats the right choice here. Hopefully someone else can help out

I should also add that the tests are a little flaky, when I add the default scope, it goes from either 5 tests failing to 3, intermittently.

So maybe I might turn this on it’s head a bit, why do you need a default scope?


(Dan Weaver) #6

Hey @Tom_Ridge, I use default_scope to restrict the data my customers see, based on a Comapny.current_id which is set in an around_filter in application_controller. It’s the method outlined by Ryan Bates in his Railscasts episode.

# application_controller.rb
around_filter :scope_current_company

def scope_current_company
  Company.current_id = current_user.company_id if signed_in?
  yield
ensure
  Company.current_id = nil
end
# employee.rb
default_scope { where(company_id: Company.current_id) }

I chose this method over PostgreSQL schemas because (1) it was easier for me to implement at the time since I had just started using Rails and (2) I read a lot of talk about performance issues with PostgreSQL schema-based mutli-tenancy (which I have actually since discounted).

It’s working well for me and apart from scoping all find calls to the current company it also automatically sets the comapny_id for all new records too.

I have faced issues using this method when there’s no tenant set, i.e. on the landing page/sign up page and in the console, but on the whole I’ve solved most of those. Testing can be a little cumbersome at times but I’m working on smoothing that out.


(Dan Weaver) #7

To wrap this up - I monkey patched the gem with unscoped as outlined above and it works.

Remote validations on field uniqueness now ignore default_scope.


(Tom Ridge) #8

@weavermedia did you manage to get the specs passing? Id love to take a look at the code if you don’t mind? Was scratching my head on how to TDD it last night.


(Dan Weaver) #9

No, I’m afraid I’m flying by the seat of my pants. I found what I thought looked like the likely place in the code:

# lib/client_side_validations/active_record/middleware.rb

8  def self.is_unique?(klass, attribute, value, params)
...
41   !klass.where(relation).exists?
42 end

and noted that it didn’t include .unscoped when it ran the query.

As far as I know ActiveRecord includes .unscoped when building a query for a uniqueness validation:

# rails-master/activerecord/lib/active_record/validations/uniqueness.rb

def build_relation(klass, table, attribute, value)
  ...
  klass.unscoped.where(comparison)
end

so I monkey patched the gem directly in my app and it works fine in that limited environment.

I haven’t forked the gem, done any tests or created a pull request with the change. That’s a little beyond my skills at the moment.