Passing a variable between actions within same controller Rails - ruby-on-rails

My show action:
def show
# Multiple keywords
if current_user.admin?
#integration = Integration.find(params[:id])
else
#integration = current_user.integrations.find(params[:id])
end
#q = #integration.profiles.search(search_params)
#profiles = #q.result.where(found: true).select("profiles.*").group("profiles.id, profiles.email").includes(:integration_profiles).order("CAST( translate(meta_data -> '#{params[:sort_by]}', ',', '') AS INT) DESC NULLS LAST").page(params[:page]).per_page(20)
#profiles = #profiles.limit(params[:limit]) if params[:limit]
end
There can be many different filters taking place in here whether with Ransacker, with the params[:limit] or others. At the end I have a subset of profiles.
Now I want to tag all these profiles that are a result of the search query.
Profiles model:
def self.tagging_profiles
#Some code
end
I'd like to create an action within the same controller as the show that will execute the self.tagging_profiles function on the #profiles from the show action given those profiles have been filtered down.
def tagging
#profiles.tagging_profiles
end
I want the user to be able to make a search query, have profiles in the view then if satisfied tag all of them, so there would be a need of a form
UPDATE:
This is how I got around it, don't know how clean it is but here:
def show
# Multiple keywords
if current_user.admin?
#integration = Integration.find(params[:id])
else
#integration = current_user.integrations.find(params[:id])
end
#q = #integration.profiles.search(search_params)
#profiles = #q.result.where(found: true).select("profiles.*").group("profiles.id, profiles.email").includes(:integration_profiles).order("CAST( translate(meta_data -> '#{params[:sort_by]}', ',', '') AS INT) DESC NULLS LAST").page(params[:page]).per_page(20)
#profiles = #profiles.limit(params[:limit]) if params[:limit]
tag_profiles(params[:tag_names]) if params[:tag_names]
end
private
def tag_profiles(names)
#profiles.tagging_profiles
end
In my view, I created a form calling to self:
<%= form_tag(params.merge( :controller => "integrations", :action => "show" ), method: :get) do %>
<%= text_field_tag :tag_names %>
<%= submit_tag "Search", class: "btn btn-default"%>
<% end %>
Is this the best way to do it?

Rails public controller actions correspond always to a http request. But here there is just no need for 2 http requests. A simple solution would be just creating to private controllers methods filter_profiles(params) and tag_profiles(profiles) and just call them sequentially.
You can also extract this problem entirely to a ServiceObject, like this:
class ProfileTagger
attr_reader :search_params
def initialize(search_params)
#search_params = search_params
end
def perform
search
tag
end
def tag
#tag found profiles
end
def search
#profiles = #do the search
end
end
As processing 30,000 records is a time consuming operation, it would make sence to perform it outside of the rails request in background. This structure will allow you to delegate this operation to a sidekiq or delayed_job worker with ease

Instance Variables
If you want to "share" variable data between controller actions, you'll want to look at the role #instance variables play.
An instance of a class means that when you send a request, you'll have access to the #instance variable as long as you're within that instance of the class, I.E:
#app/controllers/your_controller.rb
Class YourController < ApplicationController
before_action :create_your_var
def your_controller
puts #var
end
private
def create_your_var
#var = "Hello World"
end
end
This means if you wish to use the data within your controller, I would just set #instance variables, which you will then be able to access with as many different actions as you wish
--
Instance Methods
The difference will be through how you call those actions -
#app/controllers/your_controller.rb
Class YourController < ApplicationController
def action
#-> your request resolves here
method #-> calls the relevant instance method
end
private
def method
#-> this can be called within the instance of the class
end
end

Related

Rails use the same endpoint instead of two new

In my app I need to display purchased Book in one page and planned_purchase Book in other page. The view will be the same so my question is - do I need to create new controller method and routes to display both or can I use e.g. Index and somehow display two different values depending on request?
current code below:
class BooksController < ApplicationController
before_action :fetch_conversation
def index
#planned = Book.planned_purchase
#purchased = Book.purchased
end
end
class Book < ApplicationRecord
scope :purchased, -> { where(purchased: true) }
scope :planned_purchase, -> { where(purchased: false) }
end
As I can understand: you can do this thing using a single controller GET action.
So, you've this BooksController and index action, which I assume can be accessible via books_path.
You can modify the index method, as follows to accept a new parameter by which you can filter the books:
def index
case params[:filter]
when 'purchased'
#records = Book.purchased
when 'planned_purchase'
#records = Book.planned_purchase
else
#records = Book.all
end
end
Now, you have a view page books/index.html.erb for this index action. Let's break this into 2 separate partials.
In books/index.html.erb:
<% if params[:filter] == 'purchased' %>
<%= render "partial_for_parchased" %>
<% elsif params[:filter] == 'planned_purchase' %>
<%= render "partial_for_planned_parchased" %>
<% end %>
Inside those partials you can modify the view based on the category.
Now, to get those two different page, you need to define 2 separate urls:
<%= link_to 'Purchased', books_path(filter: 'purchased') %>
<%= link_to 'Planned Purchased', books_path(filter: 'planned_purchase') %>
As your, def index, is a GET method and not depending on the strong parameters, so you don't need to add filter in your params.required(:book).permit(...)
Hope I covered all the areas!
I think the answer should be pretty simple and straight.
You can just pass a parameter to the index method and filter records inside it and return them.
def index
case params[:filter]
when 'purchased'
#records = Book.purchased
when 'planned_purchase'
#records = Book.planned_purchase
else
# get all records or throw an error
end

Passing params in Rails helper class

I am trying to refactor my Rails helpers and move breadcrumbs and navigation menu logic into separate classes. But in these classes I don't have access to params, cookies hashes etc. I think that passing params on and on between different classes is a bad idea. How can I avoid that?
For example I have:
module NavigationHelper
def nav_item(name, path, inactive = false)
NavItem.new(params, name, path, inactive ).render
end
class NavItem
include ActionView::Helpers
include Haml::Helpers
def initialize(params, name, path, inactive )
init_haml_helpers
#params = params
#name = name
#path = path
#inactive = inactive
end
def render
capture_haml do
haml_tag :li, item_class do
haml_concat link_to #name, #path
end
end
end
def item_class
klass = {class: 'active'} if active?
klass = {class: 'inactive'} if #inactive
klass
end
# Class of the current page
def active?
slug = #path.gsub /\//, ''
#params[:page] == slug || #params[:category] == slug
end
end
end
I don't think Rails provide any mechanism to access Params out of ActionPack. The way you have done is seems correct to me. You have to pass on params, cookies atleast once to initialize your classes.

Does accessing the database in views via view-models or view-helpers break the MVC paradigm?

I believe all of the following break the MVC paradigm but wanted to double check if this was the case. In all cases the view is directly accessing data rather than having the data being passed in. From my understanding of MVC, it should never do that. The controller should get all the data that is necessary to render the view as to not couple the view and model directly. Is my understanding correct?
Accessing the database through a view helper
# in app/helpers/view_helper.hrb
def some_view_helper(person_id)
#person = Person.find(person_id)
end
Accessing another web server through a view helper
# in app/helpers/view_helper.hrb
def another_view_helper(person_id)
# makes http request over the wire to get json back
#json = WebService.get_person(person_id)
end
Accessing the database through a view model
# in apps/controller/person_controller.rb
def show
#person = Person.find(params[:id])
#page_model = PageModel.new(#person)
end
#in app/views/persons/show.html.erb
<% #page_model.friends.each do |friend| %>
...
<% end %>
#in app/models/person.rb
class Person < ActiveRecord::Base
has_many :friends
end
#in app/models/page_models/page_model.rb
def initialize(person)
#person = person
end
def friends
#person.friends
end
Accessing web server to get data through a view model
# in apps/controller/person_controller.rb
def show
#person = Person.find(params[:id])
#page_model = PageModel.new(#person)
end
#in app/views/persons/show.html.erb
<% #page_model.friends.each do |friend| %>
...
<% end %>
#in app/models/page_models/page_model.rb
def initialize(person)
#person = person
end
def friends
WebService.get_friends_for_person(person_id)
end
For 1 and 2, you could just set an instance variable (#person) in the controller.
For 3, your view code isn't so bad, but why have a separate page model? You can also load the friends up front in the controller:
# in apps/controller/person_controller.rb
def show
#person = Person.find(params[:id], :include => :friends)
#friends = #person.friends
end
Example 4 is a bit worse, since you're doing external web service calls in a view. Don't do that.
This article has a good example of what an ideal clean view would look like: http://warpspire.com/posts/mustache-style-erb/

Controller best practices: Multiple Methods or Multiple cases in Show

I'm frequently building controllers where i would like multiple methods
(in addition to index, edit, show, etc.). Most of the time the actions i
desire could be lumped into show as they are simple GET operations,
however I don't want to put too much logic in any one controller action.
Here is a quick example of two different ways to achieve the same
thing...
class TwitterFriendController < ApplicationController
## lump everything into show?
def show
if params[:id] == "follow"
users = current_user.following
elsif params[:id] == "follow_me"
users = current_user.users_who_follow_me
elsif params[:id] == "following_follow_me"
users = current_user.following_who_follow_me
elsif params[:id] == "following_who_do_not_follow_me"
users = current_user.following_who_do_not_follow_me
...
end
respond_with do |format|
format.json do {...}
end
end
## or split everything out into separate methods, this requires
additional routing
def following
...
end
def users_who_follow_me
...
end
def following_who_follow_me
...
end
def following_who_do_not_follow_me
...
end
end
Everything in show
a ton of logic in one method
DRY ? # lots of extra code needed for logic
Less routing
Seperate Methods
More routing
not DRY
Easy method lookup
Easier to read individual methods
So again the real question is, which one of those techniques are less
bad.
I would do something like:
FOLLOW_WHITELIST = %w[ follow follow_me following_follow_me following_who_follow_me following_who_do_not_follow_me ]
def show
if FOLLOW_WHITELIST.include? params[:id]
users = current_user.send params[:id].to_sym
end
respond_with do |format|
format.json do {...}
end
end
This will call whatever method is passed in params[:id], as long as it's in the whitelist (to prevent arbitrary code injection).
If having separate routes was a plus to you (nicer urls?), you could also dynamically generate the methods and routes with something like this:
class TwitterFriendController < ApplicationController
FOLLOW_ACTIONS = %w[ follow follow_me following_follow_me following_who_follow_me following_who_do_not_follow_me ]
FOLLOW_ACTIONS.each do |action|
define_method action do
users = current_user.send action.to_sym
respond_with do |format|
format.json do {...}
end
end
end
end
And then in routes.rb:
FOLLOW_ACTIONS.each do |action|
match action.to_sym => "controller##{action}"
end

Rails way to render different actions & views based on user type?

I have a couple different user types (buyers, sellers, admins).
I'd like them all to have the same account_path URL, but to use a different action and view.
I'm trying something like this...
class AccountsController < ApplicationController
before_filter :render_by_user, :only => [:show]
def show
# see *_show below
end
def admin_show
...
end
def buyer_show
...
end
def client_show
...
end
end
This is how I defined render_by_user in ApplicationController...
def render_by_user
action = "#{current_user.class.to_s.downcase}_#{action_name}"
if self.respond_to?(action)
instance_variable_set("##{current_user.class.to_s.downcase}", current_user) # e.g. set #model to current_user
self.send(action)
else
flash[:error] ||= "You're not authorized to do that."
redirect_to root_path
end
end
It calls the correct *_show method in the controller. But still tries to render "show.html.erb" and doesn't look for the correct template I have in there named "admin_show.html.erb" "buyer_show.html.erb" etc.
I know I can just manually call render "admin_show" in each action but I thought there might be a cleaner way to do this all in the before filter.
Or has anyone else seen a plugin or more elegant way to break up actions & views by user type? Thanks!
Btw, I'm using Rails 3 (in case it makes a difference).
Depending on how different the view templates are, it might be beneficial to move some of this logic into the show template instead and do the switching there:
<% if current_user.is_a? Admin %>
<h1> Show Admin Stuff! </h1>
<% end %>
But to answer your question, you need to specify which template to render. This should work if you set up your controller's #action_name. You could do this in your render_by_user method instead of using a local action variable:
def render_by_user
self.action_name = "#{current_user.class.to_s.downcase}_#{self.action_name}"
if self.respond_to?(self.action_name)
instance_variable_set("##{current_user.class.to_s.downcase}", current_user) # e.g. set #model to current_user
self.send(self.action_name)
else
flash[:error] ||= "You're not authorized to do that."
redirect_to root_path
end
end

Resources