ActionCable + Devise + Pundit + ApplicationController.render - ruby-on-rails

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

Related

Rails: External API Integration using RestClient (undefined local variable or method `username')

I am building a digital library, and I have completed a lot of the functionalities needed. I am currently having an issue with integrating the digital library with a Learning Management System (LMS).
I already have an admin authentication system for the digital library using the Devise gem. My goal is to allow users who want to access the digital library to login to the digital library using their Learning Management System (LMS) credentials (username and password).
I have been provided with the Login API endpoint and other needed parameters of the Learning Management System (LMS), and I have created the User Model, the Sessions Controller and the Sessions View Templates.
I am currently using the RestClient Gem for the API call, but I having an error undefined local variable or method `username' for # Did you mean? user_path. I can't figure out where things went wrong.
Sessions Controller
require 'rest-client'
class SessionsController < ApplicationController
def new
end
def create
response = RestClient::Request.execute(
method: :post,
url: 'https://newapi.example.com/token',
payload: { 'username': "#{username}",
'password': "#{password}",
'grant_type':'password' },
headers: { apiCode: '93de0db8-333b-4f478-aa92-2b43cdb7aa9f' }
)
case response.code
when 400
flash.now[:alert] = 'Email or password is invalid'
render 'new'
when 200
session[:user_id] = user.id
redirect_to root_url, notice: 'Logged in!'
else
raise "Invalid response #{response.to_str} received."
end
end
def destroy
session[:user_id] = nil
redirect_to root_url, notice: 'Logged out!'
end
end
Sessions New View
<p id=”alert”><%= alert %></p>
<h1>Login</h1>
<%= form_tag sessions_path do %>
<div class="field">
<%= label_tag :username %>
<%= text_field_tag :username %>
</div>
<div class="field">
<%= label_tag :password %>
<%= password_field_tag :password %>
</div>
<div class="actions">
<%= submit_tag 'Login' %>
</div>
<% end %>
User Model
class User < ApplicationRecord
has_secure_password
validates :username, presence: true, uniqueness: true
end
Any form of help with code samples will be greatly appreciated. I am also open to providing more information about this integration if required. Thank you in advance.
I think that the problem is in fact that inside your SessionsController in create action, you are interpolating username and password. There's no definition for these methods in your code so you get undefined local variable or method.
You could probably pick those from params like this:
def username
params[:username]
end
def password
params[:password]
end
Or interpolate them directly in payload replacing current method calls with params[:username] and params[:password].
In such situations, it is good to use byebug or pry to debug your code and see what's happening inside your controller.
You could also think of closing some parts of your logic in Service objects - you shouldn't have more 10-15 lines in your controller action (unless the situation requires it)
Maybe you should use params[:username] rather than only username ?
username and password in payload are undefined variables. Please set their values. Possible values could be params[:username] and params[:password]

undefined method - issues calling a helper method from a partial in a mail controller

I've set up an ActionMailer to email and pull a partial to post the data for the user, however the helper methods are comming back as undefined - i moved them to the application helper but still the same error, I think its in the way im passing the variable to the mailer ?
I've searched for same issue online but find that theres no concise response - I fear I'm doing something basic somwewhere wrong
Error:
undefined method `tidy_address' for #<#<Class:0x007f5c90681b10>:0x007f5c90ad8ba0>
My partial in order views : _enquiry_details.html.erb
<div class="row">
<div class="col-xs-2">
<h3><%= #customer.name %></h3>
<hr>
<h5><%= tidy_address(#customer.locations.first) %></h5>
<% #phone_number.each do |pn| %>
<h5><%= pn.name %> : <%=pn.phone_number.phone%></h5>
<% end %>
in my user mailer.rb
def lead_received(enquiry)
#order=enquiry
if #order.user
#customer=#order.user
else
#customer=#order.company
end
#locations=#customer.locations
#phone_number=#customer.phone_maps
mail to: "myemailaddress#domain.com", subject: "New Lead Received"
end
which I call with this passing the order , think this is where im going wrong
in order controller..
if #order.save
UserMailer.lead_received(#order).deliver_now
For clarity in my mailer view lead_received.html.erb
<%= render "orders/enquiry_details" %>
And finally in my locations helper
module LocationsHelper
def google_string(lat,long,size)
case size
when "s"
mysize="150x150&zoom=12"
when "m"
mysize="350x300&zoom=14"
when "l"
mysize="570x300&zoom=13&scale=2"
end
"https://maps.googleapis.com/maps/api/staticmap?"+URI.encode("markers=#{lat},#{long}&size=#{mysize}&key=AIzaSyAxRuThoVl-xziFElt3GPCESLsaye4_aGA")
end
# Return a sorted neat adress block
def tidy_address(location)
unless location.blank?
t_address=""
t_address="#{location.address1}<br>" if location.address1.present?
t_address=t_address+location.address2+"<br>" if location.address2.present?
t_address=t_address+location.address3+"<br>" if location.address3.present?
t_address=t_address+location.city+"<br>" if location.city.present?
t_address=t_address+location.postcode if location.postcode.present?
# t_address=t_address+"("+location.id.to_s+")"
#t_address=t_address+"<br><a href=''>Directions to here</a>"
t_address.html_safe
else
t_address="<link_to 'Add an address' '#'>".html_safe
end
end
end
Add the helper in the mailer code to use inside mailer.
class UserMailer < ActionMailer::Base
default from: "" # default from email
helper LocationsHelper
helper UserHelper
def lead_received(enquiry)
#order=enquiry
if #order.user
#customer=#order.user
else
#customer=#order.company
end
#locations=#customer.locations
#phone_number=#customer.phone_maps
mail to: "myemailaddress#domain.com", subject: "New Lead Received"
end
end

Rails Partials - conditionality with locals

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

How do you create a rails app without using all of resource in create,update,delete?

In the rails guides tutorial creating a blog app after we create the rails app and create a resources in the routes then we start working on a form_for for creating a posts title and text in the guide it tells me that we need to add this line <%= form_for :post, url: posts_path do |f| %>
the posts_path helper is passed to the :url option. What Rails will do with this is that it will point the form to the create action of the current controller, the PostsController, and will send a POST request to that route.
so what am trying to understand is the passing to 'create action' you see I have a simple app where i want is when a text is entered in the title field and the submit button is entered I want it to pass to the create action where I just out put the text in the create action view or another view, the rails guide goes through teaching the 'CRUD' but I just want to understand How to build an app that doesn't use 'CRUD' for instance an app that takes an input and outputs it in another view?
my form:
<h1>Here Lets create a simple post</h1>
<%= form_for :post, url: posts_path do|f| %>
<p>
<%= f.label :title %>
<%= f.text_field :title %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
posts controller:
class PostsController < ApplicationController
def new
#post = Post.new
end
def create
#post = Post.new(post_params)
end
def post_params
params_require(:post).permit(:title)
end
end
create view:
<h1>THis is the post create action</h1>
<%= #post.title %>
routes:
Learnnobase::Application.routes.draw do
resources :posts
root "welcome#home"
end
Right now am getting an error stating uninitialized constant PostsController::Post highlighting my create method? I've done so many rails app tutorials using 'CRUD' I really wanna learn building a simple app without using 'CRUD', I was trying to experiment with this app even though I do use the create action of "CRUD".
We generally use Rails to build database-backed applications, but for learning purposes, you can do it this way.
The problem you are facing here is: You are tyring to create an object of the Post class, that will be the model in the example you are referring to. The error comes up since you have not created the Post model.
To meet your requirement you can make your create action be:
def create
#post = post_params #this will be a hash
end
Then change your view to:
<h1>THis is the post create action</h1>
<%= #post[:title] %>
Since you have started with rails, I would ask you how did your posts/new page load with #post = Post.new in posts/new action when you do not have post model file and class?
It is not possible. Second, with what Manoj Monga has suggested you to use params by assigning it to an instance variable(wrong way to do so with params), if you try to use create_path for posts resources which literally is '/posts' you would end up hitting posts/index action.
Rails has standard reserve action names like :index(GET, /posts), :show(GET, /posts/:id), :new(GET, /posts/new), :create(POST, /posts), :edit(GET, /posts/:id/edit), :update(PATCH, /posts/:id) You should not attempt at overriding their purpose.
What I understand that you did use post model class and loaded posts/new page, then you deleted post model class and tried with what you have asked about in your question. You should respect Rails' standards.

Rails single table inheritance. Shared controller, howto Update (CRUD)

I've got three classes Admin, Client, Agent which all inherit from User < ActiveRecord::Base. The system is designed using STI, hence all classes share the same users table.
I am interested in keeping the CRUD functionality pretty much the same, hence for now I am using a single UsersController.
When implementing the Update functionality I'm faced with a doubt.
Here's my edit form:
#Edit Form
<%= form_for(#user,{url:user_path(#user),method: :put}) do |f| %>
<%= render 'edit_fields',f:f %>
<%= f.submit "Save", class: "btn btn-large btn-primary"%>
<% end %>
#UsersController
def edit
#user=User.find(params[:id])
end
def update
binding.pry
#if #user.update_attributes(params[:user]) #<---BEFORE
#WORKAROUND BELOW
if #user.update_attributes(params[#user.type.downcase.to_sym])
flash[:success]="User was updated successfully."
redirect_to user_path(#user)
else
flash[:danger]="User could not be updated."
render 'new'
end
end
My "problem" is that params is dependent on the #user.type of the #user instance. Therefore sometimes there's a params[:client], other times a params[:admin] or params[:agent].
Hence the line
if #user.update_attributes(params[:user]) does not always work.
The workaround I implemented works fine, but I was wondering whether there's a more DRY or elegant way to approach these issues, i.e. sharing CRUD between different STI-ed classes.
There is indeed a much more elegant solution. Just change your form_for declaration and add the as option, like this:
<%= form_for(#user, as: :user, url: user_path(#user), method: :put) do |f| %>
That way in your controller your parameters will be scoped under the user key instead of the model's class.
In your controller, check for the User type in a before_filter as below. I've used this for similar STI Controllers and works great for me.
before_filter :get_user_type
private
def get_user_type
#klass = params[:type].blank? ? User : params[:type].constantize
end
And then for example you could call a show method as :
def show
#user = #klass.find params[:id]
#render
end
Using #klass across your CRUD actions should simplify your Controller.

Resources