Displaying different views for the same controller, based on the route - ruby-on-rails

I am building an admin interface for a website. I have certain controllers that have admin functions/views that also have user facing views. For example, when a user goes to /blog, it should show the title, date, first paragraph, etc. of each blog post with links to read the whole post. If an admin goes to admin/posts they would see a list of just the blog post titles, how many comments, edit/delete links, link to create a post, etc.
How would I accomplish this? My (simplified) routes files is this:
namespace :admin do
resources :posts
end
Do I need to have separate controllers?

Usually when using namespaces you want your code to be namespaced as well. I would go for 2 different controllers serving 2 different views.
app/controllers/posts_controller.rb
app/controllers/backend/posts_controller.rb
or
app/controllers/posts_controller.rb
app/controllers/admin_area/posts_controller.rb
You get the idea. I would do the same thing with the views.
You controllers would look like this:
class PostsController < ApplicationController
end
class Backend::PostsController < BackendController
end
class BackendController < ApplicationController
end

There are quite a few ways you could approach this, I can't really think of one being "right" over the other. For simplicity's sake, I'll offer one quick solution, though, admittedly, it's a shortcut.
Presumably, you have a logged in user going to this /admin route, and the current_user would be authorized as an admin, so you can use that to your advantage in your show method.
if current_user.admin?
render 'admin_show'
else
render 'show'
Your views, of course, would render the different implementations.
If you are going to have a number of differences in what the methods do, though, it may be worth creating a new admin_posts_controller. But if there are only a couple differences, then that could be good enough.
Meh, hope that helps.

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 CanCan: reference to resource_instance from application_controller

I have a bunch of controllers that extend the ApplicationController and each one loads a different cancan resource, e.g. UsersController -> #user, PostsController -> #post. I was wondering, is it possible to reference the resource from the ApplicationController without knowing the instance variable name? Something like resource_instance.
Ok, I got my final answer and it's No. I explored cancancan, cancan's well maintained spiritual child and the code is there, but is not meant to be used by the end user.
You could get what you want, it isn't pretty:
self.class.cancan_resource_class.new(self).send(:resource_instance)
That said, please don't. cancan_resource_class isn't documented to to be used by end users and :resource_instance is protected, hence the send. The developers could choose to change this and break your application.
If all have a company, they belong too, I recommend you nest the routes, so all routes have a :company_id you could use to get the #company object you want: companies/:company_id/users, etc.

Dynamic Routes Rails 4, taken from db

Frustrating, I can't find an eligible solution for my problem.
In my Rails 4 app, I want to give my users the possibility to add their own custom post types to their sites. Like:
www.example.com/houses/address-1
www.example2.com/sports/baseball
Both would work, but only for the linked sites. Sports and houses would be the (RESTful) post types, taken from the db, added by users.
I have been struggling to find a elegant solution to accomplish this. I found http://codeconnoisseur.org/ramblings/creating-dynamic-routes-at-runtime-in-rails-4 but that feels kinda hacky and I'm not sure if reloading the routes works in production, I'm getting signals that it won't.
I'd say I have to use routes constraints http://guides.rubyonrails.org/routing.html#advanced-constraints but I don't have a clue how to approach this.
To be clear, I have no problem with the site setting stuff, the multi tenancy part of my app is fully functional (set in Middleware, so the current site is callable in the routes.rb file). My issue is with the (relative) routes, and how they could be dynamically set with db records.
Any pointers much appreciated.
I think route constraints don't work for you because your domain is a variable here. Instead, you should be examining the request object.
In your ApplicationController, you could define a method that would be called before any action, like so:
class ApplicationController < ActionController::Base
before_action :identify_site
def identify_site
#site = Site.where(:domain => request.host).first
end
end
As you scale, you could use Redis for your domains so you're not making an expensive SQL call on each request.
Then you can just add the #site as a parameter to whatever call you're making. I'm assuming you're doing some sort of "Post" thing, so I'll write some boilerplate code:
class PostController < ApplicationController
def show
#post = Post.where(:site => #site, :type => params[:type], :id => params[:id])
end
end
Just write your routes like any other regular resource.

Organizing site navigation actions in 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

What is the best way to implement comments in Rails?

To the point: I want to put Post has_many Comments but I do not want to create a separate comment controller and subsequent views. Mainly because the comments will never show up anywhere else but inside the SHOW action of a Post. Or am I breaking the MVC paradigm?
You are breaking the MVC paradigm, as you said. The point of MVC is to split everything up into bite-size chunks so it is more manageable. That's how I see it at least.
How would comments be created without a specific controller for them. The showing part on a Post is the easy part:
#comments = #post.comments
There is a fundamental distinction to be made between the internal domain model of your system and the public interface your system exposes.
If you are using a relational database, it is good practice to have
Comment.belongs_to :post
Post.has_many :comments
The internal domain model of your system can help you design your public interface - but you can also tailor your public interface how you want it, without being forced to make it a strict reflection of your internal domain model!
In your case, I would suggest having a CommentsController. But in this controller class, you do not need all of the normal REST actions. You only need a few of them.
# app/controllers/comments_controller.rb
class CommentsController < ApplicationController
respond_to :js
def create
#post = Post.find(params[:post_id])
#comment = post.comments.create(params[:comment])
respond_with [#post, #comment]
end
end
In this controller, you only have a create action which would be the target of the "new comment" form at the bottom of the page displaying a post. You do not need any of the other REST actions because people never view, edit, or delete a comment in isolation - they only create new ones, and not from a dedicated new-comment page either. The routing for this is as follows:
# config/routes.rb
MyApp::Application.routes.draw do
resources :posts do
resources :comments, :only => [:create]
end
end
The more you deviate from the MVC paradigm, the more problems you'll have later on. For example, if you wanted to add admin views for your Comments, it would be easier to expand on it through the Comments Controller. Else, you'll end up having multiple actions for your comment in the Posts controller (eg. approve_comment, delete_comment, voteup_comment, etc).
That being said, you can always wire things up so that actions on your comments direct the user back to the Post that originated it. So, comment related actions will reside in the Comments Controller, but the user is generally working with Posts (and its associated Comments).

Resources