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

#decorator method doesn't work properly with cache_classes = false #32

Closed
jfelchner opened this issue Sep 29, 2011 · 27 comments
Closed
Labels

Comments

@jfelchner
Copy link
Contributor

Due to the way the the #decorator method is added to the decorated class, when cache_classes is false, the decorator class isn't loaded at startup and therefore the #decorator method is never added to the class.

This method seems to be in the very beta stages since there's no documentation around it. It's extremely handy though so I hope someone who knows more than me about Rails load order can find a solution.

@jfelchner
Copy link
Contributor Author

Ignore this. I was loading something incorrectly.

-Edit Actually ignore me saying 'ignore this'. This is in fact a problem with #decorator

@jfelchner jfelchner reopened this Oct 2, 2011
@jcasimir
Copy link
Member

I see what you're saying here and am observing the same in my sample app.

The load-order catch is that the gem is loaded before the application models, so there's no easy moment to tell the app to "go ahead and include this module into various models" unless we setup an initializer.

But I'm thinking that will be necessary, anyway. It would help with this issue and with some challenges around the view_context approach.

@jcasimir
Copy link
Member

Actually, let me nix what I said there. It looks like all other important issues are handled without an initializer.

I'll reach out to @paulelliott for ideas.

The problem in summary:

When Rails is in development mode and cache_classes = false, then it lazy-loads the models and decorators. If you fetch a model instance, say article = Article.first then you'd like to be able to say article.decorator to get a decorated instance.

However, since the decorator has not yet been loaded, the decorates method has not been triggered, the ModelSupport module has not been included into the Article class, so this method fails.

If you first create some instance of the decorator, causing the class to be loaded, then the above works just fine.

@paulelliott
Copy link
Contributor

We have been dealing with this same issue and I haven't decided on an appropriate course of action yet. I am leaning towards having a method on the model class that allows you to specify what it is decorated by. I feel like there is a larger architectural issue at hand though.

To get through the day, you can just reference the Decorator class at the bottom of your model file. I know it's gross, but it won't negatively impact performance and dev will work fine. Like so...

class MyModel
end
MyModelDecorator

@jfelchner
Copy link
Contributor Author

@paulelliott I thought about doing it that way and yeah, I agree it's kinda gross. What I decided on instead was to just include the ModelSupport module in the classes that I need to test. That seems a bit less hacky to me since it's including the method that you want defined anyway instead of doing something in your code specifically for development.

Although you could argue you're still doing it specifically for development. :)

@paulelliott
Copy link
Contributor

That appears cleaner but you are relying on a non-public class from draper. Either way has its drawbacks. In any event, this needs to be solved by the gem so that none of this is necessary.

@amerine
Copy link

amerine commented Oct 21, 2011

Just throwing a guess out here, but couldn't a change to the System.setup method like this work:

module Draper
  class System    
    def self.setup
      ActionController::Base.send(:include, Draper::ViewContextFilter) if defined?(ActionController::Base)
      Rails::Applications.config.after_initialize Draper::RailsInitializer.load if defined?(Rails)
    end
  end
end

The Draper::RailsInitializer.load call can just require the files in app/decorators

I haven't tested this theory... and it kinda seems tacky.

@cookrn
Copy link
Contributor

cookrn commented Oct 21, 2011

I've had a similar problem with RoleUp, a young gem backed by CanCan that allows you to create and segment ability definitions in a Rails app in a logical way. The solution I came up with is to do certain things on application boot and on each request. Here's the relevant code:

https://github.com/quickleft/role_up/blob/master/lib/role_up/railtie.rb

# FILE :: lib/role_up/railtie.rb
require "rails/railtie"

module RoleUp

  class Railtie < Rails::Railtie

    # eager load our app/abilities directory
    #
    # HAPPENS FIRST
    config.before_configuration do |app|
      app.config.paths.app.abilities "app/abilities" , :eager_load => true , :glob => RoleUp.glob
    end

    # make sure our ability files are reloaded on each request in development
    # ONLY if we are not caching classes
    #
    # HAPPENS SECOND
    config.to_prepare do
      RoleUp::Railtie.force_load_abilities if Rails.env.development? && !Rails.application.config.cache_classes
    end

    # force load our ability files if we are in a non-development environment and
    # if we aren't caching classes
    #
    # HAPPENS THIRD
    config.after_initialize do |app|
      force_load_abilities if !Rails.env.development? && !app.config.cache_classes
    end

    def self.force_load_abilities
      Dir[ "#{ Rails.root }/app/abilities/#{ RoleUp.glob }" ].each { |a| load a }
    end

  end

end

I don't like the logic that's being depended on necessarily, but overall it works in development where non-referenced constants are also reloaded upon each request. The above also works in non-development environments where the files are loaded once at boot. Ideally ActiveSupport's code reloading facilities would/could be utilized.

I had a short but great discussion with @rkh about this and his RSoC essay on Rails code reloading was invaluable. I hope this is relevant or useful!

@radar
Copy link

radar commented Oct 21, 2011

I agree with @cookrn on the possible solution for this. Have you considered using a to_prepare callback rather than checking the cache_classes setting?

@ericallam
Copy link

I think the best way would be to include the ModelSupport module in ActiveRecord::Base when it's loaded, using a railtie and defining an initializer, like this:

initializer "active_record.initialize_draper" do
  ActiveSupport.on_load(:active_record) do
    ActiveRecord::Base.send :include, Draper::ModelSupport
  end
end

@amerine
Copy link

amerine commented Oct 21, 2011

@rubymaverick Would that solve the issue? I thought we had to actually execute the decorates call in each decorator class?

@ericallam
Copy link

@amerine Well, no I guess it wouldn't completely solve the issue. The way I'd imagine it could work would be that all ActiveRecord::Base subclasses would get the #decorator method, but calling #decorator on an model object that didn't have a corresponding decorator class would return nil (or maybe it should raise an error?).

The other thing you'd have to account for is the ability to change the decorate class for an AR class, maybe using a AR::Base.decorated_by method.

@leshill
Copy link
Contributor

leshill commented Oct 21, 2011

+1 to @paulelliot's suggestion for a macro on the model (inverse of the current decorates :model macro)

@Mange
Copy link
Contributor

Mange commented Oct 21, 2011

I think it's bound to add some bad effects if the model gets some magic methods by being indirectly referenced from another class. Honestly, having a ActiveModel#decorate method should be an explicit thing by either a macro or by including a module in it.

The kosher, non-magic way to decorate would be to call SomeDecorator.decorate(some_model_instance), right?

@cookrn
Copy link
Contributor

cookrn commented Oct 21, 2011

Philosophy of how decorators are related to models aside, loading decorators from app/decorators/ on application boot is the desired fix, correct? This would evaluate decorates calls in the decorator classes and fix up your models. I think there are a few options:

  • Defining a named Rails path with options set that will load the code on boot. I cannot, for the life of me, figure out if this is even possible. https://github.com/rails/rails/blob/master/railties/lib/rails/paths.rb
  • Using a to_prepare block in the Draper Engine/Railtie to (re)load code on every request in development and at application boot in production. Use the same ActiveSupport APIs that Rails does if possible, otherwise force it.

@steveklabnik
Copy link
Member

This problem still exists, even though it's really old, and Draper has changed a lot. I've created a test repository that you can run rake test in to see it fail.

I'm still not sure what the right answer is, but test cases are good. And confirming 7 month old issues are good.

cookrn added a commit to cookrn/draper that referenced this issue May 9, 2012
cookrn added a commit to cookrn/draper that referenced this issue May 9, 2012
…ther files should be `load`-ed or `require`-ed -- Issue drapergem#32
cookrn added a commit to cookrn/draper that referenced this issue May 9, 2012
…paths. Describe the reasoning behind each hook/callback. -- Issue drapergem#32
@steveklabnik
Copy link
Member

This was closed by #188

@cookrn
Copy link
Contributor

cookrn commented May 9, 2012

EDIT: Haha well it did get closed while I was writing this...

Before this issue gets closed, I'd like to +1 the suggestion from @Mange. His strategy could be supported in the future as best practice for users and allows for all of Rails code reloading fu to be used out of the box without the extra code. I also find it to be much more clear and hopefully leading people down the path of avoiding model or database-connected objects in their views at all.

@steveklabnik
Copy link
Member

@cookrn :)

I feel pretty good about this solution. Do you still think @Mange is right? I think lots of these suggestions got muddled in the details...

@jfelchner
Copy link
Contributor Author

@cookrn Thanks for the PR, it'll be nice to remove the extra boilerplate code from my app. Much appreciated!

@cookrn
Copy link
Contributor

cookrn commented May 10, 2012

@steveklabnik Do I think he's right? Yes. Do I think that solution is a good thing for Draper short-term? No. Do I think that solution is a good thing for Draper long-term? Yes. But that's just one opinion on something that is a pretty serious usage consideration for well-used library. My reasoning? It seems a little bit inverted that a model would know about the the thing that decorates it. That might be better as a one-way street. It's a toughie for sure :) -- small code change with a large effect!

@jfelchner My pleasure! I hope it works well.

@steveklabnik
Copy link
Member

Right. Well, as Draper approaches 1.0, it's probably going to undergo significant internal change, so I'll consider anything, really. Feel free to submit any patches you have to push it in that direction!

Especially this view_context stuff. Might have to get Rails to give us a cleaner API into these kinds of things.

@cgunther
Copy link
Contributor

Looks like this change causes the decorators and their models to be loaded too early if you use Spork in testing.

I'm using this in my spec_helper file to avoid the preloading issue:

Spork.trap_class_method(Draper::System, :load_app_local_decorators)

@steveklabnik
Copy link
Member

Ugh, Spork. I hate spork so much...

Do you think this is a good general thing? Should this be in draper proper?

@cookrn
Copy link
Contributor

cookrn commented May 14, 2012

What does it mean that it's "loading too early"? Too early for Spork to properly preload? Or too early in the Rails environment in general?

@cgunther
Copy link
Contributor

Too early in that Spork wont detect changes to the file on test runs until you reload Spork. No issues with Rails at all, just a Spork issue.

Given that there's already some debate over whether the current fix for this issue is the right long-term solution, I might hesitate to update Draper proper to support this Spork issue. I just wanted to add a note so when evaluating pros/cons of the current implementation, this was known.

Gems preloading files in Spork prematurely is a common problem, Devise suffers a similar issue. In the interim, as Draper approaches 1.0, maybe a simple note with the workarond in the Readme would suffice.

@steveklabnik
Copy link
Member

Cool. I am gonna add it in, but not right this second... let me make another issue to keep track of this, since this is only related to this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests