Let us assume we have two resources: auctions and bids, and that an auction has many bids possibly modeled as nested resources. There are two possible collections of bids: the collection of all bids in the rails application and the collection of bids associated with a particular auction. If the user wants to access both of these collections, how would we model this in the controllers?
One possible solution is to construct two routes (auctions/:auction_id/bids and bids/) that point to the BidsController#index action and branch according to the presence of params[:auction_id]. One can imagine that the situation worsens if a bid also belongs to a user, thus creating another conditional on our BidsController checking for params[:user_id] to return the collection of bids associated to a given user. What is the rails way of dealing with this problem?
The out of the box solution for dealing with nested resources is to handle it in child resource controller:
Sampleapp::Application.routes.draw do
resources :bids
resources :users do
resources :bids # creates routes to BidsController
end
resources :auctions do
resources :bids # creates routes to BidsController
end
end
class BidsController < ApplicationController
before_action :set_bids, only: [:index]
before_action :set_bid, only: [:show, :update, :destroy]
def index
end
# ... new, create, edit, update, destroy
def set_bid
# when finding a singular resources we don't have to give a damn if it is nested
#bid = Bids.find(params[:id])
end
def set_bids
#bids = Bids.includes(:user, :auction)
if params.has_key?(:user_id)
#bids.where(user_id: params[:user_id])
elsif params.has_key?(:auction_id)
#bids.where(auction_id: params[:auction_id])
end
#bids.all
end
end
The advantage here is that you get the full CRUD functionality with very little code duplication.
You could however quite simply override the index routes if you want to have different view templates, or do different sorting etc.
Sampleapp::Application.routes.draw do
resources :bids
resources :users do
member do
get :bids, to: 'users#bids', :bids_per
end
resources :bids, except: [:index]
end
resources :auctions do
member do
get :bids, to: 'auction#bids', as: :bids_per
end
resources :bids, except: [:index]
end
end
Another solution would be creating a AuctionsBidsController and defining the route as such:
resources :auctions do
resources :bids, only: [:index], controller: 'AuctionsBids'
resources :bids, except: [:index]
end
You don't necessarily have to involve the bids controller every time you want to display a list of bids. For example, if I were setting up this scenario and wanted to display a list of bids related to a particular auction, I would handle that in the auctions controller, not the bids controller, like so:
class AuctionsController < ApplicationController
...
def show
#auction = Auction.find(params[:id])
#bids = #auction.bids #assuming the bids belong_to your Auction model
end
Then, somewhere in the view app/views/auctions/show.html.erb you can list the bids using the same partial you use in your bids index:
<ul>
<%= render partial: 'bids/bid', collection: #bids %>
</ul>
This is just one way to handle the situation. You could do the same thing with your User model or any other model in your application. The point is that you don't have to involve a model's specific controller every time you want that model to appear in a view.
Related
My project is about an online shopping site, using Ruby on Rails to buy phones.
My Database is User, Product, Phone.
I'm trying to create Basket model.
My route:
resources :products do
resources :phone do
resources :baskets
end
end
And my Code is:
class User < ActiveRecord::Base
has_many :baskets
end
class Phone < ActiveRecord::Base
belongs_to :product
has_many :baskets
end
class Basket < ActiveRecord::Base
belongs_to :user
belongs_to :phone
end
When i in the Show action of Product,it Show name of Product and index Phones in this Product,i want to add 1 Phone to Basket,the error is :
No route matches {:action=>"new", :controller=>"baskets", :id=>"38", :product_id=>"30"} missing required keys: [:phone_id]
I think the problem is :
http://localhost:3000/products/30/phone/38
It's Product_id = 30,but not Phone_id = 30,in here just is Id = 30.
Someone could help me fix it !
resources :products do
resources :phone do
resources :baskets
end
end
means you have to have route like this:
/products/:product_id/phones/:phone_id/baskets/:basket_id(.:format)
Which means, that in link_to you should pass the phone_id as well:
link_to 'show basket' product_phone_basket_path(product_id: #product.id, phone_id: #phone.id, basket_id: #basket.id)
link_to 'New basket' new_product_phone_basket_path(product_id: #product.id, phone_id: #phone.id)
Regardless of whether you got it working (I upvoted #Andrey's answer), you'll want to consult your routing structure.
Resources should never be nested more than 1 level deep. docs
--
In your case, I am curious as to why you have phones nested inside products. Surely a phone is a product?
Further, why are you including resources :baskets? Surely the basket functionality has nothing to do with whether you're adding a product, phone, or anything else?
I would personally do the following:
resources :products, only: [:index, :show] do
resources :basket, path:"", module: :products, only: [:create, :destroy] #-> url.com/products/:product_id/
end
#app/controllers/products/basket_controller.rb
class Products::BasketController < ApplicationController
before_action :set_product
def create
# add to cart
end
def destroy
# remove from cart
end
private
def set_product
#product = Product.find params[:product_id]
end
end
I've implemented a cart (based on sessions) before (here).
I can give you the code if you want; I won't put it here unless you want it. It's based on this Railscast.
I am trying to make a "next" button that should generate a Viewed_lesson (user_id, lesson_id, boolean: true) and then redirect to the next lesson. The purpose is to make a tracking progress to show the progress of the user over a course.
My models:
class Course
has_many :lessons
end
class Lesson
#fields: course_id
belongs_to :course
end
class User
has_many :viewed_lessons
has_many :viewed_courses
end
class ViewedLesson
#fields: user_id, lesson_id, completed(boolean)
belongs_to :user
belongs_to :lesson
end
class ViewedCourse
#fields: user_id, course_id, completed(boolean)
belongs_to :user
belongs_to :course
end
What would the create action of viewed_lesson controller should look like?
There is only a create method needed.
And how would the button/form would look like in the view? The button is placed in lectures/id/lesson/id
Thank you for your help in advance!
I would say that your design sounds a little bit off. In rails there is not always a 1:1 crud relation between models and controllers.
First off I would rename those models UserLesson and UserCouse. It makes the relation really obvious and ViewedLesson stinks because it implies some sort of state.
In Rails a 1:1 relation between models and controllers is not always the best solution. And sometimes just using the standard crud verbs might not cut it.
Imagine the follow scenarios:
Does POST /user/1/lessons mean that a user has completed a course?
Or does it mean the user has just started a lesson?
Instead you might want to do a design like the following:
# config/routes.rb
resources :lessons # Used by admins to CRUD the curriculum.
resources :users
resources :lessons, controller: `users/lessons` do
member do
post :complete
end
end
end
# app/controllers/users/lessons_controller.rb
module Users
class LessonsController
before_action :set_lesson, only: [:show, :update, :complete]
# ...
# Enroll user in a lession
# POST /users/:user_id/lessons/:id
def create
#user_lesson = UserLesson.create(lesson: params[:id], user: params[:user_id])
end
# POST /users/:user_id/lessons/:id/complete
def complete
# #todo check if user has completed all steps...
end
# Save user progress when they reach certain milestones
def update
# #todo update user_lesson
end
def set_lesson
#lesson = Lession.find(params[:id])
end
def set_user_lesson
#user_lesson = UserLesson.find_by(user: params[:user_id], lession_id: params[:id])
end
end
end
Another alternative would be if you don't want to nest the routes under users:
resources :lessions, controller: 'user_lessions' do
member do
post :complete
end
end
# or
resources :courses, controller: 'user_courses' do
resources :lessions, controller: 'user_lessions' do
member do
post :complete
end
end
end
namespace :admin do
resources :lessions # used to crud the corriculum
end
It should look like following:
class ViewedLessonsController < ApplicationController
before_filter :set_user_and_lesson
def create
#viewed_lesson = ViewLession.new(user_id: #user.id, lession_id: #lesson.id, completed: true)
if #viewed_lesson.save
# redirect_to appropriate location
else
# take the appropriate action
end
end
end
In your ApplicationController class, you can do:
def set_user_and_lesson
#user = User.find_by_id(params[:user_id])
#lesson = Lesson.find_by_id(params[:lesson_id])
end
You also need to define the routes for it, and remember, you need to have models as well for ViewedLesson and ViewedCourse.
I have the following models:
class Article < ActiveRecord::Base
belongs_to :category
end
class Category < ActiveRecord::Base
end
Articles are nested under categories like this:
resources :categories, only: [] do
resources :articles, only: [:show]
end
which gives me the following route
GET /categories/:category_id/articles/:id articles#show
To use this in my views do i need to write
link_to category_article_path(article.category, article)
# => "/categories/1/articles/1
Their isn't any article_path, all articles will always be nested under the associated category. However i was wondering if their is a way for me to DRY this up so i could simply write
link_to article_path(article)
# => "/categories/1/articles/1
and still have the route nested, under category.
I know that i could just write a helper or something similiar, but i would like a more general solution, because i know that i would get more nested resources under categories at some point (like blogs, videos, etc.) that all has a belongs_to relation to the categories.
So basically am I looking for something like this
# Example, doesn't work (article_id is not available here):
resources :categories, only: [] do
resources :articles, only: [:show], defaults: { category_id: Article.find(article_id).category.id }
end
However that doesn't work, as article_id isn't available in the defaults block. As far as i know, can you only create static default values, and i need a dynamic one based on the current article.
You can achieve what you want by aliasing your resource like you would do in a normal route.
resources :categories, only: [] do
resources :articles, only: [:show], defaults: { category_id:Article.find(article_id).category.id }, as: 'article'
end
Now you can use article_path(article) instead of category_article_path
I have a resources :shops
which results in a /shops, /shops/:id, etc
I know I can scope collection or members with
resources :shops do
scope ":city" do
# collection and members
end
end
or do it before with
scope ":city" do
resources :shops
end
But I can't figure out how to make the route be on all members (including the standard REST ones) and collection, like so
/shops/:city/
/shops/:city/:id
As per your use case and question, you are trying to have logically wrong routes. You have shops within the city, NOT city within the shop.
Firstly you should normalize your database. You should create another table cities and replace your city attributes with city_id in shops table.
You need has_many and belongs_to association between cities and shops.
# Models
class City < ActiveRecord::Base
has_many :shops
... # other stuff
end
class Shop < ActiveRecord::Base
belongs_to :city
... # other stuff
end
Routes
resources :cities do
resources :shops
end
It will generate routes like:
POST /cities/:city_id/shops(.:format) shops#create
new_city_shop GET /cities/:city_id/shops/new(.:format) shops#new
edit_city_shop GET /cities/:city_id/shops/:id/edit(.:format) shops#edit
city_shop GET /cities/:city_id/shops/:id(.:format) shops#show
PATCH /cities/:city_id/shops/:id(.:format) shops#update
PUT /cities/:city_id/shops/:id(.:format) shops#update
DELETE /cities/:city_id/shops/:id(.:format) shops#destroy
Logically, these routes will show that in which city the particular shop exists.
Namespace
You may wish to consider including a namespace
Since you're trying to pull up the cities for shops (IE I imagine you want to show shops in Sao Paulo), you'd be able to do this:
#config/routes.rb
namespace :shops do
resources :cities, path: "", as: :city, only: [:index] do #-> domain.com/shops/:id/
resources :shops, path: "", only: [:show] #-> domain.com/shops/:city_id/:id
end
end
This will allow you to create a separate controller:
#app/controllers/shops/cities_controller.rb
Class Shops::CitiesController < ApplicationController
def index
#city = City.find params[:id]
end
end
#app/controllers/shops/shops_controller.rb
Class Shops::ShopsController < ApplicationController
def show
#city = City.find params[:city_id]
#shop = #city.shops.find params[:id]
end
end
This will ensure you're able to create the routing structure you need. The namespace does two important things -
Ensures you have the correct routing structure
Separates your controllers
I have simple blog app with posts, comments and so on and wanna add categorization to posts, i.e. every post belongs to only one category and inside it post showed.
Now inside routes
resources :category do
resources :posts
end
I wanna paths like
category/job
I generate CategoryController, but how to fill it and chain with already existing posts controller?
class CategoryController < ApplicationController
def index
#category = Category.all
end
def show
#category = Category.find(params[:id])
end
end
Also how could be views looks like of categories so that inside it showed posts?
On your place I would leave posts and categories as separate resources. Like:
resources :posts
resources :categories, only: [:show]
Then your route category/job will actually be a simple #show action which you could implement like
class CategoriesController < ApplicationController
def show
#category = Category.find(params[:id])
#posts = #category.posts
end
end
In order to achieve the "job" to be an id inside the url in the Category model you should add something like
class Category < Active
def to_param
name
end
end
This way you will keep your resources clean and simple and not introduce unnecessary complexity.