required_scopes 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4d5c01aa1fe08e48a58a5d0b04403fdb1776cde9
4
+ data.tar.gz: a5e438c3698e6def089b4d451bb4dc2651c00ad3
5
+ SHA512:
6
+ metadata.gz: 5dc8c8bc939f6932c84408f7dfcc4a5bcc811373a110919818dc3a0f94723b629cdf35913b7137997277dd6b959ed0affdc5345fecbaae85b3198d1bf6a0bf76
7
+ data.tar.gz: cccbab92e292d84397b6f8e29c102d0e8e136cccac43ca7c83e893f14644faed1e0b72d577b15e9388825c9bdd2734fa2dde63d9e4592cf61761f0a07661f2c5
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ /spec_database_config.rb*
data/.travis.yml ADDED
@@ -0,0 +1,29 @@
1
+ before_install:
2
+ - gem install rubygems-update -v2.1.11
3
+ - gem update --system 2.1.11
4
+ - gem --version
5
+ rvm:
6
+ - "1.8.7"
7
+ - "1.9.3"
8
+ - "2.0.0"
9
+ - "jruby-1.7.6"
10
+ env:
11
+ - REQUIRED_SCOPES_AR_TEST_VERSION=3.2.16 REQUIRED_SCOPES_TRAVIS_CI_DATABASE_TYPE=mysql
12
+ - REQUIRED_SCOPES_AR_TEST_VERSION=3.2.16 REQUIRED_SCOPES_TRAVIS_CI_DATABASE_TYPE=postgres
13
+ - REQUIRED_SCOPES_AR_TEST_VERSION=3.2.16 REQUIRED_SCOPES_TRAVIS_CI_DATABASE_TYPE=sqlite
14
+ - REQUIRED_SCOPES_AR_TEST_VERSION=4.0.2 REQUIRED_SCOPES_TRAVIS_CI_DATABASE_TYPE=mysql
15
+ - REQUIRED_SCOPES_AR_TEST_VERSION=4.0.2 REQUIRED_SCOPES_TRAVIS_CI_DATABASE_TYPE=postgres
16
+ - REQUIRED_SCOPES_AR_TEST_VERSION=4.0.2 REQUIRED_SCOPES_TRAVIS_CI_DATABASE_TYPE=sqlite
17
+ before_script:
18
+ # - export JRUBY_OPTS="-J-Xmx256m -J-Xms256m $JRUBY_OPTS"
19
+ - mysql -e 'create database myapp_test;'
20
+ - psql -c 'create database myapp_test;' -U postgres
21
+ matrix:
22
+ exclude:
23
+ # ActiveRecord 4.x doesn't support Ruby 1.8.7
24
+ - rvm: 1.8.7
25
+ env: REQUIRED_SCOPES_AR_TEST_VERSION=4.0.2 REQUIRED_SCOPES_TRAVIS_CI_DATABASE_TYPE=mysql
26
+ - rvm: 1.8.7
27
+ env: REQUIRED_SCOPES_AR_TEST_VERSION=4.0.2 REQUIRED_SCOPES_TRAVIS_CI_DATABASE_TYPE=postgres
28
+ - rvm: 1.8.7
29
+ env: REQUIRED_SCOPES_AR_TEST_VERSION=4.0.2 REQUIRED_SCOPES_TRAVIS_CI_DATABASE_TYPE=sqlite
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in required_scopes.gemspec
4
+ gemspec
5
+
6
+ ar_version = ENV['REQUIRED_SCOPES_AR_TEST_VERSION']
7
+ ar_version = ar_version.strip if ar_version
8
+
9
+ version_spec = case ar_version
10
+ when nil then nil
11
+ when 'master' then { :git => 'git://github.com/rails/activerecord.git' }
12
+ else "=#{ar_version}"
13
+ end
14
+
15
+ if version_spec
16
+ gem("activerecord", version_spec)
17
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013-2014 Andrew Geweke
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,277 @@
1
+ # RequiredScopes
2
+
3
+ RequiredScopes keeps developers from being able to accidentally forget about critical scopes on database tables. For
4
+ example:
5
+
6
+ * If you provide software-as-a-service to many clients, forgetting to include the client ID in your query may leak
7
+ data from one client to another — potentially a truly disastrous thing to happen.
8
+ * If you store time-series data in a table that gets very large, querying without including a time range can cause
9
+ huge scalability problems as you accidentally scan the entire table.
10
+ * If you associate permissions with various records in a table, then forgetting to constrain on the permissions in a
11
+ query results in them being completely ineffective.
12
+ * If you soft-delete records (via a `deleted_at` or `deleted` flag), forgetting to either explicitly include or exclude
13
+ deleted records can result in "deleted" data reappearing, which can be very bad.
14
+
15
+ ...and the list goes on.
16
+
17
+ RequiredScopes works by letting you create one or more _required scope categories_, each named via a symbol
18
+ (_e.g._, `:client_id`, `:time_range`, `:permissions`, or `:deleted`). You can then declare scopes (or class methods)
19
+ as _satisfying_ one or more categories. When time comes to query the database, at least one scope satisfying each
20
+ category must have been used, or else an exception will be raised.
21
+
22
+ For example:
23
+
24
+ class StatusUpdate < ActiveRecord::Base
25
+ must_scope_by :client_id, :recency
26
+
27
+ scope :last_week, lambda { where("created_at >= ?", 1.week.ago) }, :satisfies => :recency
28
+ scope :last_month, lambda { where("created_at >= ?", 1.month.ago) }, :satisfies => :recency
29
+
30
+ class << self
31
+ def for_client(client_id)
32
+ where(:client_id => client_id).scope_category_satisfied(:client_id)
33
+ end
34
+ end
35
+ end
36
+
37
+ StatusUpdate.last(100) # => RequiredScopes::Errors::RequiredScopeCategoriesNotSatisfiedError
38
+ StatusUpdate.for_client(982).last_week # => [ <StatusUpdate id:321890414>, <StatusUpdate id:321890583>, ... ]
39
+
40
+ There's much more, too. For example, it's trivial to skip these checks if you want &mdash; the idea is to keep
41
+ developers from simply _forgetting_ about scoping, not to make their lives more difficult. See below, under **Usage**,
42
+ for more information.
43
+
44
+ #### As an alternative to `default_scope`
45
+
46
+ RequiredScopes was actually born as an alternative to `default_scope`. Rails' `default_scope` is a great idea, but, in
47
+ actual usage, we've seen it cause a surprisingly large number of bugs. (It works exactly the way it claims it does,
48
+ but it turns out this isn't actually a great way for it to work.)
49
+
50
+ What goes wrong? It turns out that there's almost never a single scope that you truly want applied 100%, or even 99%,
51
+ of the time. Instead, it's more like 85%; but, because it's the default, developers almost always completely forget
52
+ about it, and bugs result.
53
+
54
+ For example, take the classic case of a `deleted_at` column on a User model, and
55
+ `default_scope lambda { where('deleted_at IS NULL')} }`. This works great for most "normal" functions of the
56
+ application. But, then, the edge cases start creeping in:
57
+
58
+ * In your admin controllers, where you _want_ to be able to see deleted users, you keep forgetting to apply `#unscoped`
59
+ &mdash; and lots of errors on `nil` result.
60
+ * When a new user signs up and chooses a username, you forget to unscope when checking if an existing user has that
61
+ username, resulting in database unique-index failures on `users.username` when creating a new user (and HTTP 500
62
+ pages returned to the end user).
63
+ * Your "reset password" page almost certainly wants to find a user account even if it's deleted, and (at minimum)
64
+ display a message telling the user they have a deleted account. You don't want to act like the user simply doesn't
65
+ exist.
66
+
67
+ The truth is, there _isn't_ a single default scope that can ever be safely applied across-the-board. Developers have to
68
+ think about it, every single time. This isn't hard; it takes an extra second or two, and prevents hours of debugging
69
+ time (and lots of user frustration at bugs that would've resulted). But base Rails only lets you decide to either apply
70
+ a single `default_scope` across the board (where you run into the above problems) or not (where you run into even worse
71
+ ones, as developers completely forget about your `deleted_at` column, or whatever).
72
+
73
+ Hence, RequiredScopes. It prevents you from forgetting about critical scopes, yet doesn't try to shoehorn a single
74
+ `default_scope` everywhere.
75
+
76
+ #### Supported Versions
77
+
78
+ RequiredScopes supports:
79
+
80
+ * Ruby 1.8.7, 1.9.3, 2.0.0, 2.1.0, and JRuby 1.7.9.
81
+ * ActiveRecord 3.2.x and 4.0.x.
82
+ * Any database that works with ActiveRecord.
83
+
84
+ Note that because RequiredScopes ties in quite tightly with ActiveRecord, supporting previous ActiveRecord versions
85
+ would be significant work. Patches are always welcome. :-)
86
+
87
+ Current build status: ![Current Build Status](https://api.travis-ci.org/ageweke/required_scopes.png?branch=master)
88
+
89
+ ## Installation
90
+
91
+ Add this line to your application's Gemfile:
92
+
93
+ gem 'required_scopes'
94
+
95
+ And then execute:
96
+
97
+ $ bundle
98
+
99
+ Or install it yourself as:
100
+
101
+ $ gem install required_scopes
102
+
103
+ ## Usage
104
+
105
+ #### `base_scope_required!`, or, the simple version
106
+
107
+ An example:
108
+
109
+ class User < ActiveRecord::Base
110
+ base_scope_required!
111
+
112
+ # #base_scope is just like #scope, except that it says "this scope satisfies base_scope_required!"
113
+ base_scope :deleted, lambda { where("deleted_at IS NOT NULL") }
114
+ base_scope :not_deleted, lambda { where("deleted_at IS NULL") }
115
+
116
+ # #scope does not satisfy the requirement
117
+ scope :young, lambda { where("age <= 20") }
118
+
119
+ class << self
120
+ def deleted_recently
121
+ # The call to #base_scope_satisfied says "the scope I'm returning satisfies #base_scope_required!"
122
+ where("deleted_at >= ?", 1.week.ago).base_scope_satisfied
123
+ end
124
+ end
125
+ end
126
+
127
+ This sets up the following behavior:
128
+
129
+ # Forgetting the scope gives you an error
130
+ User.first # => RequiredScopes::Errors::BaseScopeNotSatisfiedError
131
+ User.young.first # => RequiredScopes::Errors::BaseScopeNotSatisfiedError
132
+
133
+ # Any of the declared scopes work just fine
134
+ User.deleted.first # => SELECT * FROM users WHERE deleted_at IS NOT NULL LIMIT 1
135
+ User.not_deleted.first # => SELECT * FROM users WHERE deleted_at IS NULL LIMIT 1
136
+ User.deleted_recently.first # => SELECT * FROM users WHERE deleted_at >= '2013-12-30 05:23:01.367705' LIMIT 1
137
+
138
+ # You can chain them like normal, and use them anywhere
139
+ User.where(:name => 'some user').not_deleted.first
140
+ # => SELECT * FROM users WHERE deleted_at IS NULL AND name = 'some user' LIMIT 1
141
+
142
+ # Pass them around, build on them...they work just like any other scope
143
+ s = User.where(:name => 'some user')
144
+ s.not_deleted.first
145
+ # => SELECT * FROM users WHERE deleted_at IS NULL AND name = 'some user' LIMIT 1
146
+ s.deleted.first
147
+ # => SELECT * FROM users WHERE deleted_at IS NOT NULL AND name = 'some user' LIMIT 1
148
+ s.deleted_recently.where("age > 20").first
149
+ # => SELECT * FROM users WHERE deleted_at >= '2013-12-30 05:23:01.367705' AND name = 'some user' AND age > 20 LIMIT 1
150
+
151
+ # The special scope "ignoring_base" is generated for you; it satisfies the requirement without constraining in any way
152
+ User.ignoring_base.first # => SELECT * FROM users LIMIT 1
153
+ # #base_scope_satisfied automatically satisfies the requirement, without constraining in any way
154
+ User.base_scope_satisfied.first # => SELECT * FROM users LIMIT 1
155
+
156
+ #### `must_scope_by`, or, the general case
157
+
158
+ `base_scope_required!` and `base_scope` are actually just syntactic sugar on top of a more general system that lets
159
+ you declare one or more _scope categories_ (each of which is just a symbol) and various scopes and class methods that
160
+ _satisfy_ those categories.
161
+
162
+ (`base_scope_required!` is exactly equivalent to `must_scope_by :base`, and `base_scope :foo, lambda { ... }` is
163
+ exactly equivalent to `scope :foo, lambda { ... }, :satisfies => :base`.)
164
+
165
+ For example:
166
+
167
+ class User < ActiveRecord::Base
168
+ must_scope_by :deleted, :client
169
+
170
+ scope :not_deleted, lambda { where("deleted_at IS NULL") }, :satisfies => :deleted
171
+ scope :deleted, lambda { where("deleted_at IS NOT NULL") }, :satisfies => :deleted
172
+
173
+ scope :admin_active, lambda { where("deleted_at IS NULL AND client_id = 0") }, :satisfies => [ :deleted, :client ]
174
+
175
+ class << self
176
+ def for_client(c)
177
+ where(:client_id => c.id).scope_category_satisfied(:client)
178
+ end
179
+
180
+ def active_for_client(c)
181
+ where(:client_id => c.id, :deleted_at => nil).scope_categories_satisfied(:client, :deleted)
182
+ end
183
+ end
184
+ end
185
+
186
+ This sets up two categories of scopes that _both_ must be satisfied before you can query the database, `:deleted` and
187
+ `:client`. The scopes `not_deleted` and `deleted` satisfy the `:deleted` category; the scope `admin_active` satisfies
188
+ both. The class method `for_client` satisfies the `:client` category; the class method `active_for_client` satisfies
189
+ both categories.
190
+
191
+ For each required category, a special `ignoring` scope is automatically defined &mdash; `ignoring_deleted`
192
+ and `ignoring_client` in the above example. This tells RequiredScopes that you're explicitly deciding _not_ to apply
193
+ any scopes for the given category. So, while `User.not_deleted.first` will raise an exception complaining that you
194
+ haven't satisfied the `:client` category, `User.not_deleted.ignoring_client.first` will run just fine, and will not
195
+ constrain on client in any way.
196
+
197
+ All scopes get methods called `#scope_category_satisfied` and `#scope_categories_satisfied`. (You can actually pass
198
+ either a single scope or multiple scopes to either one.) These mark categories as satisfied, without constraining in
199
+ any way; this is useful for class methods, as above, that should be considered to satisfy a requirement. They also
200
+ function in block form, just like `#scoping` or `#unscoped` from ActiveRecord do:
201
+
202
+ User.scope_category_satisfied(:client) do
203
+ User.not_deleted.first # => SELECT * FROM users WHERE deleted_at IS NULL LIMIT 1
204
+ end
205
+
206
+ Note that the built-in ActiveRecord `#unscoped` method does not interact with RequiredScopes in any way. Unscoping
207
+ neither satisfies nor removes the satisfaction of any required categories.
208
+
209
+ #### RequiredScopes and Inheritance
210
+
211
+ If you use inheritance among your model classes, child classes will require any scope categories that their parents
212
+ have declared to require; if you add a separate `must_scope_by` call in the child class, then it will additionally
213
+ require those categories, too.
214
+
215
+ If you do _not_ want a child class to require all the categories of its parent, call
216
+ `ignore_parent_scope_requirement :client, :deleted` (or whatever categories you want to skip the requirement for) in
217
+ the child class. This removes the requirement from the child class.
218
+
219
+ #### How Smart Is It?
220
+
221
+ It's important to note that RequiredScopes does not, in any way, _actually look at your `WHERE` clauses_. That is, the
222
+ only thing it's doing is matching the categories you've said are required with scopes that satisfy those categories;
223
+ it does not know or care what those scopes actually _do_.
224
+
225
+ If you say a scope satisfies a category, then RequiredScopes will be happy with it, even if it actually just does
226
+ `ORDER BY id ASC` (or does nothing at all!). If no scope is applied that satisfies a category, you'll get an error,
227
+ even if you've constrained every column in seven different ways.
228
+
229
+ This hopefully makes the entire system much easier to understand, but it's worth noting.
230
+
231
+ #### Along With `default_scope`
232
+
233
+ Note that RequiredScopes does not affect the behavior of `default_scope` in any way; if you declare a `default_scope`,
234
+ it will still be used, as normal. `default_scope`s cannot satisfy any categories, however. (But this wouldn't make any
235
+ sense, anyway: if your default scope satisfies a category, then it's really not required any more, is it?)
236
+
237
+ ## Contributing
238
+
239
+ 1. Fork it
240
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
241
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
242
+ 4. Push to the branch (`git push origin my-new-feature`)
243
+ 5. Create new Pull Request
244
+
245
+ ### Running Specs
246
+
247
+ RequiredScopes is quite thoroughly tested, using RSpec. Note that all of its tests are "system" tests, in that they
248
+ test the entire Gem, all the way down through ActiveRecord, to the database. This is because there is really no
249
+ significant code in RequiredScopes that's independent of its interface to ActiveRecord, and so unit tests would be of
250
+ little use. (In other words, making this gem work correctly is all about getting the patches to ActiveRecord right,
251
+ not about consistency of any sophisticated internal logic.)
252
+
253
+ To run these specs, you'll need a database server up and running that ActiveRecord can talk to. The specs create and
254
+ drop various tables (all prefixed with `rec_spec_`, so they're highly unlikely to conflict with anything). Because
255
+ they do this, there's no need to prepare the database ahead of time, and it should be safe to use a database that's
256
+ also used for other things. (On the other hand, having its own dedicated database won't hurt; and if you run these
257
+ specs against a database containing data that's precious, you're just asking for it.)
258
+
259
+ To run these specs:
260
+
261
+ 1. If you want to test against a particular ActiveRecord version, `export REQUIRED_SCOPES_AR_TEST_VERSION=3.2.16` (for example). If you want the latest stable ActiveRecord, simply skip this step.
262
+ 2. `cd required_scopes` (the root of the gem).
263
+ 3. Create a file called `spec_database_config.rb` at the root level of the gem. It should define your connection to the database, like so:
264
+
265
+ REQUIRED_SCOPES_SPEC_DATABASE_CONFIG = {
266
+ :require => 'pg',
267
+ :database_gem_name => 'pg',
268
+ :config => {
269
+ :adapter => 'postgresql',
270
+ :database => 'some_database',
271
+ :username => 'postgres',
272
+ :password => 'some_password'
273
+ }
274
+ }
275
+
276
+ 4. `bundle install`. (This step must come _after_ you create `spec_database_config.rb`; it uses that file to know what database gem to include.)
277
+ 5. `bundle exec rspec spec` will run all specs. (Or just `rake`.)
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,17 @@
1
+ # There's currently nothing at all in the RequiredScopes module except for its use as a namespace.
2
+ module RequiredScopes
3
+ end
4
+
5
+ require "required_scopes/version"
6
+ require "required_scopes/active_record/base"
7
+ require "active_record"
8
+
9
+ # Add methods to ::ActiveRecord::Base that let you declare scoping requirements, and declare scopes that satisfy
10
+ # them...
11
+ ActiveRecord::Base.send(:include, ::RequiredScopes::ActiveRecord::Base)
12
+
13
+ # ...and add methods to ::ActiveRecord::Relation that enforce those requirements.
14
+ require "required_scopes/active_record/relation"
15
+
16
+ require "required_scopes/active_record/version_compatibility"
17
+ ::RequiredScopes::ActiveRecord::VersionCompatibility.apply_version_specific_fixes!
@@ -0,0 +1,235 @@
1
+ require 'active_support'
2
+ require 'active_record'
3
+
4
+ module RequiredScopes
5
+ module ActiveRecord
6
+ # This is the module that gets +include+d into ::ActiveRecord::Base when +required_scopes+ is loaded. It defines
7
+ # the exposed methods on ::ActiveRecord::Base, and overrides a few (like #scope and #unscoped).
8
+ module Base
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ class << self
13
+ delegate :scope_categories_satisfied, :scope_category_satisfied,
14
+ :all_scope_categories_satisfied, :to => :relation
15
+ end
16
+ end
17
+
18
+ # Calling #destroy ends up generating a relation, via this method, that is used to destroy the object. We need
19
+ # to make sure we don't trigger any checks on this call.
20
+ def relation_for_destroy
21
+ out = super
22
+ out.all_scope_categories_satisfied!
23
+ out
24
+ end
25
+
26
+ module ClassMethods
27
+ # Declares that all users of your model must scope it by one or more categories when running a query, performing
28
+ # a calculation (like #count or #exists?), or running certain bulk-update statements (like #delete_all).
29
+ # Categories are simply symbols, and they are considered satisfied if a scope is used that declares (_e.g._)
30
+ # <tt>:satisfies => :deletion</tt> is included before running a query.
31
+ #
32
+ # This is the heart of +required_scopes+. Its purpose is to remind developers that certain kinds of constraints
33
+ # should always be taken into account when accessing data in a particular table, so that they can't
34
+ # under-constrain queries and potentially ignore soft deletion of rows, or cross client boundaries, or similar
35
+ # things.
36
+ #
37
+ # For example:
38
+ #
39
+ # class User < ActiveRecord::Base
40
+ # must_scope_by :deletion
41
+ #
42
+ # scope :normal, lambda { where(:deleted => false) }, :satisfies => :deletion
43
+ # scope :deleted, lambda { where(:deleted => true) }, :satisfies => :deletion
44
+ # end
45
+ #
46
+ # Now, if you say
47
+ #
48
+ # the_user = User.where(:name => 'foo').first
49
+ #
50
+ # ...you'll get a RequiredScopes::Errors::RequiredScopeCategoriesNotSatisfiedError; you must instead call
51
+ # one of these:
52
+ #
53
+ # the_user = User.normal.where(:name => 'foo').first
54
+ # the_user = User.deleted.where(:name => 'foo').first
55
+ #
56
+ # For any given scope category, a scope starting with +ignoring_+ is automatically created; this does not
57
+ # actually constrain the scope in any way, but marks that category as satisfied. (The point is not to _force_
58
+ # developers to constrain on something, but to make sure they can't simply forget about the category.) So this
59
+ # will also work:
60
+ #
61
+ # the_user = User.ignoring_deletion.where(:name => 'foo').first
62
+ #
63
+ # An explicit call to #unscoped removes all requirements, whether used in its direct form or its
64
+ # block form, so these will also both work:
65
+ #
66
+ # the_user = User.unscoped.where(:name => 'foo').first
67
+ # User.unscoped { the_user = User.where(:name => 'foo').first }
68
+ #
69
+ # ActiveRecord also lets you use class methods as scopes; if you want one of these to count as satisfying a
70
+ # scope category, use #scope_category_satisfied (or #scope_categories_satisfied):
71
+ #
72
+ # class User < ActiveRecord::Base
73
+ # must_scope_by :client
74
+ #
75
+ # scope :active_clients, lambda { where(:client_active => true) }, :satisfies => :client
76
+ #
77
+ # class << self
78
+ # def for_client_named(client_name)
79
+ # client_id = CLIENT_MAP[client_name]
80
+ # where(:client_id => client_id).scope_category_satisfied(:client)
81
+ # end
82
+ # end
83
+ # end
84
+ #
85
+ # In the above example, either <tt>User.active_clients.first</tt> or <tt>User.for_client_named('foo').first</tt>
86
+ # will count as having satisfied the requirement to scope by +:client+, and hence will not raise an error.
87
+ def must_scope_by(*args)
88
+ categories = args.map(&:to_sym)
89
+ @required_scope_categories ||= [ ]
90
+ @required_scope_categories += categories
91
+
92
+ categories.each do |category|
93
+ scope "ignoring_#{category}", lambda { send(::RequiredScopes::ActiveRecord::VersionCompatibility.relation_method_for_ignoring_scopes) }, :satisfies => category
94
+ end
95
+ end
96
+
97
+ # Returns the set of scope categories that must be satisfied for this class, as a (possibly-empty) Array.
98
+ def required_scope_categories
99
+ if self == ::ActiveRecord::Base
100
+ [ ]
101
+ else
102
+ out = (@required_scope_categories || [ ]) | superclass.required_scope_categories
103
+ out - (@ignored_parent_scope_requirements || [ ])
104
+ end
105
+ end
106
+
107
+ # If you're using inheritance in your ActiveRecord::Base classes, and a parent class declares a required scope
108
+ # category (using #must_scope_by), you can remove that requirement in a subclass using
109
+ # #ignore_parent_scope_requirement. (This should be quite rare.)
110
+ def ignore_parent_scope_requirement(*args)
111
+ categories = args.map(&:to_sym)
112
+ @ignored_parent_scope_requirements ||= [ ]
113
+ @ignored_parent_scope_requirements |= categories
114
+ end
115
+
116
+ # We want #unscoped to remove any actual scoping rules, just like it does in ActiveRecord by default. However,
117
+ # we *don't* want it to remove any category satisfaction that we're currently under. Why? So you can do this:
118
+ #
119
+ # User.all_scope_categories_satisfied do
120
+ # ...
121
+ # User.unscoped.find(...)
122
+ # ...
123
+ # end
124
+ def unscoped
125
+ satisfied_by_default = current_scope.try(:satisfied_scope_categories) || [ ]
126
+
127
+ out = super
128
+ out = out.scope_category_satisfied(satisfied_by_default) if satisfied_by_default.length > 0
129
+ out
130
+ end
131
+
132
+
133
+ # Declares that use of this ActiveRecord::Base class must be scoped by at least one _base scope_; a base scope
134
+ # is any scope declared using #base_scope, instead of #scope. (Other than satisfying this requirement,
135
+ # #base_scope behaves identically to #scope.) This can be used to ensure that developers don't forget to scope
136
+ # out deleted records, or don't forget to scope by client, or so forth. (See the +README+ for +required_scopes+
137
+ # for why this is a better solution in many cases than just using +default_scope+.)
138
+ #
139
+ # For example:
140
+ #
141
+ # class User < ActiveRecord::Base
142
+ # base_scope_required!
143
+ #
144
+ # base_scope :undeleted { where(:deleted => false) }
145
+ # base_scope :deleted { where(:deleted => true) }
146
+ # end
147
+ #
148
+ # Now, you'll get the following:
149
+ #
150
+ # User.where(...).first # RequiredScopes::Errors::BaseScopeNotSatisfiedError
151
+ #
152
+ # User.undeleted.where(...).first # => SELECT * FROM users WHERE deleted = 0 AND ... LIMIT 1
153
+ # User.deleted.where(...).first # => SELECT * FROM users WHERE deleted = 1 AND ... LIMIT 1
154
+ # User.ignoring_base.where(...).first # => SELECT * FROM users WHERE ... LIMIT 1
155
+ #
156
+ # (This is simply syntactic sugar on top of #must_scope_by, using a category name of +:base+; see that method
157
+ # for more details.)
158
+ def base_scope_required!
159
+ must_scope_by :base
160
+ end
161
+
162
+ # Declares a scope that satisfies the requirement introduced by #base_scope_required!. In all other ways, it
163
+ # behaves identically to ActiveRecord::Base#scope.
164
+ def base_scope(name, body, &block)
165
+ scope(name, body, :satisfies => :base, &block)
166
+ end
167
+
168
+ # Returns a scope identical to the one it's called on, but that's marked as satisfying the requirement
169
+ # introduced by #base_scope_required!. This is useful in class methods that return a scope you want to count
170
+ # as satisfying that requirement:
171
+ #
172
+ # class User < ActiveRecord::Base
173
+ # base_scope_required!
174
+ #
175
+ # class << self
176
+ # def for_client_named(c)
177
+ # where(:client_id => CLIENT_ID_TO_NAME_MAP[c]).base_scope_satisfied
178
+ # end
179
+ # end
180
+ # end
181
+ def base_scope_satisfied(&block)
182
+ scope_category_satisfied(:base, &block)
183
+ end
184
+
185
+
186
+ # Overrides ActiveRecord::Base#scope to mark a scope as satisfying one or more categories if an extra option
187
+ # is passed:
188
+ #
189
+ # scope :foo, lambda { ... }, :satisfies => :deletion
190
+ #
191
+ # You can also pass an Array of category names to :satisfies.
192
+ def scope(name, body, *args, &block)
193
+ if args && args[-1] && args[-1].kind_of?(Hash)
194
+ opts = args.pop
195
+
196
+ categories = Array(opts.delete(:satisfies)).compact
197
+
198
+ if categories && categories.length > 0
199
+ if body.kind_of?(Proc)
200
+ # New, happy, dynamic scopes -- i.e., scope :foo, lambda { where(...) }
201
+ old_body = body
202
+ body = lambda do
203
+ out = old_body.call
204
+ out.scope_categories_satisfied!(categories)
205
+ out
206
+ end
207
+ else
208
+ # Old, sad, static scopes -- i.e., scope :foo, where(...)
209
+ body = body.clone
210
+ body.scope_categories_satisfied!(categories)
211
+ body
212
+ end
213
+ end
214
+
215
+ args.push(opts) if opts.size > 0
216
+ end
217
+
218
+ super(name, body, &block)
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end
224
+
225
+ # When you call #includes to include an associated table, the Preloader ends up building a scope (via #build_scope)
226
+ # that it uses to retrieve these rows. We need to make sure we don't trigger any checks on this scope.
227
+ ::ActiveRecord::Associations::Preloader::Association.class_eval do
228
+ def build_scope_with_required_scopes_ignored
229
+ out = build_scope_without_required_scopes_ignored
230
+ out.all_scope_categories_satisfied!
231
+ out
232
+ end
233
+
234
+ alias_method_chain :build_scope, :required_scopes_ignored
235
+ end