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

Needs to provide "controller" for helper_method helpers #12

Closed
jaylevitt opened this issue Aug 10, 2011 · 31 comments
Closed

Needs to provide "controller" for helper_method helpers #12

jaylevitt opened this issue Aug 10, 2011 · 31 comments

Comments

@jaylevitt
Copy link

I'm using Devise, which generates its helpers using AbstractController#helper_method. That works like so:

  def helper_method(*meths)
    meths.flatten!
    self._helper_methods += meths

    meths.each do |meth|
      _helpers.class_eval <<-ruby_eval, __FILE__, __LINE__ + 1
        def #{meth}(*args, &blk)
          controller.send(%(#{meth}), *args, &blk)
        end
      ruby_eval
    end
  end

So the helper ends up wanting to call controller, and that's nil since you don't proxy it. Any ideas how to solve it?

@jcasimir
Copy link
Member

I think this is an easy fix, but I need to build a sample devise app and try it out. I'm an all-OmniAuth guy these days :)

@jaylevitt
Copy link
Author

Cool. You shouldn't even need to build a devise app; just look at how devise uses helper_method, and create a helper the same way. (Which means you can even add a spec for it!)

@numbers1311407
Copy link

I ran into this same issue, helpers which require access to controller.

Have you thought about sending the controller's view_context to the decorator instead of an all helpers module? It would give you all the proxies (request, controller, etc), and instead of all helpers, you'd get those available to the controller. Perhaps there's some obvious issue I'm not considering here?

@jcasimir
Copy link
Member

I have tried all kinds of hackery to figure out the current controller without having to pass it in during the decorator instantiation, but all efforts have so far failed.

@numbers1311407
Copy link

I actually did this yesterday morning after making the comment above, essentially replacing helpers with the controller's view_context, but yeah, I am simply passing it along to the constructor, which does feel a bit clunky. It works fine so far though, giving me access to all the context I need. The examples use current_user but I don't know how that works without having access to the request context? Like jaylevitt says, it certainly doesn't work out of the box with devise, which expects controller to be defined as it depends on helper_method.

I wonder if storing the current controller in a before filter in a thread local variable would work?

@jaylevitt
Copy link
Author

I wonder if storing the current controller in a before filter in a thread local variable would work?

This is essentially what we're doing in our app. Icky, but it works:

class ApplicationController < ActionController::Base
  prepend_before_filter { @@current_controller = self }
end

class YourPresenter
  def current_user
    ApplicationController.current_controller.current_user
  end
end

@numbers1311407
Copy link

I'll probably end up combining this with my prior view_context hack, providing access to the current controller through a similar variable set each request, probably using Thread.current, then rather than building out a helpers module, simply grabbing that controller's view_context instead.

Essentially:

def helpers
  ApplicationController.current.view_context
end

I just wonder, is this somehow against the design philosophy of this gem?

@jcasimir
Copy link
Member

ApplicationController.current.view_context would be awesome, but does it exist? I couldn't find ApplicationController.current in the API and it didn't work in my experiment.

@numbers1311407
Copy link

Sorry didn't mean to be confusing there. It doesn't exist, it's just something I wrote as an example to what I was trying to do. I did implement it this afternoon, along with my override of helpers above, and so far everything is working well (not backed by any tests yet though).

# In a module BaseController that I mix into ApplicationController
def BaseController.current
  Thread.current['controller']
end

def BaseController.current=(controller)
  Thread.current['controller'] = controller
end

# in ApplicationController
before_filter { BaseController.current = self }

# in my top level subclass of Draper::Base
def helpers
  @helpers ||= BaseController.current.view_context
end
alias :h :helpers

Working perfectly for me so far.

@jcasimir
Copy link
Member

jcasimir commented Oct 3, 2011

Oh shit, I think I understand what you're doing here. This could be a really excellent idea that solves multiple problems. I need to experiment more.

@jfelchner
Copy link
Contributor

+1

If that actually works, it should enable button_to, image_tag, form_for and all of the other tags that are currently unavailable in the Decorator models. @numbers1311407 that's very clever stuff. Are there any potential gotchas with Thread.current?

@numbers1311407
Copy link

No gotchas so far. I've been using this approach for a few months now and it's all worked fine. That's essentially the entirety of the code in the above comment.

The view_context not only gives you access to the request context, but it also ensures you have the specific helpers available to the controller.

@jcasimir
Copy link
Member

jcasimir commented Oct 8, 2011

I just released 0.8.0 using this technique. Would you let me know if it helps the original issue?

@jfelchner
Copy link
Contributor

@jcasimir so it looks like this may actually work but it broke something else. The way the code is currently written, it's saying that there will definitely be a view_context at all times. This is fine when running the app, but not fine at all when testing. Since, when you're testing you want to be running the decorator methods in isolation.

What ends up happening is that, in the decorator's unit tests, #helpers returns nil.

I see one of two options, either:

  1. a conditional is added so that if view_context is empty, it reverts to ApplicationController::all_helpers. This is an okay solution but seems to be patching the code just to run the tests. Not horrible but not ideal.
  2. figure out a way, in the decorator's specs, give it a stubbed view context so it has access to everything it needs.

Thoughts?

@samuelhwong
Copy link

I'm getting some odd behaviour: h.current_user works as expected but only on the first access. When I view the page as a signed-in user it's fine, but then I sign out, the value of h.current_user remains. If I restart thin, it resets...

@jfelchner
Copy link
Contributor

Ok, I decided to go with #2 above. This is what I've come up with so far and it seems to be working okay after about 20 minutes of messing with it:

class DecoratorView < ActionView::Base
  include Rails.application.routes.url_helpers
  include ApplicationHelper

  def default_url_options
     {
      :host => #my host:port here,
      :port => #my port here
     }
  end
end

Then in my spec all I have to do is:

Thread.current[:current_view_context] = DecoratorView.new

The source for this was a SO question here: http://stackoverflow.com/questions/4262044/rails-3-how-to-render-erb-template-in-rake-task

The only other problem with doing it this way is that all of the custom application helpers need to be included manually. @jcasimir can we use some of the code that was originally used in #helper on Draper::Base and utilize that here to include all of the helpers automatically?

My initial thought was to put this in the README but I'm concerned that the 'knowledge of the masses' won't be respected there. In other words, if there's a problem, people are likely to change it in their app and not modify the README to be correct.

Now I'm thinking it'd be better off as DraperActionView or something which people can include in their specs. If there's a problem, it's more likely to garner an issue or PR this way.

@samuelhwong
Copy link

After implementing the code snippets by @numbers1311407, my issue is gone... thanks! It would be nice to have it work out of the box, though -- especially for newbie Rails devs like myself. ;)

@jcasimir
Copy link
Member

Arg, I see some issues that the changes have brought up. I will do my best to carve out some time tomorrow night.

@numbers1311407
Copy link

@jcasimir: I think the issue @rocketscientist was having with the persisting current_user can be solved by simply not trying to cache the view_context. It's set with ||= now, which would be problematic because the thread doesn't die after it's done handling a request.

@esdras
Copy link

esdras commented Oct 10, 2011

@numbers1311407 Yup, you're right, I had the same issue and "Thread.current[:current_view_context] = self.view_context" seems to solve the problem. I'm not familiar with the project though, Am I missing something?

@jfelchner
Copy link
Contributor

More notes regarding testing the decorators in isolation:

To use anything which requires configuration variables from the controller, you'll need to create a controller instance and pass that to your ActionView instance. Adding on to what I mentioned above, you would have:

class Draper::ApplicationController < ApplicationController
  def url_options
    {
      :host     => <whatever_your_host_is>,
      :protocol => 'http'
    }
  end
end

controller = Draper::ApplicationController.new
Thread.current[:current_view_context] = Draper::ActionView.new(nil, {}, controller)

Using this, you should be able to use things like image_tag without a problem in your decorator specs.

@numbers1311407
Copy link

@jfelchner What if you went at this from the opposite approach, delegating to a stubbed controller for view_context rather than instantiating a view manually? This more closely approximates the actual code and thus feels less artificial. It also solves a problem not yet noted: the absence of a request.

# something like...
before_my_tests do
  controller = WhateverController.new
  controller.request = ActionDispatch::Request.new('HTTP_HOST' => 'whateverhost.com')
  Thread.current[:current_view_context] = controller.view_context
end

@jcasimir
Copy link
Member

Looks like we've got this covered.

@jfelchner
Copy link
Contributor

@numbers1311407 Awesome. I'm not very familiar with how all that stuff is wired up. Your solution worked perfectly. Thank you!

@fpellanda
Copy link

For me, this works, quick and dirty:

class ApplicationDecorator < Draper::Base
  def helpers
    @helpers ||= super.instance_variable_get("@_controller").view_context
  end
  alias :h :helpers
end

@e9digital
Copy link

@fpellanda I don't understand what's happening here. I don't know which version you're using but in current versions super already does this work and is the view_context, no? Effectively this method would be saying:

def helpers
  view_context.controller.view_context
end

@fpellanda
Copy link

As far as I can see in my source code (draper 0.9.5)

module Draper
  class Base
  ....
  class << self
    def helpers
      Draper::ViewContext.current
    end
    alias :h :helpers
  end
end

And this would return:

Thread.current[:current_view_context]

Which has, in my application, has not the local variables from the controller set.

But your right, this would be nicer:

def helpers
  Draper::ViewContext.current.controller.view_context
end

@numbers1311407
Copy link

The point is that Draper::ViewContext.current _is_ the view_context for the current controller. It's set in a before_filter each request. Your override retrieves it, then retrieves its controller, which in turn returns the selfsame view_context, effectively doing nothing.

@fpellanda
Copy link

I know they should, but they are really not the same:

I'm using Rails 3.1.3 and draper 0.9.5.

How to reproduce:

rails new testapp; cd testapp
# Add this to gemfile: gem "draper"
bundle update
rails generate scaffold post title:string body:text published:boolean
rails generate draper:decorator Post
rake db:migrate

remove javascript stuff from application layout (gave me an error)

Add to post_decorator.rb

def title
  ("<br>" + helpers.instance_variables.inspect +
    "<br>" + Draper::ViewContext.current.controller.view_context.instance_variables.inspect).html_safe
end

Decorate @post in show action of posts_controller.rb:

def show
  @post = Post.find(params[:id])
  @post = PostDecorator.decorate(@post)
  ...

When you now create a post and goto show, you will see in the title, that the
the two objects are not the same. @post is missing.

My output of http://127.0.0.1:3000/posts/1 :

Title:
[:@_config, :@view_renderer, :@_routes, :@_assigns, :@_controller, :@_request, :@view_flow, :@output_buffer, :@virtual_path]
[:@_config, :@view_renderer, :@_routes, :@_assigns, :@_controller, :@_request, :@view_flow, :@output_buffer, :@virtual_path, :@post]
...

This means, if you want to use @post in a helper, it will be nil.

@numbers1311407
Copy link

This is a good catch. The problem you're seeing happens because the view_context itself is a snapshot, built on the fly.

def view_context
  view_context_class.new(lookup_context, view_assigns, self)
end

In most cases it's a non-issue because, well, the intention of helpers isn't to function as the view_context. The view_context helpers method was introduced to give access to the request context and controller-defined helpers.

In other words, if your decorators are dependent on action-specific instance variables, you're probably doing it wrong (in this case, @post is the model, and you'd access it as such).

_That being said_, the current implementation is problematic, if only because it introduces the possibility for inconsistent behavior based on filter order.

Perhaps it should only store a reference to the current controller, rather than caching a view_context snapshot, then helpers itself could invoke and cache view_context.

Something like my original suggestion:

# in the before filter
Draper::ViewContext.current_controller = self

# and
def helpers
  @helpers ||= Draper::ViewContext.current_controller.view_context
end

@fpellanda
Copy link

Yes I know that I shouldn't use @post in the helper - this was just an example, in my case I set an instance variable in a before filter to change behaviour in some helper methods.

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

No branches or pull requests

8 participants