Suppose you want a Blog with two different layouts. One layout should look like a conventional Blog with a header, a footer, a menu and so on. The other layout should only contain the blog posts and nothing more. How would you do that without losing the connection to the model, forcing the execution and rendering of only one action and prevent to repeat yourself (DRY)?
posts_controller.rb
class PostsController < ApplicationController
layout :choose_layout
# chooses the layout by action name
# problem: it forces us to use more than one action
def choose_layout
if action_name == 'diashow'
return 'diashow'
else
return 'application'
end
end
# the one and only action
def index
#posts = Post.all
#number_posts = Post.count
#timer_sec = 5
respond_to do |format|
format.html # index.html.erb
format.json { render json: #posts }
end
end
# the unwanted action
# it should execute and render the index action
def diashow
index # no sense cuz of no index-view rendering
#render :action => "index" # doesn't get the model information
end
[..]
end
Possibly I want to go the wrong way, but I can't find the right one.
Update:
My solution looks like this:
posts_controller.rb
class PostsController < ApplicationController
layout :choose_layout
def choose_layout
current_uri = request.env['PATH_INFO']
if current_uri.include?('diashow')
return 'diashow'
else
return 'application'
end
end
def index
#posts = Post.all
#number_posts = Post.count
#timer_sec = 5
respond_to do |format|
format.html # index.html.erb
format.json { render json: #posts }
end
end
[..]
end
config/routes.rb
Wpr::Application.routes.draw do
root :to => 'posts#index'
match 'diashow' => 'posts#index'
[..]
end
Two different routes are pointing at the same location (controller/action).
current_uri = request.env['PATH_INFO'] saves the url into a variable and the following if current_uri.include?('diashow') checks if it is the route we configured in our routes.rb.
You would select which layout to render depending on a certain condition. For example, a parameter in the URL, the device in which the page is being rendered etc.
Just use that condition in your choose_layout function, instead of deciding the layout on the basis of action_name. The diashow action is unnecessary.
Related
Trying to implement a tree-like comments on the site via a gem - acts-as-commentable-with-threading.
Comments are excellent and are displayed on the site when I visit a site under the user (implemented via the gem devise).
But when trying to view pages anonymously, naturally, I receive an error that id is not may be due to the elements onto a blank.
This is my controller recipes_controller.rb:
class RecipesController < ApplicationController
before_action :authenticate_chef!, except: [:index, :show]
def show
#recipe = Recipe.find(params[:id])
#comments = #recipe.comment_threads.order('created_at desc')
#user_who_commented = current_chef
#new_comment = Comment.build_from(#recipe, #user_who_commented.id, "")
end
...
comments_controller.rb:
class CommentsController < ApplicationController
before_action :authenticate_chef!
def create
commentable = commentable_type.constantize.find(commentable_id)
#user_who_commented = current_chef
#comment = Comment.build_from(commentable, #user_who_commented.id, body)
respond_to do |format|
if #comment.save
make_child_comment
format.html { redirect_to(:back, :notice => 'Comment was successfully added.') }
else
format.html { render :action => "new" }
end
end
end
...
recipe.rb:
class Recipe < ActiveRecord::Base
acts_as_commentable
...
In views (recipes/show.html.erb) I put this render :
<%= render partial: "comments/template", locals: {commentable: #recipe, new_comment: #comment} %>
I think that you may need in the controller to create something like a design if ... else for those who just browse the site, because the default at this point in the show method is set current_chef, because of what and error.
You need to handle the special case in view(probably comment template) for anonymous visit. Cause then current_chef would be nil. So where you're using it in view and controller, handle that properly.
A tip: You don't need to assign current_chef to any instance variable actually. It's already a helper method. You can call it directly from view.
Is it possible to have a controller that interacts in a standard way at both the top level and also the nested level? Or will static routes need to be configured?
When I visit the first address /list/:list_id/items I want it to follow the nested_index method to display only a subset of the listed items (The items that belong to the list).
http://localhost:3000/list/:list_id/items
When I visit the below (/items) address I want it to show the whole list of items.
http://localhost:3000/items
/app/controllers/items_controller.rb
def index
#Item = Item.all
respond_to do |format|
format.html # index.html.erb
format.json { render json: #Item }
end
end
def nested_index
#list = List.find(params[:list_id])
#items = #list.items.paginate(page: params[:page], per_page: 5)
respond_to do |format|
format.html # index.html.erb
format.json { render json: #list }
end
end
/config/routes.rb
AppName::Application.routes.draw do
resources :list do
resources :items
end
end
# Do I need to add further routes here?
Personally, I think you should split this out into two separate controllers.
The index method of your controller should be designed to do just one thing. In the case of your nested route it should be fetching all the items appropriate for the selected list and passing them to the appropriate view. In the other instance it is fetching all items and (probably) passing them to a completely different view.
It seems you're trying to get one controller to do the job of two, simply for the sake of the controller's name.
I'd suggest creating an apps_controller and use that to collect all your items and display them, and leave your items_controller for its nested use.
Remember you don't need to name a controller after the model it interacts with ... rather, you should name it after the function it is responsible for. A controller which receives an activation code for a user account might update an is_active boolean on a User model, but you would call this controller Activations since that is what it does.
If you have lots of overlap between controllers you can move their code into modules and then include those modules in both controllers. This way you can DRY up your code whilst keeping the logic separate where necessary.
Take a look at these links for some ideas on code extraction:
http://railscasts.com/episodes/398-service-objects
http://railscasts.com/episodes/416-form-objects
But before you start refactoring all of your code into modules ... consider whether it adds anything to your codebase. Does it make things simpler? Does it make things more readable? Does it save you anything other than typing out a few more lines? If there's no benefit to refactoring ... just don't do it.
#Jon is right. This should be split into several different controllers:
# app/controllers/items_controller.rb
class ItemsController < ApplicationController
# default RESTful actions to operate on lists, for example #index
def index
#Item = Item.all
respond_to do |format|
format.html
format.json { render json: #item }
end
end
end
# app/controllers/lists_controller.rb
class ListsController < ApplicationController
# default RESTful actions to operate on lists
end
# app/controllers/lists/items_controllers.rb
class Lists::ItemsController < ApplicationController
def show
#list = List.find(params[:list_id])
#items = #list.items.paginate(page: params[:page], per_page: 5)
respond_to do |format|
format.html
format.json { render json: #items }
end
end
end
Routes:
AppName::Application.routes.draw do
resources :items
resources :lists do
resources :items
end
end
There is the following routes:
namespace :api do
namespace :v1 do
resources :places, only: [:index]
end
end
The code of the controller:
class API::V1::PlacesController < API::V1::ApplicationController
def index
#places = (!params[:id]) ? Place.all : Place.find_all_by_type_id(params[:id])
respond_to do |format|
format.json { render json: #places }
format.html
end
end
end
'Place' has 'type_id' field, and I want to filter places by its filter_id. As you can see, now I send the parameter through URL as "places?id=1". But may be I must send parameter as "places/1"? I need also to set up paths; now they doesn't work with "?id=1" form. Please, tell me, how should I do? Thanks.
Rails convention would be to have the list of the places in the "index" action mapped to the relative path /places (GET method).
And then /places/1 (GET) would be mapped to "show", which is intended for presenting a member of the collection. For "show", the route would assign the ID segment of the path ("1") to params[:id].
The guides have a table of default route mappings. The :type_id attribute in the model vs. the :id attribute in the route probably confused you.
A simple solution would be to use /places?type_id=1 instead. In your controller, you can have something like:
def index
collection = Place.all
collection = collection.where(:type_id => params[:type_id].to_s) unless params[:type_id].to_s.blank?
respond_to do |format|
# ...
end
end
Setting :type_id as a query parameter instead of integrating into the relative path seems especially reasonable to me since you are building an API and might add support for more filters in the future.
My recommendation is to rewrite it like this:
# Your routes
namespace :api do
namespace :v1 do
resources :places, only: [:index]
get "/places/by_type/:type_id" => "places#by_type", as: :places_by_type
end
end
# Your controller
class API::V1::PlacesController < API::V1::ApplicationController
def index
respond_to do |format|
format.json { render json: #places }
format.html
end
end
def by_type
#places = Place.where(type_id: params[:type_id])
respond_to do |format|
format.js { render json: #places }
format.html do
render action: "index"
end
end
end
end
I could be slightly wrong about the routes, but I'm pretty sure it should work.
I am completely new to rails and playing with the code to make pages work.
The link localhost:3000/zombies/1 works (show action)
but localhost:3000/zombies (index action) doesn't. Below are my routes and controller:
ROUTES ARE:
resources :zombies
CONTROLLER is:
class ZombiesController < ApplicationController
before_filter :get_zombie_params
def index
respond_to do |format|
format.html # index.html.erb
format.json { render json: #zombies }
end
end
def show
#disp_zombie = increase_age #zombie, 15
#zombie_new_age = #disp_zombie
respond_to do |format|
format.html # show.html.erb
format.json { render json: #zombie }
end
end
def increase_age zombie, incr
zombie = zombie.age + incr
end
def get_zombie_params
#zombie=Zombie.find(params[:id])
#zombies = Zombie.all
end
end
Why is this?
Editing answer based on the comment
I get a page with the error: ActiveRecord::RecordNotFound in
ZombiesController#index Couldn't find Zombie without an ID Rails.root:
C:/Sites/TwitterForZombies Application Trace | Framework Trace | Full
Trace app/controllers/zombies_controller.rb:85:in `get_zombie_params'
The url, localhost:3000/zombies which calls index action does not include id parameter.
That's why the app is failing at #zombie=Zombie.find(params[:id]).
If you want to fix this issue, use before_filter on show action only.
before_filter :get_zombie_params, only: :show
And insert this into index action as I have originally suggested.
def index
#zombies = Zombies.all
...
end
This is happening because when you define resources :zombies, you get these routes :
/zombies
/zombies/:id
Therefore when navigating to /zombies you don't have a params[:id], it is nil
Zombie.find method will raise an error if it can't find any record with the given id and halt further processing of your code.
You can use Zombie.find_by_id if you don't want an exception raised when there is no result.
But I don't think that this what you want here, you'd rather define a get_zombie_by_id method and a get_all_zombies method and separate the code from your get_zombie_params
Then you would have to define which method should be called before what action by changing your before_filter like so, in your case :
before_filter :get_zombie_by_id, :only => :show
before_filter :get_all_zombies, :only => :index
This way Zombie.find(params[:id]) will only get called when on the show action.
You can also use :except to do the opposite.
it does work because you need to send back ( to your index view ) the list of your zombies.
The get_zombie_params() excutes right but does not send #zombies to the index() action.
you need to do :
def index
#zombies = Zombie.all
#... the rest of the code
end
I seem to have an authorization hiccup in my Ruby on Rails app. I have been using the following method in my application controller and it has been working beautifully.
def require_owner
obj = instance_variable_get("##{controller_name.singularize.camelize.underscore}") # LineItem becomes #line_item
return true if current_user_is_owner?(obj)
render_error_message("You must be the #{controller_name.singularize.camelize} owner to access this page", root_url)
return false
end
I then filter in the specific controllers by:
before_filter :require_owner, :only => [:destroy, :update, :edit]
I recently created a new controller which has a bit of a different naming convention that seems to be causing a problem. Normally my controllers read messages_controller or posts_controller. In this specific case I named the resource box_wod which generated box_wods_controller.
This is the only controller that seems to be having a problem with this filter so I bet I can tell it is in the naming of it and therefore the application_controller method is not recognizing the owner of the record.
I am not getting an error message but the application is not letting me edit, update or destroy a record because I am not the BoxWod owner. My routes are correct as are my associations and the correct information is getting passed to the box_wod table.
Is there a way to rewrite the application_controller method to recognize the additional underscore in the box_wod resource? Or is this even my problem?
UPDATE:
Here are the three methods in the BoxWodsController:
def edit
#workout_count = Workout.count
#box_wod = BoxWod.find(params[:id])
end
def update
#box_wod = BoxWod.find(params[:id])
respond_to do |format|
if #box_wod.update_attributes(params[:box_wod])
flash[:notice] = 'BoxWod was successfully updated.'
format.html { redirect_to(#box_wod) }
format.xml { head :ok }
else
format.html { render :action => "edit" }
format.xml { render :xml => #box_wod.errors, :status => :unprocessable_entity }
end
end
end
def destroy
#box_wod = BoxWod.find(params[:id])
#box_wod.destroy
respond_to do |format|
format.html { redirect_to(box_wods_url) }
format.js
end
end
In situations like this, I like to create a controller method that I can override when necessary. For example:
# application_controller.rb
class ApplicationController
def require_owner
obj = instance_variable_get("##{resource_instance_variable_name}")
# Do your authorization stuff
end
private
def resource_instance_variable_name
controller_name.singularize.camelize.underscore
end
end
# box_wods_controller.rb
class BoxWodsController
private
def resource_instance_variable_name
'box_wod' # Or whatever your instance variable is called
end
end
Lastly, please post your BoxWodsController code so we can better diagnose the problem.
It would seem that the #box_wod instance variable is not created until the require_owner method is invoked so current_user_is_owner? is checking a nil value, resulting in it always returning false. Perhaps you need another before_filter to populate the instance variable before require_owner is invoked.