Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails 4.2: using deliver_later with a tableless model

I am trying to setup a contact form using Rails 4.2's deliver_later method. However, I can only get deliver_now to work, as deliver_later is trying to serialize my object and fails each time.

Here's my setup:

messages_controller.rb

class MessagesController < ApplicationController
  def new
    @message = Message.new
  end

  def create
    @message = Message.new(params[:message])
    if @message.valid?
      ContactMailer.contact_form(@message).deliver_later
      redirect_to root_path, notice: "Message sent! Thank you for contacting us."
    else
      render :new
    end
  end
end

contact_mailer.rb

class ContactMailer < ApplicationMailer
  default :to => Rails.application.secrets['email']

  def contact_form(msg)
    @message = msg
    mail(:subject => msg.subject, from: msg.email)
  end
end

message.rb

class Message
    include ActiveModel::Model
    include ActiveModel::Conversion

    ## Not sure if this is needed ##
    include ActiveModel::Serialization

    extend ActiveModel::Naming

    attr_accessor :name, :subject, :email, :body

    validates_presence_of :email, :body
    validates_format_of :email, with: /\A([^\s]+)((?:[-a-z0-9]\.)[a-z]{2,})\z/i
    validates_length_of :body, :maximum => 1000

    def initialize(attributes = {})
      attributes.each { |name, value| send("#{name}=", value) }
    end

    ## Not sure if this is needed ##
    def attribtues
      {'name' => nil, 'subject' => nil, 'email' => nil, 'body' => nil}
    end
end

The error I get when calling ContactMailer.contact_form(@message).deliver_later is:

ActiveJob::SerializationError in MessagesController#create 

Unsupported argument type: Message
Extracted source (around line #10): 
if @message.valid?
  ContactMailer.contact_form(@message).deliver_later
  redirect_to root_path, notice: "Message sent! Thank you for contacting us."
else
  render :new

Ideally I'd like this to be a background process. I will be adding something like Sidekiq soon but I think it's best I get this serialization problem fixed beforehand.

Any help is appreciated! Thanks :)

like image 598
DaniG2k Avatar asked Jan 12 '15 08:01

DaniG2k


3 Answers

In order to use your class with ActiveJob (that's what deliver_later delegates to), it needs to be able to uniquely identify the object by its ID. Further, it needs to find it later by the ID when deserializing (no manual deserialize is necessary in the mailer / job).

class Message
  ...
  include GlobalID::Identification
  ...

  def id
    ...
  end

  def self.find(id)
    ...
  end
end

ActiveRecord would provide you with these methods but since you're not using it, you need to implement it yourself. It's up to you to decide where you want to store the record but honestly I think you'd be better off by using ActiveRecord and the table underneath.

like image 180
Jiří Pospíšil Avatar answered Nov 09 '22 20:11

Jiří Pospíšil


A simple solution that avoids having to back the object with ActiveRecord or create an unnecessary table:

Instead of passing the Message object to the contact_form method, you can also pass the message params to the contact_form method and then initialize the Message object inside that method.

This will solve the problem without having to create a table, because you are initializing the object in the delayed job worker's memory space.

For example:

messages_controller.rb

MessagesController < ApplicationController
    def new
        @message = Message.new
    end

    def create
        @message = Message.new(params[:message])

        if @message.valid?
            ContactMailer.contact_form(params[:message]).deliver_later
            redirect_to root_path, notice: "Message sent! Thank you for contacting us."
        else
            render :new
        end
    end
end

contact_mailer.rb

class ContactMailer < ApplicationMailer
    default :to => Rails.application.secrets['email']

    def contact_form(msg_params)
        @message = Message.new(msg_params)
        mail(:subject => msg.subject, from: msg.email)
    end
end
like image 41
npostolovski Avatar answered Nov 09 '22 19:11

npostolovski


I had a similar problem today and solved it as follows.

  1. Convert a tableless object into a JSON sting
  2. Pass it to a mailer
  3. Convert the json string to hash

Environment

  • Rails 5.0.2

messages_controller.rb

class MessagesController < ApplicationController

  # ...

  def create
    @message = Message.new(message_params)
    if @message.valid?
      ContactMailer.contact_form(@message.serialize).deliver_later
      redirect_to root_path, notice: "Message sent! Thank you for contacting us."
    else
      render :new
    end
  end

  # ...
end

contact_mailer.rb

class ContactMailer < ApplicationMailer
  default :to => Rails.application.secrets['email']

  def contact_form(message_json)
    @message = JSON.parse(message_json).with_indifferent_access

    mail(subject: @message[:subject], from: @message[:email])
  end
end

message.rb

class Message
  include ActiveModel::Model

  attr_accessor :name, :subject, :email, :body

  validates_presence_of :email, :body
  validates_format_of :email, with: /\A([^\s]+)((?:[-a-z0-9]\.)[a-z]{2,})\z/i
  validates_length_of :body, :maximum => 1000

  # Convert an object to a JSON string
  def serialize
    ActiveSupport::JSON.encode(self.as_json)
  end
end

Hope this will help anybody.

like image 4
mnishiguchi Avatar answered Nov 09 '22 21:11

mnishiguchi