I have a method to dynamically call a mailer that look like this :
def mail
application = mail[:application].capitalize
class_template = mail[:template].split('/').[0]+ 'Mailer'
template = mail[:template].split('/').second
email_params = mail[:params].values.map(&:inspect).join(', ')
"#{application}::#{class_template}.#{template}(#{email_params}).deliver_now"
# Here I have something like : "Application::TemplatetMailer.test_name(\"John\", \"Doe\").deliver_now"
end
How can I have something like :
Application::TemplatetMailer.test(\"John\", \"Doe\").deliver_now
instead of
"Application::TemplatetMailer.test(\"John\", \"Doe\").deliver_now"
You can look up arbitrary constants with constantize:
mailer = [ application, class_template ].join('::').constantize
mailer.send(template, *email_params).deliver_now
Be extremely careful with what access you allow to end users. Do not expose this in a way that allows them to make arbitrary method calls on arbitrary classes. Having an allow-list of classes and methods is way safer.
Related
I'm attempting to restrict an API's content type in a RoR application, with a method that gets inherited by all controllers.
CONTENT_TYPE = 'application/vnd.api+json'
def restrict_content_Type
return if request.content_type = CONTENT_TYPE
render_content_type_error
end
this works fine, but now I must use a different content type for a single endpoint and controller, and I'd like to just change the content of the CONTENT_TYPE constant while reusing the code I already have. To use a different constant I must use a reader method that looks up the constant in the current controller.
I refactored the code into:
def get_content_type
self::CONTENT_TYPE
end
def restrict_content_type
return if request.content_type == get_content_type
...
end
The reason I've used a get_* reader is that self.content_type returns the Request's content type: https://api.rubyonrails.org/classes/ActionDispatch/Response.html#method-i-content_type
At this point Rubocop is complaining because of the name I used, get_*readers are not idiomatic Ruby.
I can surely override this behaviour in rubocop but I'd like to hear what are my other options and if there are other solutions, because I don't like the name of the method either.
Any idea?
You can use some other names that reveals the purpose of this this method, e.g. current_content_type, restricted_content_type, disabled_content_type - whatever suits you best.
About the naming it could be nice to have a method called invalid_content_type? which returns a Boolean.
For e.g :
def invalid_content_type?(content_type)
request.content_type == content_type
end
def restrict_content_type
return if invalid_content_type(self::CONTENT_TYPE)
...
end
How do I follow OOP standards within RoR controllers?
The setup: submitting data to a form & then manipulating it for display. This is a simplified example.
app/controllers/discounts_controller.rb
...
def show
#discount = Discount.find(params[:id])
formatted = calc_discounts(#discount)
end
...
private
calc_discounts
half_off = #discount.orig_price * .5
quarter_off = #discount.orig_price * .25
return {:half => half_off, :quarter => quarter_off}
end
...
Or is it better to place this in a library with attr_accessor and then create new instances of the library class within the controller? Or is there an even better way of accomplishing this?
The question to ask yourself is "is this logic useful for the view, model, or both?"
If the answer is that it's only useful for display purposes, I would put that logic in a view helper. If it's also beneficial to the model, put it there. Maybe something like this:
class Discount
def options
{half: (self.orig_price * .5), quarter: (self.orig_price * .25)}
end
end
Then in your controller you can just locate the record in question:
def show
#discount = Discount.find(params[:id])
end
And display it in the view:
<h1>half: <%= #discount.options[:half] %> </h1>
<h1>quarter: <%= #discount.options[:quarter] %> </h1>
Well, you can can add half_off and quarter_off as methods to your model:
class Discount < ActiveRecord::Base
def half_off
orig_price * 0.5
end
def quarter_off
orig_price * 0.25
end
end
.. and then do the following:
def show
#discount = Discount.find(params[:id])
end
Now you can call #discount.half_off and #discount.quarter_off in your view..
First off, you've got some syntax issues there. When you define methods you need to use a def keyword, and since Ruby 1.9 you can use a shortcut when defining hashes that avoids hashrockets, so it's:
def calc_discounts
half_off = #discount.orig_price * .5
quarter_off = #discount.orig_price * .25
return {half: half_off, quarter: quarter_off}
end
Also, you defined a local variable formatter inside of your controller's show method. This doesn't actually do anything but assign some values to a variable that only exists within that method. Only the controller's instance variables (variables with an #) can be passed to the view.
That being said, the best practice in RoR is to keep controllers "skinny", which means only using controllers to authenticate, authorize, load a model, assign an instance variable for you view, handle errors with any of the former, and then render the view according to the format requested.
It's another best practice not to include much logic in your views. This way, your logic can be shared with and reused by other views instead of having to be re-written for each new view you make. It also makes your views more readable, as they will read like simple lists of what is to be shown instead of making people try to decipher embedded ruby all over the place.
If the code is something that one of your other models could benefit from being able to use, put it inside your model code (or make a new plain old Ruby object if the logic is complex or not really cohesive with the existing model).
If the logic is something that is just for making a view prettier or in a better format, but won't actually be used by the models, then it should go in some type of view helper or decorator.
I have an application in Ruby/Rails where I am going to have to connect to a third-party application (Xero accounting) in order to send/pull data. This happens in several controllers thoughout the application. However, whilst the specifics are different, the actual connection is the same, and looks like this:
require 'xero_gateway'
xero_config = YAML.load_file("#{Rails.root}/config/xero.yml")["testing"]
xero_private_key = "#{ENV['HOME']}/certs/breathehr_xero.key" || ENV["xero_private_key"]
xero_gateway = XeroGateway::PrivateApp.new(xero_config['consumer_key'], xero_config['consumer_secret'], xero_private_key)
The next step of the code might be fetch something, like an invoice, as in:
xero_gateway.get_invoice('INV-001')
Or, in another controller, it might be to create a contact, such as:
xero_gateway.build_contact
Is there somewhere I can put the connection code so that I can end up just calling xero_gateway in each specific controller? It doesn't feel right to be repeating the same code again and again each time I have to authenticate and connect.
To build on #Dave Newton's answer:
You would create a "provider" class that marries an object factory to some configuration:
File: lib/xero_gateway_provider.rb
require 'xero_gateway'
class XeroGatewayProvider
cattr_accessor :default_private_key, :default_consumer_key, :default_consumer_secret
def initialize(overrides = {})
#private_key = overrides[:private_key] || self.class.default_private_key
#consumer_key = overrides[:consumer_key] || self.class.default_consumer_key
#consumer_secret = overrides[:consumer_secret] || self.class.default_consumer_secret
end
def create_private_app
XeroGateway::PrivateApp.new(#consumer_key, #consumer_secret, #private_key)
end
end
Then you could create a Rails initializer: config/initializers/xero_gateway_provider.rb
require 'xero_gateway_provider'
conf = YAML.load_file("#{Rails.root}/config/xero.yml")[Rails.env]
XeroGatewayProvider.default_private_key = "#{ENV['HOME']}/certs/breathehr_xero.key" || ENV["xero_private_key"]
XeroGatewayProvider.default_consumer_key = conf["consumer_key"]
XeroGatewayProvider.default_consumer_secret = conf["consumer_secret"]
And to use it:
# Using default configs
provider = XeroGatewayProvider.new;
private_app = provider.create_private_app
private_app.get_invoice("...")
# Using overrides
provider = XeroGatewayProvider.new :consumer_key => '...', :consumer_secret => '...';
private_app = provider.create_private_app
private_app.get_invoice("...")
Edit: Just realized there is no point to instantiating XeroGatewayProvider if it uses class level properties, so I made them defaults allowing you to configure each provider individually.
Also #Gareth Burrows comment on where to put and name the class, I think this would fit just fine in the lib/ directory. See the edits to the post for specifics.
You could put it just about anywhere, including:
A utility class used by your actions, or
A base action class (meh), or
A module included in your actions (meh).
I lean towards a utility class, because:
It's easy to instantiate or mock/stub it for testing, and
It doesn't make the surface area of your action class any bigger
You can create a regular ruby class in the models folder called Xero or something and do this code in the initializer.
require 'xero_gateway'
class Xero
def initialize
xero_config = YAML.load_file("#{Rails.root}/config/xero.yml")["testing"]
xero_private_key = "#{ENV['HOME']}/certs/breathehr_xero.key" || ENV["xero_private_key"]
xero_gateway = XeroGateway::PrivateApp.new(xero_config['consumer_key'], xero_config['consumer_secret'], xero_private_key)
end
end
And then just call:
xero_gateway = Xero.new
Another option is to create an initializer in the initializers/ folder.
xero_gateway.rb
And put the initialization code in there. This way it will be parsed only on application startup.
I need to be able to generate links to sites that all run off my app but which have different domains (I'm running a whitelabel service).
The email being sent on behalf of these domains to set a different host depending on the mailing.
Normally I'd setup the host value application.rb:
config.action_mailer.default_url_options[:host] = 'myhost.com'
However, because my host varies according to the link I'm trying to do this at runtime instead.
user_mailer.rb:
Rails.configuration.action_mailer.default_url_options[:host] = new_host
mail(...)
The problem is that every time I run this it continues to use whatever's defined in application.rb. I can't seem to get the application to respect the newly defined value of default_url_optiions[:host]. What am I doing wrong?
The default_url_options method is set on ActionMailer using class_attribute which is defined in ActiveSupport core extensions for Class. According to the documentation it also provides an instance level accessor that can be overridden on a per instance basis without affecting the class level method. So you should be able to override the host setting directly for each email
class UserMailer < ActionMailer::Base
def welcome(user)
#user = user
# don't need this if you override #mail method.
self.default_url_options = default_url_options.merge(host: #user.host)
mail(to: user.email, subject: "Welcome")
end
# might be better to override mail method if you need it for all emails in
# a particular mailer
private
def mail(headers, &block)
self.default_url_options = default_url_options.merge(host: #user.host)
super
end
end
This should allow you to modify the setting at runtime; please ignore the #user.host call and replace it with however you would determine the host.
If there aren't a whole lot of views you could simply define the host on the url_for helper, and if there are too many views I'd suggest you write your own helper that wraps the url_for helper with the :host => 'mysite.com'.
Hey,
i need to use current_user model in order to perform some calculations inside a function. Inside the function i need to do something like current_user.name = 'whatever', thus changing the current value of name.
However, i want that change to be local, only done inside that function. Since Rails uses objects though, it's a problem. So i'm thinking, what is the best thing to do ?
Maybe clone current_user to a new object and use that inside the function ? This seems expensive.
Or maybe creating a hash out of a model ? And if i do that, the actual model will not be changed ?
EDIT: It seems that hash works, but there is no type associated with it, so if i do something like :
#attacker = current_user.attributes
then, to use it, i have to specify to_s like (else i get a nil error for some reason):
#attacker[:name].to_s = 'whatever'
Parameters?
def my_f(name)
new_name = "lorem" + name
return new_name
end
Somewhere in your controller:
loremized_name = my_f(current_user.name)
If you need all of the logic in your model, the easiest way would be to simply clone it:
def local_function
user = current_user.clone
# Perform some calculations on user here
end