Rails Partials - conditionality with locals - ruby-on-rails

I have the following code in a Rails partial being used in some mailers but am not happy with my solution and have the feeling this is far from optimal.
I have an email which
From my mailer:
def the_email_i_am_sending(user, inquiry, params = {})
get_variables(inquiry) #This also provides access to my `#user` object
#contact_name = [params[:guest_last_name].to_s, " ", params[:guest_first_name].to_s].join
I always have #user but on occasion a specific partner will call our API with additional params of [:guest_last_name] and [:guest_first_name] as defined above. This allows me to define #contact_name as a separate instance variable.
When this is .present? i.e. not nil, I want to render #contact_name in a field on the email rather than the #user.login that would pull from our DB.
My mailer view then uses the following code to decide which partial it will render.
<% if #contact_name.present? %>
<%= render 'meet_your_guest_v3', tujia_guest: #contact_name %>
<% else %>
<%= render 'meet_your_guest_v3' %>
<% end %>
My solution is then to utilise this code in the partial being rendered in the mailer. It seems a little verbose but I am unsure about the correct usage of local_assigns.has_key?
<% if local_assigns.has_key?(:partner_guest) %>
<%= partner_guest %> <p>(via our partner</p>
<% else %>
<%= #user.login %>
<% end %>
Is there a better way?

You should definitely follow the advice from #Jon regarding dealing with params in your controller/mailer. Additionally you should just pass #contact_name every time to the underlying partial, regardless if it is present or not, then check only where you want to render it, if it is present. This way you would skip one conditional:
#email_view.html.erb
render 'meet_your_guest_v3', parnter_guest: #contact_name
_contact_name.html.erb
<% partner_guest.present? %>
...
A further step could be using a special decorator object, which would deal with the presentation logick. It would check wether contact_name was provided from outside or from the model and render the desired html tag for the contact_name (or it could just return it as string). See following pseudocode using the draper gem:
class MyController < ApplicationController
def send_mail
#user = User.find(...).decorate(
contact_name: [params[:guest_last_name].to_s, " ", params[:guest_first_name].to_s].join
)
MyMailer.the_email_i_am_sending(#user)
end
end
class MyMailer < ApplicationMailer
def the_email_i_am_sending(user)
#user = user
mail(to: ..., subject: ...)
end
end
class UserDecorator < Draper::Decorator
def contact_name_tag
if (contact_name.present?)
h.content_tag(:div, contact_name)
else
h.content_tag(:div, user_name)
end
end
end
#email_view.html.erb
<%= #user.contact_name_tag %>
However if the presentation logic isn't very complicated, going with a couple conditionals and perhaps extracting them into basic rails helpers is fine and using a presenter may be an overkill

Related

ActionCable + Devise + Pundit + ApplicationController.render

I'm trying to render a template in a ActionJob to be broadcast via ActionCable.
ApplicationController.render(partial: "messages/message", locals: { message: message }, assigns: { current_user: user}).squish
In most instances, this works fine, however some of my templates use Punit for authorization in the view.
<% if policy(message).show? %>
<%= message.body %>
<% end %>
This raises an error when the job is ran.
ActionView::Template::Error: Devise could not find the `Warden::Proxy` instance on your request environment.
A quick Google search reveals this issue: https://github.com/plataformatec/devise/issues/4271
The mentioned in the ticket and links, there is no env['warden'] available because no middleware has executed to add it.
How can I work around this?
As a workaround, this is what I've done:
class ActiveJobController < ActionController::Base
end
In my partial, instead of using the policy helper, I'm doing this
<% if Pundit::PolicyFinder.new(message).policy.new(current_user, message).show? %>
<%= message.body %>
<% end %>
and from my ActiveJob
ActiveJobController.render(partial: "messages/message", locals: { message: message, current_user: user }).squish
This avoids any of the stock Devise and Pundit helpers which references env["warden"]. It isn't ideal but works for now both when rendered in a request and in a job.
Another scalable/maintainable approach is to use a helper or a view library like ViewComponents/cells. This way, you can extract your existing view into a component and parameterize any devise/warden methods called in the view. The advantage of this approach is
The view is easier to test
You can call it from anywhere in your code
There are no coupled dependencies
Example using view helpers
Say you have in app/views/messages/show.html.erb the following
<%= current_user.first_name %>
<%= #message %>
Calling MessagesController.render :show outside the controller will cause an error since access to the request object is not available. Using ViewComponents, we extract the view into its own component
in app/components/message_component.rb
class MessageComponent < ViewComponent::Base
def initialize(user:, message:)
#user = user
#message = message
end
end
in app/components/message_component.html.erb
<%= #user.first_name %>
<%= #message %>
Usage
In app/views/messages/show.html.erb just call
<%= render(MessageComponent.new(message: #message, user: current_user) %>
In ActionCable, call it like so
ApplicationController.render(MessageComponent.new(message: #message, user: current_user)
Since you have access to ActiveRecord models from anywhere in your code, you should be able to fetch #message

Remove rails presenter object assignment from view

I am using a namespaced Presenter object to help refactor some view presentation logic for my model attributes.
For one object being sent from the controller I would do
#user = Users::UserPresenter.new(#user)
and that works fine. For a query of users, I created a .present() method that maps and applies the UserPresenter.new to each user, so I do
#users = Users::UserPresenter.present(users)
and that works fine. But what about when I am passing an object that then iterates through a relationship in the view. A simple example would be
<% appointment.users.each do |user| %>
<% user = Users::UserPresenter.new(user) %>
<li> <%= user.age%></li>
<% end %>
A more complex example would be
<% appointment.appointment_host.family.users.each do |user| %>
<% user = Users::UserPresenter.new(user) %>
<li> <%= user.age%></li>
<% end %>
user_presenter.rb
module Users
class UserPresenter < SimpleDelegator
# methods
end
end
I don't like having to set the Presenter object in the view. What is a better way to handle this? Ideally using similar patterns as I have so far.
Perhaps you could create a hierarchy of presenters similar to model associations, and then pass only the root presenters to the view. Something like this:
class AppointmentPresenter
def initialize(appointment)
#appointment = appointment
end
def users
Users::UserPresenter.present(#appointment.users)
end
def host_family_users
Users::UserPresenter.present(#appointment.appointment_host.family.users)
end
# or perhaps even indeed create a presenter for each collection:
def appointment_host
AppointmentHostPresenter.new(#appointment.appointment_host)
# this presenter would have the `family` method returning a FamilyPresenter, etc.
end
end
I.e. some kind of "decorators" for the model associations, returning presenters instead of model objects.

Why my helpers does not outputting nothing in my view?

I'm new on rails and I have a book to study them. In one practice, I created a helper in my Application Helper, the test from RSpec work fine, until I have to print the result of my helper. No show any result and no error happens.
application_helper.rb
module ApplicationHelper
def title(*parts)
unless parts.empty?
content_for :title do
(parts << "Ticketee").join(" - ")
end
end
end
end
show.html.erb
<% title(#project.name) %>
projects_controller.rb
class ProjectsController < ApplicationController
def show
#project = Project.find(params[:id])
end
end
and when I go to the show link I supposed to see "Random Project name - Ticketee", however only they show me "Ticketee".
Any help...
<% title(#project.name) %>
Means don't show to the user
<%= title(#project.name) %>
Means show to the user - notice the equals.

Getting 'form_for(#something)' to work outside of new.html.erb

I want to place my <%= form_for(#something) do |f| %> which is currently located in app/views/something/new.html -- inside multiple pages, so maybe in app/views/layouts/application.html.erb
How do I get the #something variable and the form to work properly there, or somewhere else -- since it's defined in the controller #new action of SomethingController, it only seems to be available in the appropriate new.html.erb view..
You can put the form anywhere, just provide an instance variable of #something in controller
The basic usage is here.
ThisThingsController
def show
#this_thing = foo
#that_thing = bar
end
end
# View
<%= #this_thing %>
<%= form_for #that_thing %>
Of course you can use partial to render the form, as long as you feed it with variable it needs.
Try
<%= form_for SomeThing.new do |f| %>
Without fully understanding what you are trying to accomplish, I'll make this suggestion.
Add a before filter to your ApplicationController (alternatively you could create a module and mix it in where needed). Then call the before_filter when needed. This example will always run the before filter:
class ApplicationController
before_filter :set_something
private
def set_something
#something = ... # Fill in the logic here
end
end
Then add your form where needed. You can even make it appear conditionally depending on whether #something is set.
<% if #something %>
# Form goes here
<% end %>

Rails: Loading in array of partials

In my Rails app I already have the following code:
<% %w(number_of_students edit_class_name tech_help).each do |modal| %>
<%= render "common/modals/#{modal}" %>
<% end %>
There will be a few more modals added into app/views/common/modals and instead of explicitly listing them out in the %w() I was wanting to loop through the common/modals directory and just render each file.
Here is what I came up with:
def render_modals
files = Dir.glob("#{Rails.root}/app/views/common/modals/*").collect { |file| File.basename(file, ".html.erb").sub("_", "") }.flatten
files.collect do |modal|
render partial: "common/modals/#{modal}"
end.join.html_safe
end
define a simple method in where is more appropriate (maybe app helper?) like this:
def modals
%w(number_of_students edit_class_name tech_help)
end
if you need these modals in a controller/model too, maybe you should define this method in an appropriate class? For example
class Modal
def self.types
%w(number_of_students edit_class_name tech_help)
end
end
Also, if you are rendering the templates often, then also define
def render_modals
modals.map do |modal| # Modals here should be the method that you just defined, example, Modal.types
render partial: "common/modals/#{modal}"
end.join
end

Resources