Using view_context inside the Mailer - ruby-on-rails

I'm using ActiveJob to send mails:
Using deliver_now method:
invoices_controller.rb
def send_invoice
#other stuff
Members::InvoicesMailer.send_invoice(#invoice.id, view_context).deliver_now
end
invoices_mailer.rb
require 'open-uri'
class Members::InvoicesMailer < ApplicationMailer
def send_invoice(invoice_id, view_context)
#invoice = Invoice.find(invoice_id)
attachments["#{#invoice.identifier}.pdf"] = InvoicePdf.new(#invoice, view_context).render
mail :to => #invoice.client.email, :subject => "Invoice"
end
end
Notice here that I'm sending the view_context from the controller to the mailer, that will again pass it to the InvoicePdf class to generate the invoice.
Result: Email sent correctly
Using deliver_later method:
invoices_controller.rb
def send_invoice
#other stuff
Members::InvoicesMailer.send_invoice(#invoice.id, view_context).deliver_later
end
Result: ActiveJob::SerializationError in Members::InvoicesController#send_invoice Unsupported argument type: view_context.
How to inject the view_context inside the InvoicePdf, either loading it from inside InvoicePdf, or InvoiceMailer?
Edit: This is what the InvoicePdf looks like
invoice_pdf.rb
class InvoicePdf < Prawn::Document
def initialize(invoice, view_context)
#invoice, #view_context = invoice, view_context
generate_pdf
end
def generate_pdf
# calling some active_support helpers:
# #view_context.number_to_currency(//)
# calling some helpers I created
end
end

The problem with passing an object like the view context and then using deliver_later is that the parameters you give it are serialized to some backend (redis, MySQL), and another ruby background process picks it up later.
Objects like a view context are not really things you can serialize. It's not really data.
You can just use ActionView::Base.new, for example from rails console:
# New ActionView::Base instance
vagrant :002 > view = ActionView::Base.new
# Include some helper classes
vagrant :003 > view.class_eval { include ApplicationHelper }
vagrant :004 > view.class_eval { include Rails.application.routes.url_helpers }
# Now you can run helpers from `ApplicationHelper`
vagrant :005 > view.page_title 'Test'
"Test"
# And from url_helpers
vagrant :006 > view.link_to 'Title', [:admin, :organisations]
=> "Title"
Here's what I do in my PdfMaker class, which is probably similar to your InvoicePdf class.
def action_view
#action_view ||= begin
view = ActionView::Base.new ActionController::Base.view_paths
view.class_eval do
include Rails.application.routes.url_helpers
include ApplicationHelper
include FontAwesome::Rails::IconHelper
include Pundit
def self.helper_method *name; end
def view_context; self; end
def self.before_action f; end
def protect_against_forgery?; end
end
view
end
end

Related

Dynamic url in liquid template - Ruby On Rails

I am trying to add a dynamic link to email. The body of the emails is fetched and rendered using a liquid template.
I have added the dynamic link as below, but not sure if it's the most elegant way. Any help in this will be great. Below is the relevant part of the code.
class UserDrop < Liquid::Drop
def search_path
ActionController::Base.helpers.content_tag(
:a,
#user.email,
:href => Rails.application.routes.url_helpers.admin_users_url(
search: #user.email,
host: Rails.application.config.action_mailer.default_url_options[:host])
)
end
end
Liquid template code
Email: {{user.search_path}}
You can really clean this up with a drop of inheritance:
class BaseDrop < Liquid::Drop
# shamelessly stolen from
# http://hawkins.io/2012/03/generating_urls_whenever_and_wherever_you_want/
class Router
include Rails.application.routes.url_helpers
def self.default_url_options
ActionMailer::Base.default_url_options
end
end
private
def router
#router ||= Router.new
end
def helpers
#helpers ||= ActionController::Base.helpers
end
end
class UserDrop < BaseDrop
def search_path
helpers.link_to(#user.email, router.admin_users_url(search: #user.email))
end
end

How can you use a setup method in ActionMailer Previews?

I would like to avoid duplicating the setup for multiple mailer previews. What is the best way to clean this up?
class MyMailerPreview < ActionMailer::Preview
def email1
setup
mailer.email1
end
def email2
setup
mailer.email2
end
def email3
setup
mailer.email3
end
end
Here are two possible solutions I found:
There is something called preview_interceptors that are used when generating mailer previews, you could add your own like this:
config/environments/development.rb
config.action_mailer.preview_interceptors = :my_setup
test/mailers/previews/my_setup.rb
class MySetup
def self.previewing_email(message)
message.subject = "New subject"
end
end
test/mailers/previews/user_mailer_preview.rb
class UserMailerPreview < ActionMailer::Preview
include ActionMailer::Previews
register_preview_interceptor :my_setup
def welcome_email
UserMailer.with(user: User.first).welcome_email
end
end
The message parameter is an instance of ActionMailer::Parameterized::MessageDelivery, I am not sure everything you can do with it, but you can set some attributes on the email itself.
I couldn't find much documentation on preview interceptors, but here is a link to how they are used in Rails.
# Previews can also be intercepted in a similar manner as deliveries can be by registering
# a preview interceptor that has a <tt>previewing_email</tt> method:
#
# class CssInlineStyler
# def self.previewing_email(message)
# # inline CSS styles
# end
# end
#
# config.action_mailer.preview_interceptors :css_inline_styler
#
# Note that interceptors need to be registered both with <tt>register_interceptor</tt>
# and <tt>register_preview_interceptor</tt> if they should operate on both sending and
# previewing emails.
I tried to include Rails before_action in the class, but it wouldn't hook the methods in the previewer, so the second option I found is to build your own before_action like this:
module MySetup
def before_action(*names)
UserMailer.instance_methods(false).each do |method|
alias_method "old_#{method}", method
define_method method do
names.each do |name|
send(name)
end
send("old_#{method}")
end
end
end
end
class UserMailerPreview < ActionMailer::Preview
extend MySetup
def welcome_email
UserMailer.with(user: User.first).welcome_email
end
before_action :setup
private
def setup
puts "Setting up"
end
end
Use an initialize method.
Just override the parent initialize method, call super and then run your setup:
class MyMailerPreview < ActionMailer::Preview
def initialize( params = {} )
super( params )
#email_address = "jules#verne.com"
end
def email1
mailer.email1( #email_address )
end
end
You can view the ActionMailer::Preview.new method here as a reference.
Based on my understanding of what you're asking maybe you could add it into one single method that takes the mailer method as a param
class MyMailerPreview < ActionMailer::Preview
def email_for(emailx) # (Pass the method(email1, etc) as an argument where you're calling it
setup
mailer.send(emailx.to_sym) # Call the method param as a method on the mailer
end
end
Would that work for you?

Rails decorator method not being called in production, works in development

I add the following override for a controller that I inherit from a gem (Spree):
module Spree
module Admin
UsersController.class_eval do
def index
if params[:role].present?
included_users = Spree::User.joins(:role_users).
where( spree_role_users: { role_id: params[:role] } ).map(&:id)
flash[:notice] = "Filtered in #{included_users.count} users"
#users = #users.where(id: included_users)
end
end
end
end
end
Basically, it filters by an additional parameter on the Admin::UsersController controller. The source for that controller in the gem doesn't actually define the index method, so mine just gets called instead.
Now, this works perfectly well in development. However, in production, this method never gets called.
Is there something about class_eval that I'm not getting here? Shouldn't things like this work basically the same in production as they do in development?
Thanks for any help.
Decorators are objects that wrap another object. For example they are often used to wrap models with presentational logic.
class UserDecorator < SimpleDelegator
def full_name
"#{first_name} #{last_name}"
end
end
> #user = UserDecorator.new(User.new(first_name: 'John', last_name: 'Doe'))
> #user.full_name
=> "John Doe"
This is not a decorator method - you are just reopening the class and adding a method. This is known as monkey-patching.
Using class_eval in this case exactly the same in this as using the class keyword:
module Spree
module Admin
class UsersController
def index
if params[:role].present?
included_users = Spree::User.joins(:role_users).
where( spree_role_users: { role_id: params[:role] } ).map(&:id)
flash[:notice] = "Filtered in #{included_users.count} users"
#users = #users.where(id: included_users)
end
end
end
end
end
With monkey-patches the key is ensuring that your class redefinition is read. I'm guessing the difference between dev and production is due to class catching which prevents the class from being read from /app if it has already been defined by the Spree gem. config/application.rb uses bundler to require all the gems when the application starts up.
You can assure that a monkey-patch is loaded by placing it in config/initializers as all files in that directory are loaded on startup.
But a better alternative to monkeypatching may be to instead subclass the vendor controller and route to it:
class MyUsersController < ::Spree::Admin::UsersController
def index
if params[:role].present?
included_users = Spree::User.joins(:role_users).
where( spree_role_users: { role_id: params[:role] } ).map(&:id)
flash[:notice] = "Filtered in #{included_users.count} users"
#users = #users.where(id: included_users)
end
end
end
see also:
3 Ways to Monkey-Patch Without Making a Mess

Rspec Test Haml Template Output in Object Presenter

I am trying to figure out class/method/instantiation process I should use in Rspec to get a functioning view template that includes both the standard ActionView herlper methods and HAML helper methods.
I have implemented presenters in my rails app as is done in RailsCast #287
I have a presenter where I want to do some output that uses one of the HAML helpers.
Here is the base presenter:
class BasePresenter
attr_reader :object, :template
def initialize(object, template)
#object = object
#template = template
end
end
Here is an example of a theoretical presentable object and it's presenter class:
class FakeObject
def a_number
rand(10).to_s
end
end
class FakePresenter < BasePresenter
def output_stuff
# #content_tag is standard ActionView
template.content_tag(:span) do
# #succeed is from the haml gem
template.succeed('.') do
object.a_number
end
end
end
end
If I were to write a test for this in Rspec and only the #content_tag method were being used, this would work, as it's part of ActionView. However, #succeed is from the haml gem and isn't available with my current test.
Test:
require 'rails_helper'
describe FakeObjectPresenter do
let(:template){ ActionView::Base.new }
let(:obj){ FakeObject.new }
let(:presenter){ FakeObjectPresenterPresenter.new obj, template }
describe '#output_stuff' do
it{ expect(presenter.output_stuff).to match(/<span>\d+<\/span>/) }
end
end
Failures:
1) FakeObjectPresenter #output_stuff
Failure/Error:
template.succeed('.') do
object.a_number
NoMethodError:
undefined method `succeed' for #<ActionView::Base:0x00560e818125a0>
What class should I be instantiating as my template instead of ActionView::Base.new to get the haml methods available in the test?
If you'd like to easily play around with this, you can dump the below code into a spec.rb file in a rails app and run it as a demo:
require 'rails_helper'
# Define classes to be tested
class FakeObject
def a_number
rand(10).to_s
end
end
class BasePresenter
attr_reader :object, :template
def initialize(object, template)
#object = object
#template = template
end
end
class FakeObjectPresenter < BasePresenter
def output_stuff
# #content_tag is standard ActionView
template.content_tag(:span) do
# #succeed is from the haml gem
template.succeed('.') do
object.a_number
end
end
end
end
# Actual spec
describe FakeObjectPresenter do
let(:template){ ActionView::Base.new }
let(:obj){ FakeObject.new }
let(:presenter){ FakeObjectPresenter.new obj, template }
describe '#output_stuff' do
it{ expect(presenter.output_stuff).to match(/<span>\d+<\/span>/) }
end
end

How to automatically add and serialize virtual attribute to model object in rails module?

How to decorate Plugin object to add automatically virtual page_links attribute to attributes if Plugin name is SomePluggin?
Example:
#page.plugins
# As is => [#<Plugin id: 241, url: "some_url", page_id: 118>]
# As I want to be: => [#<Plugin id: 241, url: "some_url", page_links: "1234,main_page,articles", page_id: 118>]
Current code:
module Cms
class SomePluggin
def initialize(plugin)
#url = plugin.url
#it doesn't work
plugin.page_links = "1234,main_page,articles"
plugin.attributes.merge!("page_links" => "1234,main_page,articles")
#decorate = SimpleDelegator.new plugin
end
def get_content
puts "content"
end
end
module Pluggin
extend ActiveSupport::Concern
included do
after_initialize :pluggin
end
delegate :get_content, to: :pluggin
attr_writer :pluggin
def pluggin
#pluggin ||= "Cms::Pluggin::#{name}".camelize.constantize.new(self) # name=SomePluggin
end
end
end
Model:
class Plugin < ActiveRecord::Base
attr_accessor :page_links
belongs_to :page
include Cms::Pluggin
end
I assume page_links would be a virtual attribute. Your code structure is complicated, and you can basically add page_links and page_links= methods to class Plugin with initialization, but if you want to keep this attribute in SomePluggin, you can do it in this way:
module Cms
class SomePluggin
attr_accessor :page_links
def initialize
self.page_links = "1234,main_page,articles"
end
def get_content
puts "content"
end
end
module Pluggin
extend ActiveSupport::Concern
included do
after_initialize :wrap_object
end
def wrap_object
pluggin
end
delegate :get_content, :page_links, :page_links=, to: :pluggin
attr_writer :pluggin
def pluggin
#pluggin ||= SomePluggin.new
end
end
end
Here I've added :page_links and :page_links= methods to SomePluggin and initial value setting in initialize method.
Some console output:
p = Plugin.new
p.page_links # => "1234,main_page,articles"
p.page_links = '123'
p.page_links # => "123"

Resources