How to get the current controller instance - ruby-on-rails

I have a controller, that calls another class (say, a mutation) to perform some action. I want to be able to instruct this mutation to interact control flow, e. g. to redirect_to or to show flash notice. In pseudocode this looks like:
my_controller.rb
def create
MyCreateMutation.run!(params).tap do |result|
render result ? :success : :error
end
end
my_mutation.rb
def execute **params
begin
# do creation
rescue => e
# ⇓⇓⇓⇓⇓
flash[:error] = e.message
end
end
The problem with the code above is that flash is local to current controller. I know, that I can:
pass current controller instance to a mutation and call flash on it;
declare static current method on the very top application controller and use this “global” variable;
parse a caller array and constantize the topmost found controller;
introduce a middleware and save the current controller instance somewhere.
All the above looks like an overkill to me.
Is there a common way to get the current controller, if it is presented on stack (somewhere on the stack there is a method of ApplicationController)?

Well, for future visitors: I finally stuck to declaring current class variable on the very top ApplicationController:
cattr_accessor :current
before_filter { ApplicationController.current = self }
after_filter { ApplicationController.current = nil }
After that, I have a current controller instance available via:
ApplicationController.current

Related

Set a reference field for all saved objects

My application has a Group object, of which a user has many. The navbar displays which group is currently selected, and the objects displayed on various pages are based on this. A number of my models have 'group_id' fields, and I'd like for these fields to be populated with the id of the currently selected group when they're saved.
In my application controller I have a helper_method which returns the current_group however this can't and shouldn't be accessed from a model which is the DRYest way I could think of doing this.
#inhereted_model.rb
before_save :assign_group_reference
def assign_group_reference
self.group_id = current_group.id
end
Is there an efficient and DRY way to do this that I'm missing?
You are right; any controller helper-methods cannot and should not be accessed directly from a model method.
I think a standard DRY way is to set the parameter of a model in your Controller methods. For example, do as follows in your Controller(s):
# In a Controller
def my_helper(mymodel)
mymodel.group_id = current_group
# where current_group is your Controller helper method to obtain the group name.
end
def create # or update etc.
#mymodel = set_my_model # your arbitrary method to set a model
my_helper(#mymodel)
respond_to do |format|
if #mymodel.save
format.html { redirect_to #mymodel, notice: 'Success.' }
else
raise
end
end
end
If you want, you can write my_helper (which in this case takes no argument and sets the instance variable #mymodel instead of a local variable) in before_action in combination with only or except, where you make sure the method is called after a model #mymodel is set, in order to avoid calling my_helper repeatedly in many methods in the Controller.
Alternatively, if you really want to set it at a model level for some reason, a potential workaround is to use a Ruby Thread variable, like the following.
# In a controller
def create
model = set_my_model # Arbitrary routine to set a model
Thread.new{
Thread.current.thread_variable_set(:grp, current_group)
# where current_group is your Controller helper method to obtain the group name.
model.save!
# In the model, you define a before_save callback
# in which you write something like
# self.group_id = Thread.current.thread_variable_get(:grp)
}.join
end
But I think this is a little dirty hack and I would avoid it in principle.

How can I pass in a variable defined in a class into a Rails form?

If I have a controller
class MyController < ApplicationController
vals = [...]
def new
...
end
def create
if save
...
else
render 'new'
end
end
how can I make the "vals" variable accessible to both methods? In my "new" view I want to use the "vals" variable for a drop-down menu, but rails is giving me errors. Of course, I could just copy the variable twice, but this solution is inelegant.
As Sebastion mentions a before_ hook / callback is one way to go about it, however as you mentioned it is for a dropdown menu, I am guessing it is a non-changing list, if so I would suggest perhaps using a Constant to define the values, perhaps in the model they are specific to, or if it is to be used in many places a PORO would do nicely to keep things DRY. This will then also allow you to easily access it anywhere, for example in models for a validation check, or to set the options of the dropdown menu in the view, or in the controller if you so wish:
class ExampleModel
DROPDOWN_VALUES = [...].freeze
validates :some_attr, inclusion: { in: DROPDOWN_VALUES }
end
class SomeController < ApplicationController
def new
# can call ExampleModel::DROPDOWN_VALUES here
end
def create
# also here, anywhere actually
end
end
You could use a before_* callback, e.g a before_action, this way you sets your vals variable as an instance one and make it to be available for your both new and create methods, something like:
class SomeController < ApplicationController
before_action :set_vals, only: [:new, :create]
def new
...
# #vals is available here
end
def create
if save
...
# and here
else
render 'new'
end
end
private
def set_vals
#vals = [...]
end
end
A different way from the ones before (although probably just having the instance method is preferred as in Sebastian's solution) is, take advantage of the fact that functions and local variables are called in the same way in ruby and just write:
def vals
#vals ||= [...]
end
and you should be able to access it on the controllers (not the views). If you want it on your views as well you can call at the beginning of the controller
helper_method :vals
If you want to be able to modify vals using vals="some value"
def vals= vals_value
#vals = vals_value
end
Take into account that probably using the intance variable as in Sebastian's solution is preferred, but if you, for whatever reason, are settled on being able to call "vals" instead of "#vals" on the view (for example if you are using send or try), then this should be able to do it for you.
Define in corresponding model
Eg :
class User < ActiveRecord::Base
TYPES = %w{ type1 type2 type3 }
end
and use in ur form like
User::TYPES
=> ["type1", "type2", "type3"]
You can reuse this anywhere in the application.

Rails Presenter block method

EDIT:
I got many responses with different approaches for solving the problem, thanks a lot!
Sadly, none of them worked until now.
To easily understand and reproduce the failure, I created a small Rails repo on GitHub with a Rspec suite.
One of the specs is passing (where the presenter is initialized in the view).
One of the specs is failing (where the presenter is initialized in the controller).
How make them both pass ?
ORIGINAL QUESTION BELOW:
This is my Presenter:
class UserPresenter
def initialize(user, vc)
#user = user
#vc = vc
end
def linkify()
#
# HERE IS THE PROBLEM
#
vc.link_to("foo") do
yield
end
end
end
This is my Controller:
I initialize my Presenter in the controller, passing the view context of the controller with the presented model.
class UserController
def show
#user = User.find(#.....
#presenter = UserPresenter.new(#user, view_context)
end
end
In my Slim template, I call my Presenter to put the content in a link:
=#presenter.linkify do
p "123"
My problem is, I can't pass the block from the view to my linkify method.
In the with comment marked above code, the passed block is the whole view content, instead of the p 123.
When I initialize my Presenter in the view via: #presenter = UserPresenter.new(#user, self), it works as expected.
How I can make the linkify method uses the provided block, without initializing the presenter in the view ?
Because if you are going to use the yield command, you mustn't specify the &block, since now you are effectively receiving a block as a parameter using normal parameter syntax.
class UserPresenter
def initialize(user, vc)
#user = user
#vc = vc
end
def linkify() # <-- Remove &block
vc.link_to("foo") do
yield
end
end
end
# ...
# Somewhere else, assuming you have access to #presenter which is an instance of
# UserPresenter
# ...
def show
#presenter.linkify do
# ...
# do my view stuff here
# ...
end
end
show()
# Now, if your "View" is nothing but a block that needs to get passed in
# then you'd do this...
def show(&block)
#presenter.linkify do
block.call()
end
end
# This would be used this way:
show(lambda {
# ...
# View stuff here
# ..
})
As specified in lacrosse's answer. The wrong view context is the root of this cause. I tried to make a work around for your situation. And, this is how ended up doing it:
I created a helper method in ApplicationHelper:
module ApplicationHelper
def link(presenter)
presenter.linkify(self) do
yield
end
end
end
changed linkify() to:
def linkify(vc)
vc.link_to("foo") do
yield
end
end
which means, no need to have vc in presenter's class constructer, or you can update the vc in link method defined in the helper(your choice).
views are now looks something like this:
presenter_from_view.html.slim:
-#presenter = UserPresenter.new(#user, self)
=link #presenter do
p 123
presenter_from_controller.html.slim:
=link #presenter do
p 123
I agree, maybe this is not how you wanted your solution to be done. But, I couldn't get any cleaner work around for this. However, here you don't have to worry about passing self in views wherever you use link #presenter do..(which may become too much for writing code when you use linkify in multiple views I guess).
P.S.: Your all specs are passing now. And, if you need the modified code then I can push it to your repository in a separate branch. Let me know.
From Slim's documentation on Helpers and Capturing:
module Helpers
def headline(&block)
if defined?(::Rails)
# In Rails we have to use capture!
"<h1>#{capture(&block)}</h1>"
else
# If we are using Slim without a framework (Plain Tilt),
# this works directly.
"<h1>#{yield}</h1>"
end
end
end
Can you try using capture as follows?
def linkify(&block)
result = capture(&block)
vc.link_to("foo") do
result
end
end
The wrong view context is causing this issue. Just change UserPresenter#initialize to not accept view context, initialize presenter in the controller and pass the correct view context from the view instead, like so:
= #presenter.linkify(self) do
p "123"
What error are you getting? Just looking at the code...
In this method
def linkify()
#
# HERE IS THE PROBLEM
#
vc.link_to("foo") do
yield
end
end
where is vc defined?
I think you mean #vc which is the instance variable you're initializing.
Also as a side note... the empty () in linkify() are redundant in a ruby method with no variables. You can eliminate them.
Also you may want to take a look at the cells gem. As you're basically mirroring this behavior in your presenters and IMO cells is a cleaner way of accomplishing this in rails.
I think I figured out WHY it's not working. When you pass in the view_context in the controller, it's rendering the view_context once when you pass it in to the presenter, and then you again when you actually render the view.
def initialize(user, vc)
#user = user
#vc = vc
end
# When called from the view:
#presenter = UserPresenter.new(#user, self)
# You're passing the view directly in "as is" and renders as expected.
# However, when you pass it in from the controller:
#presenter = UserPresent.new(#user, view_context)
# you're essentially rendering the view_context in the controller, and then again
# once it renders at the end of your action. That's why you're getting this:
# "<p>123</p><p>123</p>"
You'll either need to send self in from the view each time you call linkify, or you can use an application helper method that will always have the view context.

Ruby syntax, semantic questions def status=(status)

I was looking at this code and was trying to figure what def status=(status) means. I have never seen that before.
class Tweet
attr_accessor :status
def initialize(options={})
self.status = options[:status]
end
def public?
self.status && self.status[0] != "#"
end
def status=(status)
#status = status ? status[0...140] : status
end
end
I'll try answering this in layman's terms, since I didn't understand this when starting out.
Let's say you want the Tweet class to have an attribute status. Now you want to change that attribute, well you can't since it's hidden inside the class. The only way you can interact with anything inside a class is by creating a method to do so:
def status=(status)
#status = status # using # makes #status a class instance variable, so you can interact with this attribute in other methods inside this class
end
Great! Now I can do this:
tweet = Tweet.new
tweet.status = "200" # great this works
# now lets get the status back:
tweet.status # blows up!
We can't access the status variable since we haven't defined a method that does that.
def status
#status # returns whatever #status is, will return nil if not set
end
Now tweet.status will work as well.
There are shorthands for this:
attr_setter :status #like the first method
attr_reader :status # like the second one
attr_accessor :status # does both of the above
That is a setter - the method to be called when you say thing.status = whatever.
Without such a method, saying thing.status = whatever would be illegal, since that syntax is merely syntactic sugar for calling the setter.
It means exactly the same thing that def foo always means: define a method named foo.
def initialize
Defines a method named initialize.
def public?
Defines a method named public?
def status=
Defines a method named status=
That's it. There's absolutely nothing special going on here. There is no magic when defining a method whose name ends in an = sign.
The magic happens when calling a method whose name ends in an = sign. Basically, you are allowed to insert whitespace in between the = sign and the rest of the method name. So, instead of having to call the method like this
foo.status= 42
You can call it like this:
foo.status = 42
Which makes it look like an assignment. Note: it is also treated like an assignment in another way; just like with all other forms of assignments, assignment expressions evaluate to the value that is being assigned, which means that the return value of the method is ignored in this case.

Why do my controller's instance variables not work in views (Rails)

I would like to add a couple of instance variables to my controller, since the variables in question are required from within more than one action's view. However, the below example does not work as I would expect.
class ExampleController < ApplicationController
#var1 = "Cheese"
#var2 = "Tomato"
def show_pizza_topping
# What I want is the above instance vars from within the view here
end
def show_sandwich_filling
# What I want is the above instance vars from within the view here
end
end
As I understand it, Rails takes the instance variables from the controller and makes them available in the view. If I assign the same variables within the action methods, it works fine - but I don't want to do it twice. Why does my way not work?
(Note: this is a bit of a rubbish example, but I hope it makes sense)
EDIT: I have found the answer to this question here: When do Ruby instance variables get set?
EDIT 2: when is the best time to use filters such as before_filter and the initialize method?
These types of things should be handled in a before_filter. A before filter, like the name implies, is a method that will get called before any actions, or only the ones you declare. An example:
class ExampleController < ApplicationController
before_filter :set_toppings
def show_pizza_topping
# What I want is the above instance vars from within the view here
end
def show_sandwich_filling
# What I want is the above instance vars from within the view here
end
protected
def set_toppings
#var1 = "Cheese"
#var2 = "Tomato"
end
end
Or, you could have your before_filter only work on one of your actions
before_filter :set_toppings, :only => [ :show_pizza_topping ]
Hope this helps.
EDIT: Here's some more information on filters in ActionController.
Those aren't instance variables, are they?
class A
#x = 5
def f
puts #x
end
end
A.new.f
=> nil
You're defining it at the class-level, not the instance-level. As "theIV" points out, you need to assign them inside an instance method.

Resources