with_scope and :create

Hi,

I've been using AR::Base.with_scope lately, and it's great. I'm finding
it especially useful with the new around_filters that let you specify a
block and yield to the filter chain.

But, when you pass with_scope a :create hash, there is some strange
behavior: Base.create respects the scoping, but Base.save does not, even
if you are saving a new record.

This feels inconsistent. A before_create filter in a model will fire in
both cases; shouldn't scoping behave the same?

I'd be happy to provide tests and a patch, but wanted to check before I
put those together. Is anybody working on this? Do people feel that this
is the right behavior for with_scope to have?

Thanks,
phil

D'oh! It's already been submitted, long ago:
http://dev.rubyonrails.org/ticket/3734
Should have dug in trac more before posting -- sorry folks.

Any chance this will make it into core? It has tests and cleans up a
surprise lurking in with_scope.

Phillip Kast wrote:

`Phil,

I would love to see this patch applied. The current behavior has been
irritating me

for a long time now.

Thanks,

Keith`

Phillip Kast wrote:

D'oh! It's already been submitted, long ago:
http://dev.rubyonrails.org/ticket/3734
Should have dug in trac more before posting -- sorry folks.

Any chance this will make it into core? It has tests and cleans up a
surprise lurking in with_scope.

Could you give us an example of what you're doing with with_scope that
needs this. The example in the trac ticket is definitely not
something that needs with_scope.

  login = Login.new({:name=>"anonymous"}.merge(params[:login]))
  login.save

No prob - the ticket example does look pretty contrived.

I'm using with_scope in an around_filter, similar to the way the
scoped_access plugin does.
The idea is to cleanly ensure that a number of models are always
associated with a user's account.
But, we have some places where stuff needs to be done to a new model
object before it's ready to save.
Something along the lines of:

class ApplicationController < AC::Base
    def scope_story_by_account
        Story.with_scope(:find => {:conditions => {:account_id =>
current_account.id}, :create => {:account_id => current_account.id}) {
yield }
    end
    around_filter :scope_by_account
end

class StoryController < ApplicationController
    def create
       @story = Story.new(params[:story])
       # do some stuff to @story here...

       @story.save #@story.account_id won't be set, without the patch
we're discussing.
    end
end

...But with a bunch of models, controllers, and actions thrown into the mix.

Philosophically, it also seems to me that with_scope and ActiveRecord
callbacks should have the same behavior: if new_record is true, the
create callbacks/scoping should happen, regardless of whether you called
create or save.

phil

Michael Koziarski wrote:

class StoryController < ApplicationController
    def create
       @story = Story.new(params[:story])
       # do some stuff to @story here...

       @story.save #@story.account_id won't be set, without the patch
we're discussing.
    end
end

The 'right' way to do this would be

class StoryController < ApplicationController
  def create
    @story = current_account.stories.build params[:story]
    # ...
    @story.save # account_id set as required.
  end
end

with_scope was originally just an implementation method for the
association proxies, and any time you find yourself using it, you're
probably missing out on a much much simpler option.

As it stands, I think I'd prefer the philosophical inconsistencies, if
it points people at the right way to do it.

> class StoryController < ApplicationController
> def create
> @story = Story.new(params[:story])
> # do some stuff to @story here...
>
> @story.save #@story.account_id won't be set, without the patch
> we're discussing.
> end
> end

This is the same example I see for with_scope over and over again,
which troubles me greatly. I've only ever seen one example of
with_scope that seemed reasonable. Mike Clark had something in his
model class to merge multiple conditions into one. Even though that
seemed sorta legit, I really don't like features that point people in
the wrong direction 95% of the time. Having with_scope publicly
available seems to be exactly that.

I'm seriously considering that we should deprecate with_scope as a
public method and make it private by 2.0.

This is the same example I see for with_scope over and over again,
which troubles me greatly. I've only ever seen one example of
with_scope that seemed reasonable. Mike Clark had something in his
model class to merge multiple conditions into one. Even though that
seemed sorta legit, I really don't like features that point people in
the wrong direction 95% of the time. Having with_scope publicly
available seems to be exactly that.

I'm seriously considering that we should deprecate with_scope as a
public method and make it private by 2.0.

Deprecation is a step too far, there are legitimate uses of
with_scope, particularly around encapsulated finds for permissions
models. I just think that there needs to be some serious warnings in
the documentation:

"You almost certainly can find a nicer way to do this"

The documentation should be updated to reflected it's intended use as well.

That being said, encapsulated finds with permissions models don't
need create or new or any of that stuff. As it stands, this is a
wontfix for me.

I agree with Michael,
Maybe just putting some warnings in the documentation would be nice.
I used the feature in some limited case. I found that sometimes it
helps keeping a block of the code readable and easy to understand.

That been said with_scope has its limitations.

Diego

Thanks for the feedback.
I think what I'm ultimately trying to do is legit though, so I want to
try and make the case one more time.

So what I want is to be able to say something in a controller like:
  scoped_by_current_account :story
with the result that all find calls on a Story model from that
controller will only return stories that belong to the current account,
and any new stories will always be associated with the current account.

I've done this. It's easy to whip up using with_scope and around
filters, and it seems like a clean and useful abstraction to me. Among
other things, it reduces the risk that a hole in test coverage and/or a
missing find condition could result in, say, returning stories for the
wrong account. You can nest scoping and do this for several models at a
time too, no problem.

This works great for finds. The trouble is that you can't do the same
thing for creates, because the association won't be set if you call save
instead of create, which is too fragile. But, the abstraction makes the
most sense if it works in both directions.

It seems like the legit uses of with_scope that folks have mentioned all
involve passing :find, and the bad uses I've given all use :create.
Documentation would help, but the bigger issue is consistency. I think
either with_scope/:create should treat new records the same way the rest
of ActiveRecord does, or it should go away. Perhaps the way to make it
go away would be to leave with_scope public, but extract the part that
handles the :create hash to a private method.

Thanks, and I hope I'm not ranting too much here -- I'm just interested
in seeing this little corner of Rails be less surprising.
phil

DHH wrote:

Hi,

So what I want is to be able to say something in a controller like:
  scoped_by_current_account :story
with the result that all find calls on a Story model from that
controller will only return stories that belong to the current account,
and any new stories will always be associated with the current account.

Isn't this clearer / more understandable ?

current_user.stories.find(params[:id])

It is obvious from this that you're fetching a user story, not a
random story. If I don't see your filters (which I almost never
bother to parse unless that's what I'm working on currently), then I
won't catch your filter usage.

Same goes for creating too:

current_user.stories.create!(params[:story])
# works the same with build

Bye !

The problem with with_scope, regarding showing short and simple
examples of when it is particularly useful, is that the uses where
with_scope shines aren't particularly short nor simple in nature.
I used with_scope once when I made a whole lot of complex finds and I
wanted to limit all of them to finding objects which had is_published
= 1, but only in _this_ particular case. It did, in fact, make the
code easier to read, but the biggest reason I used it was to make sure
I didn't mistakenly allow one of those many finds to select
non-published objects, in one fell swoop I eliminated the possibility
of that happening by mistake.

Improved documentation seems like a better solution than to limit the
useage. This is Ruby we're talking about, there are lots of features
available to misuse already, that doesn't take away the legitimate
uses.

Best regards,
Tomas Jogin

Deprecation is a step too far, there are legitimate uses of
with_scope, particularly around encapsulated finds for permissions
models. I just think that there needs to be some serious warnings in
the documentation:

The decision has been rendered as follows: with_scope will go protected
from Rails 2.0. That means you'll be able to use with_scope in your own
private finders (the only legitimate use I've seen), but not in, say,
filters.

Now we just have to find a way to get those deprecation messages in
there when its being called in the public space.

I think it’s a poor choice to limit something that people find useful just because it doesn’t fit your idea of useful. Why not let the coder make these decisions?

I wrote that filter patch specifically for using with_scope in controllers (besides that it was a much needed feature in general). I think it’s incredibly useful. I have an application where 5-6 of the controllers are all scoped under an Order. I use simply restful to make a path like orders/1/controller/ so that all of those methods can be scoped by that order. I add one around_filter and it’s taken care of. I also access some of those controllers separately and scoped under a person. Because I have with_scope, I don’t have to write all these weird exceptions in the controller code. Just clean and simple CRUD actions.

I feel very passionately that this is an excellent use of with_scope. There were plugins before the new around_filter that did this, and as soon as it’s introduced, people used it for that. Denying this usefulness is just going to irritate people. I myself will just write a public method that calls the protected with_scope if the public with_scope is deprecated. If this is viewed as illegitimate, so be it.

I urge you to reconsider your decision to deprecate public with_scope and instead give a real argument against it in the documentation.

Respectfully,
Martin Emde

I think it's a poor choice to limit something that people find useful just
because it doesn't fit your idea of useful. Why not let the coder make these
decisions?

You must be new here :). Rails is opinionated software. We make things
that we consider good style easy to do and bad style hard. Or rather,
we make good style beautiful and bad style ugly. So even though
with_scope is going protected, you can still use it in filters if you
really, really want to. Just use send(:with_scope) -- that'll side-step
the access control. Yes, that'll be ugly and it's intended to be.

Using around_filters in the manor you describe makes it really hard to
figure out what's going on from looking at the action in isolation. It
looks like you're doing a straight, in-secure find to the naked eye.
And there's no bread crumbs for others to follow that'll leave them to
your around_filter. It's a recipe for hard-to-read code. And it shields
the reader from understand your model hierarchy.

In summary, Rails will shake its head at you for using with_scope in
filters, but will ultimately leave it at your discretion to choose the
right path. We believe in encouragement and discouragement, not
allowing or forbidding.

Ok, I’ll accept that. I’ve always known that Rails is “oppinionated” but I’ve never run into a problem with it until now. My opinions are usually right in line with those of Rails core. I agree it can make the controller a bit harder to scan with those around_filters so I see your point.

-Martin