Organizing site navigation actions in Rails - ruby-on-rails

I'm new to Rails (I've worked in MVC but not that much) and I'm trying to do things the "right" way but I'm a little confused here.
I have a site navigation with filters Items by different criteria, meaning:
Items.popular
Items.recommended
User.items
Brand.items # by the parent brand
Category.items # by a category
The problem is that I don't know how to deal with this in the controller, where each action does a similar logic for each collection of items (for example, store in session and respond to js)
Either I have an action in ItemsController for every filter (big controller) or I put it in ItemsController BrandsController, CategoriesController (repeated logic), but neither provides a "clean" controller.
But I don't know witch one is better or if I should do something else.
Thanks in advance!

You're asking two separate questions. Items.popular and Items.recommended are best achieved in your Item model as a named scope This abstracts what Xavier recommended into the model. Then in your ItemsController, you'd have something like
def popular
#items = Item.popular
end
def recommended
#items = Item.recommended
end
This isn't functionally different than what Xavier recommended, but to me, it is more understandable. (I always try to write my code for the version of me that will come to it in six months to not wonder what the guy clacking on the keyboard was thinking.)
The second thing you're asking is about nested resources. Assuming your code reads something like:
class User
has_many :items
end
then you can route through a user to that user's items by including
resources :users do
resources :items
end
in your routes.rb file. Repeat for the other nested resources.
The last thing you said is
The problem is that I don't know how to deal with this in the controller, where each action does a similar logic for each collection of items (for example, store in session and respond to js)
If what I've said above doesn't solve this for you (I think it would unless there's a piece you've left out.) this sounds like a case for subclassing. Put the common code in the superclass, do the specific stuff in the subclass and call super.

There's a pretty convenient way to handle this, actually - you just have to be careful and sanitize things, as it involves getting input from the browser pretty close to your database. Basically, in ItemsController, you have a function that looks a lot like this:
def search
#items = Item.where(params[:item_criteria])
end
Scary, no? But effective! For security, I recommend something like:
def search
searchable_attrs = [...] #Possibly load this straight from the model
conditions = params[:item_criteria].keep_if do |k, v|
searchable_attrs.contains? k
end
conditions[:must_be_false] = false
#items = Item.where(conditions)
end
Those first four lines used to be doable with ActiveSupport's Hash#slice method, but that's been deprecated. I assume there's a new version somewhere, since it's so useful, but I'm not sure what it is.
Hope that helps!

I think both answers(#Xaviers and #jxpx777's) is good but should be used in different situations. If your view is exactly the same for popular and recommended items then i think you should use the same action for them both. Especially if this is only a way to filter your index page, and you want a way to filter for both recommended and popular items at the same time. Or maybe popular items belonging to a specific users? However if the views are different then you should use different actions too.
The same applies to the nested resource (user's, brand's and category's items). So a complete index action could look something like this:
# Items controller
before_filter :parent_resource
def index
if #parent
#items = #parent.items
else
#items = Item.scoped
end
if params[:item_criteria]
#items = #items.where(params[:item_criteria])
end
end
private
def parent_resource
#parent = if params[:user_id]
User.find(params[:user_id])
elsif params[:brand_id]
Brand.find(params[:brand_id])
elsif params[:category_id]
Category.find(params[:category_id])
end
end

Related

Ruby on Rails 7 Multistep form with multiple models logic

I am currently struggling with building up a multi step form where every step creates a model instance.
In this case I have 3 models:
UserPlan
Connection
GameDashboard
Since the association is like that:
An user has an user_plan
A connection belongs to an user_plan
A game_dashboard belongs to a connection
I would like to create a wizard to allow the current_user to create a game_dashboard going through a multi-step form where he is also creating connection and user_plan instance.
For this purpose I looked at Wicked gem and I started creating the logic from game_dashboard (which is the last). As soon as I had to face with form generating I felt like maybe starting from the bottom was not the better solution.
That’s why I am here to ask for help:
What would be the better way to implement this wizard? Starting from the bottom (game_dashboard) or starting
from the top (use_plan)?
Since I’m not asking help for code at the moment I didn’t write any controller’s or model’s logic, in case it would be helpful to someone I will put it!
Thanks a lot
EDIT
Since i need to allow only one process at a time but allowing multiple processes, to avoid the params values i decided to create a new model called like "onboarding" where i handle steps states there, checking each time the step
The simplest way would be to rely on the standard MVC pattern of Rails.
Just use the create and update controller methods to link to the next model's form (instead of to a show or index view)
E.g.
class UserPlansController < ApplicationController
...
def create
if #user_plan = UserPlan.create(user_plan_params)
# the next step in the form wizard process:
redirect_to new_connection_path(user_id: current_user, user_plan_id: #user_plan.reload.id)
else
#user_plan = UserPlan.new(user: current_user)
render :new
end
end
...
# something similar for #update action
end
For routes, you have two options:
You could nest everything:
# routes.rb
resources :user do
resources :user_plan do
resources :connection do
resources : game_dashboard
end
end
end
Pro:
This would make setting your associations in your controllers easier because all your routes would have what you need. E.g.:
/users/:user_id/user_plans/:user_plan_id/connections/:connection_id/game_dashboards/:game_dashboard_id
Con:
Your routes and link helpers would be very long and intense towards the "bottom". E.g.
game_dashboard_connection_user_plan_user_path(:user_id, :user_plan_id, :connection_id, :game_dashboard)
You could just manually link your wizard "steps" together
Pro:
The URLs and helpers aren't so crazy. E.g.
new_connection_path(user_plan_id: #user_plan.id)
With one meaningful URL variable: user_plan_id=1, you can look up everything upstream. e.g.:
#user_plan = UserPlan.find(params['user_plan_id'])
#user = #user_plan.user
Con:
(not much of a "con" because you probably wind up doing this anyway)
If you need to display information about "parent" records, you have to perform model lookups in your controllers first:
class GameDashboardController < ApplicationController
# e.g. URL: /game_dashboards/new?connection_id=1
def new
#connection = Connection.find(params['connection_id'])
#user_plan = #connection.user_plan
#user = #user_plan.user
#game_dashboard = GameDashboard.new(connection: #connection)
end
end

Rails - add scope for certain controllers in a module based up a database setting

I have an api. In that api is a basecontroller that all other controllers inherit from. The basecontroller handles authentication and whether or not the API is on at all, etc.
There are users, and users can belong to a group. Users table has a group_id column.
I'm trying to introduce a new feature, whereby a select on the settings page for admin controls which users are shown from what groups. If an option is selected, only users from that group should be shown by the api.
I could go into each controller (there is one controller for each of a few different tasks - getting all users info, just active_users ids, a single users information, etc) and add the extra statement to each
if !settings.api_group.nil?
#add the additional where("group_id = ?, settings.group_id)
but that seems like a lot of repeating myself (doing it in 8 different places)
Is there some way to add something to the basecontroller that says:
if this setting option is not nil, only return user information if they are in this group
?
Thanks
You can add a method to the BaseController, and call it in each action that should have this restriction. Something like this:
in base_controller.rb:
protected
def filtered_users
if settings.api_group
User.where(:group_id => settings.group_id)
else
User.scoped
end
end
and in the controllers that inherit from it:
def index
#users = filtered_users
end
This way, you only define the filtering in one place. If it needs to change later, you only have to change it in one place. Because filtered_users actually returns a Relation, you can continue to alter the query by tacking additional .where clauses, etc, like this:
#users = filtered_users.joins(:posts).where('posts.created_at > ?', 1.week.ago)
FYI my answer was exactly what I thought it might have to be in the initial post. I'd love for there to be a more DRY solution, but I ended up doing something like this:
IN USER MODEL
def find_in_api_group
# NOTE settings.api_group is a string => "1,2,4"
if settings.api_group.nil? || settings.api_group.blank?
where("") # THERE HAS TO BE BETTER WAY OF SAYING THIS WITHOUT INTERRUPTING THE CHAIN
else
where("group_id IN (?)", settings.api_group)
end
end
IN VARIOUS CONTROLLERS
user = User.find_in_api_group
#then chain various error tests and additional activeRecord statement

Constructing a Rails ActiveRecord where clause

What's the best way to construct a where clause using Rails ActiveRecord? For instance, let's say I have a controller action that returns a list of blog posts:
def index
#posts = Post.all
end
Now, let's say I want to be able to pass in a url parameter so that this controller action only returns posts by a specific author:
def index
author_id = params[:author_id]
if author_id.nil?
#posts = Post.all
else
#posts = Post.where("author = ?", author_id)
end
end
This doesn't feel very DRY to me. If I were to add ordering or pagination or worse yet, more optional URL query string params to filter by, this controller action would get very complicated.
How about:
def index
author_id = params[:author_id]
#posts = Post.scoped
#post = #post.where(:author_id => author_id) if author_id.present?
#post = #post.where(:some_other_condition => some_other_value) if some_other_value.present?
end
Post.scoped is essentially a lazy loaded equivalent to Post.all (since Post.all returns an array
immediately, while Post.scoped just returns a relation object). This query won't be executed until
you actually try to iterate over it in the view (by calling .each).
Mmmh, the best approach you want to use can be to spread this in 2 actions
def index
#post = Post.all
end
def get
#post = Post.where("author=?", params[:author_id])
end
IMHO it has more sense if you think about a RESTful API, index means to list all and get (or show) to fetch the requested one and show it!
This question is pretty old but it still comes up high in google in 2019, and also some earlier answers have been deprecated, so I thought I would share a possible solution.
In the model introduce some scopes with a test for the existence of the parameter passed:
class Post
scope :where_author_ids, ->(ids){ where(author_id: ids.split(‘,’)) if ids }
scope :where_topic_ids, ->(ids){ where(topic_id: ids.split(‘,’)) if ids }
Then in the controller you can just put as many filters in as you wish e.g:
def list
#posts = Post.where_author_ids(params[:author_ids])
.where_topic_ids(params[:topic_ids])
.where_other_condition_ids(params[:other_condition_ids])
.order(:created_at)
The parameter can then be a single value or a comma separated list of values, both work fine.
If a param doesn’t exist it simply skips that where clause and doesn’t filter for that particular criteria. If the param exists but its value is an empty string then it will ‘filter out’ everything.
This solution won’t suit every circumstance of course. If you have a view page with several filters on, but upon first opening you want to show all your data instead of no data until you press a ‘submit’ button or similar (as this controller would) then you will have to tweak it slightly.
I’ve had a go at SQL injecting this and rails seems to do a good job of keeping everything secure as far as I can see.
You should model url using nested resources. The expected url would be /authors/1/posts. Think of authors as resources. Read about nested resources in this guide: http://guides.rubyonrails.org/routing.html (scroll to 2.7 - Nested Resources).
Would something like this work?
def get
raise "Bad parameters...why are you doing this?" unless params[:filter].is_a?(Hash)
#post = Post.where(params[:filter])
end
Then you can do something like:
?filter[author_id]=1&filter[post_date]=... etc.

What's the correct way to run one controller action from another controller action without an HTTP redirect?

I'd like to be able to dispatch from one controller action to another conditionally, based on a combination of query parameters and data in the database.
What I have right now is something like:
class OldController < ApplicationController
def old_controller_action
if should_use_new_controller
new_params = params.dup
new_params[:controller] = "new_controller_action"
redirect_to new_params
return
end
# rest of old and busted
end
end
class NewController < ApplicationController
def new_controller_action
# new hotness
end
end
This works just fine, but it issues an HTTP redirect, which is slow. I'd like to be able to do this same thing, but within the same HTTP request.
Is there a clean way to do this?
Edit: The bounty will go to someone who can show me a clean way to do this that leaves the controllers and their actions relatively untouched (other than the redirect code itself).
Instead of calling code across actions, extract the code to lib/ or something, and call that code from both controllers.
# lib/foo.rb
module Foo
def self.bar
# ...
end
end
# posts_controller
def index
Foo.bar
end
# things_controller
def index
Foo.bar
end
Create an instance of the controller class:
#my_other_controller = MyOtherController.new
Then call methods on it:
#my_other_controller.some_method(params[:id])
I prefer the module idea, but this should do the trick.
You can also pass parameters as a whole from another controller:
#my_other_controller.params = params
I suspect you want option 3, but lets go through the some alternatives first
Option 1 - Push the controller selection logic into a helper that inserts the right link into your view. Benifits - controllers remain clean, Cons - if decision logic depending on submitted values this approach won't work. If URL is being called by external websites then this won't work.
Option 2 - Push the logic back into your model. Pro's - keeps controller clean. Cons - doesn't work well if you've got lots of sesson, params or render / redirect_to interaction.
Option 3 - Stay within the same controller. I suspect you are trying to replace some existing functionality with some new functionality, but only in some cases. Pro's - Simple and have access to everything you need. Cons - only works if it makes sense to use the same controller i.e. you're working with the same entity such as user, place or company.
Lets look an an example for option 3. My links controller has totally diferent behavour for admins than other users ...
class LinksController < ApplicationController
#...
def new
#Check params and db values to make a choice here
admin? ? new_admin : new_user
end
#...
private
def new_admin
#All of the good stuff - can use params, flash, etc
render :action => 'new_admin'
end
def new_user
#All of the good stuff - can use params, flash, etc
render :action => 'new_user'
end
end
If two controllers are trying to do the same thing, there's a very good chance this should be in a model. Take a good look at your design and -- I'm sorry I don't know your experience level with MVC -- read up on thin controller techniques:
http://weblog.jamisbuck.org/2006/10/18/skinny-controller-fat-model
http://www.robbyonrails.com/articles/2007/06/19/put-your-controllers-on-a-diet-already
http://andrzejonsoftware.blogspot.com/2008/07/mvc-how-to-write-controllers.html
If the problem is that you need the other controller to do the render, then maybe the route should have pointed there to begin with, and still the skinny controller technique should save the day.
If extracting the common code between controllers into a module doesn't work for you, I would use Rack middleware. I haven't seen code that uses ActiveRecord within middleware but I don't know of any reason why it shouldn't be possible since people have used Redis and the like.
Otherwise I think your only option would be to restart processing of the request with something like (untested, pseudo example):
env['REQUEST_URI'] = new_controller_uri_with_your_params
call(env)
This is similar to how integration tests are implemented. But I don't know if everything from call until you hit a controller is idempotent and safe to rerun like this. You could trace through the source and see. But even if it's ok now, it might break in any future version of rails or rack.
Using middleware would avoid this by letting you intercept the request before it's been run. You should still be able to share code with your rails application by extracting it out into common modules included in both places.
Honestly I think just doing the simple thing of factoring the common controller code is likely cleaner, but it's hard to know without the details of your situation so I thought I'd go ahead and suggest this.
Do this:
class OldController < ApplicationController
def old_controller_action
if should_use_new_controller
new_controller_action
end
# rest of old and busted
end
end
and the new controller
class NewController < OldController
def new_controller_action
# new hotness
end
end

Widget Data Across Multiple Controllers

Let's say that I have a widget that displays summary information about how many posts or comments that I have on a site.
What's the cleanest way to persist this information across controllers?
Including the instance variables in the application controller seems like a bad idea. Having a before filter that loads the data for each controller smells like code duplication.
Do I have to use a plugin like the Cells Plugin (http://cells.rubyforge.org/) or is there a simpler way of doing it?
Presumably, you have a single partial that displays this info. You can put the methods that fetch the data you need in ApplicationHelper or as class methods on whatever model(s) you're getting the data from. Then call that method in the partial when you need to display it.
I wound up doing something similar to this:
In controllers/application.rb
def load_sidebar
#posts = Post.find(:all)
end
To include the sidebar in various actions I did this:
before_filter :load_sidebar, :only => [ :index ] #load from application.rb file
I made the sidebar into a shared partial.
def load_sidebar
#posts = Post.find(:all)
end
You mentioned that you were displaying summary info. If you don't really want to load all of your posts into memory, you can do the following.
def load_sidebar
#post_count = Post.count(:id)
end

Resources