Extend view with decorators - ruby-on-rails

I am using a ActiveJob object to generate a PDF with wicked_pdf. To do this I need to extend some view things to be able to do this is from a Job object:
view = ActionView::Base.new(ActionController::Base.view_paths, {})
view.extend(ApplicationHelper)
view.extend(Rails.application.routes.url_helpers)
view.extend(Rails.application.routes.url_helpers)
This is all working fine, the PDF is created. But in the view I use decorators like this:
- decorate invoice do |decorates|
= decorates.title
And that is not working, probably because the object is not aware of these decorators, which are just PORO objects. So I guess I need to extend them too so they can be used here. How can I extend them? They are located in app\decorators
Edit:
The decorates method comes from a helper method:
module ApplicationHelper
def decorate(object, klass = nil)
unless object.nil?
klass ||= "#{object.class}Decorator".constantize
decorator = klass.new(object, self)
yield decorator if block_given?
decorator
end
end
end
And that helper method loads the correct decorator object:
class InvoiceDecorator < BaseDecorator
decorates :invoice
end
Which inherits from Base decorator:
class BaseDecorator
def initialize(object, template)
#object = object
#template = template
end
def self.decorates(name)
define_method(name) do
#object
end
end
end

Related

Prevent view to access models

To enforce separation of concerns and prevent dirty hacks in the views of a big project, I would like to inject the data in the views (through the controller for instance), and that the view would then not be able to access models, or any class of the project (only the data structure that has been injected).
With Rails, how can we prevent the embedded Ruby code in the views to access other parts of the project?
Ok. Here's the nut of it. (This is incomplete code and is meant to indicate direction, I've removed a lot, so you'll have to fill in the blanks.)
First, I create a module called ActsAs::Rendering. This provides an instance of ActionView::Base, which is the key to rendering anywhere.
module ActsAs::Rendering
private
def action_view() #action_view ||= new_action_view end
def new_action_view
av = ActionView::Base.new
av.view_paths = ActionController::Base.view_paths
av.class_eval do
include Rails.application.routes.url_helpers
include ApplicationHelper
end
av
end
def method_missing(meth, *params, &block)
if action_view.respond_to?(meth)
action_view.send(meth, *params, &block)
else
super
end
end
def render_partial(file_ref)
render(partial: "#{file_ref}", locals: {presenter: self})
end
end
Then, I create a PresenterBase that includes ActsAs::Rendering:
def PresenterBase
include ActsAs::Rendering
class << self
def present(args={})
new(args).present
end
end # Class Methods
#==============================================================================================
# Instance Methods
#==============================================================================================
def initialize(args)
#args = args
end
private
end
And now I create a Presenter class that implements present.
def FooPresenter < PresenterBase
#==============================================================================================
# Instance Methods
#==============================================================================================
def present
render_partial 'path/to/foo/partial'
# or do a lot of other cool stuff.
end
end
And my views all begin with:
- #presenter = local_assigns[:presenter] if local_assigns[:presenter]
And now, the view no longer has access to anything except its presenter.
* NOTE *
There's a bit more, but I have to run out. I'll update later.

Rails 5: controller action in module

I realize this is against MVC principals and all, but how can I access the controller name/method in a module that is included in a class?
module DocumentHTMLBoxes
extend ActiveSupport::Concern
included do
def icon_or_text(variable)
if controller.action_name == 'send'
"<b>#{variable.name.capitalize}</b> #{variable.text}\n"
else
variable.format!
end
end
end
end
How can I access controller and/or controller.action_name in the module?
In Rails the view_context object contains all the ivars from the controller and includes all the helpers. It also provides access to the session, cookies and request. It is the implicit self when you are rendering templates.
Models do not have access to the view context - this is a conscious design as it gives a good seperation of concerns.
If you want to break the encapsulation you need to pass the context to the model.
module DocumentHTMLBoxes
extend ActiveSupport::Concern
included do
def icon_or_text(variable, context)
if context.action_name == 'send'
"<b>#{variable.name.capitalize}</b> #{variable.text}\n"
else
variable.format!
end
end
end
end
class Thing < ApplicationModel
include DocumentHTMLBoxes
end
Congratulations, you just created some really smelly code.
But, this is a really bad idea since it just adds one more responsibility to your models which are already near god-class status in Rails. Don't add generating HTML (a view/helper responsiblity!) to that list.
Instead you should just create a simple helper method:
module BoxesHelper
def icon_or_text(obj)
if context.action_name == 'send'
"<b>#{obj.name.capitalize}</b> #{obj.text}\n"
else
obj.format!
end
end
end
Or a decorator:
# #see https://ruby-doc.org/stdlib-2.1.0/libdoc/delegate/rdoc/Delegator.html
class Decorator < Delegator
attr_accessor :object
attr_accessor :context
def intialize(obj, cxt)
#object = obj
#context = cxt
super(obj) # pass obj to Delegator constructor, required
end
# Required by Delegator
def __getobj__
#object
end
def self.decorate(collection, context)
return collection.map { |record| self.new(record, context) }
end
end
module DocumentHTMLBoxes
extend ActiveSupport::Concern
included do
def icon_or_text(variable)
if context.action_name == 'send'
"<b>#{object.name.capitalize}</b> #{object.text}\n"
else
object.format!
end
end
end
end
class ThingDecorator < Decorator
include DocumentHTMLBoxes
end
To decorate a bunch of records in the controller you would do:
#things = ThingDecorator.decorate( Thing.all, self.view_context )
And now you can call icon_or_text on the decorated model:
<% #things.each do |t| %>
<% t.icon_or_text %>
<% end %>
I`d recommend refactor the code to make it cleaner:
Controller level call would be like
model_item.icon_or_text(variable, action_name)
The module
module DocumentHTMLBoxes
extend ActiveSupport::Concern
included do
def icon_or_text(variable, action_name)
if action_name == 'send'
"<b>#{variable.name.capitalize}</b> #{variable.text}\n"
else
variable.format!
end
end
end
end
Assuming you are including this concern into the controller then inside the icon_or_text method self is the controller. You will just be able to call action_name without prefixing it with controller.
module DocumentHTMLBoxes
extend ActiveSupport::Concern
included do
def icon_or_text(variable)
if action_name == 'send'
"<b>#{variable.name.capitalize}</b> #{variable.text}\n"
else
variable.format!
end
end
end
end

Not able to access controller instance variable in rails helper?

I am trying to access helper method in my controller using helpers like below:
class MyController < ApplicationController
def index
#foo = 'bar'
helpers.my_helper_method
end
end
Inside Helper method, I am trying to access an instance variable of controller
module MyHelper
def my_helper_method
#some manipulation on foo
#foo.to_i
end
end
But in above scenario #foo is nil. When I call the same method from view, #foo is available. So the instance variable can be passed to helper method only through UI or some other way is there?
Thanks in advance.
UPDATE:
view_context
seems like reasonable solution https://apidock.com/rails/AbstractController/Rendering/view_context
class MyController < ApplicationController
def index
#foo = 'bar'
helpers.my_helper_method(#foo)
end
end
module MyHelper
def my_helper_method(foo)
#some manipulation on foo
foo.to_i
end
end
pass it as argument.
You can access instance variables that you set in a controller in your helpers. If the value is nil, then you need to deal with it in your helper:
module SomeHelper
def do_something
return 0 if !#value
value * 3
end
end
class SomeController
def index
#value = 1
helpers.do_something
end
def show
#value = nil
helpers.do_something
end
end

Expose controller's class method to view helper method?

How do I make the following method available in the view layer?
# app/controllers/base_jobs_controller.rb
class BaseJobsController < ApplicationController
def self.filter_name
(controller_name.singularize + "_filter").to_sym
end
end
I want to use it in a view helper like this:
module ApplicationHelper
def filter_link(text, options = {})
# Need access to filter_name in here....
end
end
Instead of helper_method, I prefer to include such functionality in a module.
module BaseJobsHelp
def filter_name
(controller_name.singularize + "_filter").to_sym
end
end
Then include the module in the BaseJobsController and ApplicationHelper.
class BaseJobsController < ApplicationController
include BaseJobsHelp
# ...
end
module ApplicationHelper
include BaseJobsHelp
def filter_link(text, options = {})
# You can access filter_name without trouble
end
end
Depending on the content of your methods in the module, you may need to use an alternative method to access certain data (ie. the current controller's name).

Helper method not exposed to controller Rails

Here's the code:
class SyncController < ApplicationController
def get_sync
#passed_ids = params[:ids].split(',')
#users = #passed_ids.collect{|id| User.find(id)}
#add the current user to the list
#users << current_user
#recommendations = get_recommendations(#users)
end
end
module SyncHelper
def get_recommendations(users)
users
end
end
I'm getting a can't find get_recommendations method error...
Your SyncHelper module needs to be included into your SyncController class. You can either add the line
include SyncHelper
in your class definition, or, if SyncHelper lives in the expected app/helpers file, you can use the rails helper method.

Resources