Dynamic url in liquid template - Ruby On Rails - 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

Related

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?

How do you reference another class from a controller in ruby on rails?

I am trying to use a service class inside of my controller. I cannot figure out how to reference other classes in rails. Here is my users_controller, located under app/controllers/api/v1:
class Api::V1::UsersController < ApplicationController
respond_to :json
skip_before_filter :verify_authenticity_token
before_filter :initialize_user_service
def create
#service.create
render json: CustomResponse.new(
{
user: #service.user,
}
)
end
def initialize_user_service
#service = Services::UserService.new(user_params)
end
def user_params
params.permit(:email, :password, :password_confirmation, :first_name, :last_name)
end
end
I am attempting to reference this users_service.rb class located in app/services/users_service.rb:
class Services::UserService
attr_reader :session, :user
def initialize(params)
# #user_id = params[:id]
#params = params
end
def create
user = User.create(#params)
if user.save
return user
else
false
end
end
end
But I get the following error in the console:
NameError (uninitialized constant Api::V1::UsersController::Services):
Does anyone know what I can do to access this class? I cannot find a solution. I imagine it may have something to do with my config/application.rb file, but I am not sure what.
I think, problem is with your filename: app/services/users_service.rb. Since your class name is UserService, it should rather be: app/services/user_service.rb (user_service instead of users_service).
UserService will expect to load the class from a file called as user_service.rb in autoload path.
Define your service as following:
Filename: app/services/user_service.rb.
Definition:
module Services
class UserService
attr_reader :session, :user
def initialize(params)
# #user_id = params[:id]
#params = params
end
def create
user = User.create(#params)
if user.save
return user
else
false
end
end
end
end
Services::UserService.new(user_params) should work as intended.
Should be ::Services::UserService.new(user_params), Ruby assume you declare this class in the Api::V1 module, :: means the root module. Or you can declare your service like this:
class Api::V1::Services::UserService
end
I think it is because of defined namespace. It will work by simply putting your service class directly in service directory is enough. Check for ActiveSupport::Dependencies.autoload_paths for a basic idea of which folders are included. Even if you are using namespace then you need to update for the autoload path with it.
something like below
config.autoload_paths += %W( #{config.root}/app/your_xyz_path )
Defining service like this is enough. There's no need namespace for this:
class UserService
end
# using
#service = UserService.new(user_params)
No need to define namespace, just write it with your service name, like following
class UserService
attr_reader :session, :user
def initialize(params)
# #user_id = params[:id]
#params = params
end
def create
user = User.create(#params)
if user.save
return user
else
false
end
end
end
And call it using UserService.new(params)
And for production mode define it in application.rb file to autoload the service like following
config.autoload_paths += Dir[Rails.root.join('app', 'services')]
The code under app directory is autoloaded and all the directories too. So there should be no namespace in your service class.
class UserService
end
and this should work in any controller or model/class like
user_service = UserService.new

Get root_url in a Rails service

The app I'm working on makes heavy use of Rails services. My problem is I need to get the root url of the app, similar to how you would use root_url in a view, but this doesn't work in a service. Does anyone know a way to do this other than entering the url in each of my environment setting files?
Edit
I tried using Rails.application.routes.url_helpers.root_url as it suggests to do here stackoverflow.com/a/5456103/772309 but it expects you to pass the :host => ... in as a parameter. That's what Im trying to find.
Based on what I've read from the linked 'Rails services' article, the services are just plain old ruby objects. If that's the case, then you'd need to pass the root_url from the controller to the initializer of your service object. To extend the example from that article:
UsersController
class UsersController < ActionController::Base
...
private
...
def register_with_credit_card_service
CreditCardService.new({
card: params[:stripe_token],
email: params[:user][:email],
root_url: root_url
}).create_customer
end
end
CreditCardService
class CreditCardService
def initialize(params)
...
#root_url = params[:root_url]
end
end
EDIT: Alternative solution that leverages the Rails.application.config
class UsersController < ActionController::Base
before_filter :set_root_url
def set_root_url
Rails.application.config.root_url = root_url
end
end
class CreditCardService
def some_method
callback_url = "#{Rails.application.config.root_url}/my_callback"
end
end
Since you're opposed to putting it in your environment folders you could do something like below in your App controller
class ApplicationController < ActionController::Base
def default_url_options
if Rails.env.production?
{:host => "myproduction.com"}
else
{}
end
end
end

Using view_context inside the Mailer

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

Virtual attributes in plugin

I need some help with virtual attributes. This code works fine but how do I use it inside a plugin. The goal is to add this methods to all classes that uses the plugin.
class Article < ActiveRecord::Base
attr_accessor :title, :permalink
def title
if #title
#title
elsif self.page
self.page.title
else
""
end
end
def permalink
if #permalink
#permalink
elsif self.page
self.page.permalink
else
""
end
end
end
Thanks
You can run the plugin generator to get started.
script/generate plugin acts_as_page
You can then add a module which defines acts_as_page and extends it into all models.
# in plugins/acts_as_page/lib/acts_as_page.rb
module ActsAsPage
def acts_as_page
# ...
end
end
# in plugins/acts_as_page/init.rb
class ActiveRecord::Base
extend ActsAsPage
end
This way the acts_as_page method is available as a class method to all models and you can define any behavior into there. You could do something like this...
module ActsAsPage
def acts_as_page
attr_writer :title, :permalink
include Behavior
end
module Behavior
def title
# ...
end
def permalink
# ...
end
end
end
And then when you call acts_as_page in the model...
class Article < ActiveRecord::Base
acts_as_page
end
It will define the attributes and add the methods. If you need things to be a bit more dynamic (such as if you want the acts_as_page method to take arguments which changes the behavior) try out the solution I present in this Railscasts episode.
It appears that you want a Module for this
# my_methods.rb
module MyMethods
def my_method_a
"Hello"
end
end
The you want to include it into the classes you want to use it for.
class MyClass < ActiveRecord::Base
include MyMethods
end
> m = MyClass.new
> m.my_method_a
=> "Hello!"
Take a look here for more information on mixing in modules. You can put the module wherever in a plugin if you like, just ensure its named correctly so Rails can find it.
Create a module structure like YourPlugin::InstanceMethods and include it this module like this:
module YourPlugin
module InstanceMethods
# your methods
end
end
ActiveRecord::Base.__send__(:include, YourPlugin::InstanceMethods)
You have to use __send__ to make your code Ruby 1.9 compatible. The __send__ line is usually placed at the init.rb file on your plugin root directory.

Resources