I have a many_to_many association between Articles and Categories, using has_and_belongs_to_many in a Rails 4 app:
Here are the corresponding migration and classes:
class CategoriesArticles < ActiveRecord::Migration
def change
create_table :articles_categories, id: false do |t|
t.belongs_to :category, index: true
t.belongs_to :article, index: true
end
add_index :articles_categories, [:category_id, :article_id]
end
end
class Category < ActiveRecord::Base
has_and_belongs_to_many :articles
end
class Article < ActiveRecord::Base
has_and_belongs_to_many :categories
end
When a user creates a new article, I simply want to give him or her the option to select categories that he/she wants to associate with the new article. I want the user to be able to select these categories with checkboxes.
Here's the ArticlesController:
class ArticlesController < ApplicationController
before_action :set_article, only: [:show, :edit, :update, :destroy]
before_action :authenticate_user!, only: [:new, :create, :edit, :destroy, :update]
before_action :verify_own_article, only: [:destroy]
respond_to :html
...
def new
#categories = Category.all
#article = Article.new
respond_with(#article)
end
def create
# Creates article object with current_user_id, initial_comment, and URL
#article = current_user.articles.build(article_params)
# Uses Pismo (gem) to grab title, content, photo of URL
#article.populate_url_fields
if #article.save
flash[:success] = "Article created!"
# Might need to change the location of this redirect
redirect_to root_url
else
flash[:notice] = "Invalid article."
redirect_to new_article_path
end
end
def update
#article.update(article_params)
flash[:notice] = "Article successfully updated."
respond_with(#article)
end
private
def set_article
#article = Article.find(params[:id])
end
def article_params
params.require(:article).permit(:url, :title, :datetime, :content, :photo, :initial_comment)
end
# Ensure that a signed in user can only delete articles that they have posted
def verify_own_article
#article = current_user.articles.find_by_id(params[:id])
end
end
Here's the article new.html.erb view:
<h1>New article</h1>
<%= render 'form' %>
<%= link_to 'Back', articles_path %>
... and the form partial:
<%= form_for(#article) do |f| %>
<% if #article.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(#article.errors.count, "error") %> prohibited this article from being saved:</h2>
<ul>
<% #article.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :url %><br>
<%= f.text_field :url %>
</div>
<div class="field">
<%= f.label :initial_comment %><br>
<%= f.text_field :initial_comment %>
</div>
<% #categories.each do |t| %>
<div class="field">
<%= f.label t.name %>
<%= f.check_box "categories[#{t.id}]" %>
<br />
</div>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
However, this is erroring for me, specifically the lines:
<% #categories.each do |t| %>
<div class="field">
<%= f.label t.name %>
<%= f.check_box "categories[#{t.id}]" %>
<br />
</div>
<% end %>
Specifically, it's telling me:
undefined method 'categories[1]' for #<Article:0x007f401193d520> when I try to render the New Article page. How do I fix this? Thanks.
It is better to use Rails collection_check_boxes helper instead of trying to create those checkboxes by hand. This helper already creates all the parameter / markup stuff you need in order to add or exclude items of a HABTM relation, all under the hood. So you might change you view to include the following:
<%= f.collection_check_boxes :categories_ids, #categories, :id, :name %>
Don't forget to add this in your strong parameters declaration (since you'll have to receive the selected categories ids and bind them to your Article model):
params.require(:article).permit(
:url, :title, :datetime, :content,
:photo, :initial_comment, categories_ids: []
)
For further customizations (html styling or structure for each checkbox), please refer to the complete documentation
I hope it helps :)
Related
I currently have review model that will allow a user to create reviews for a tea model. The user who creates the review can edit or delete the review. I have a nested route within teas that allows you to create a new review for teas as you are viewing all reviews for that specific tea. Currently the nested new route does not allow creation as well as a google authenticated user can not create a review. Below is my controller action and view. I am not experiencing any error it just appears to rollback the database and follow the else logic and render the new page again.
Model
class Review < ApplicationRecord
belongs_to :user
belongs_to :tea
validates :title, presence: true
validates :rating, numericality: {only_integer: true, greater_than_or_equal_to: 0, less_than: 11}
validates :tea, uniqueness: {scope: :user, message: "has already been reviewed by you" }
scope :order_by_rating, ->{left_joins(:reviews).group(:id).order('avg(rating) desc')}
end
Controller Action
def create
#review = current_user.reviews.build(review_params)
if #review.valid?
#review.save
redirect_to new_review_path(#review)
else
render :new
end
end
View
<%= form_for Review.new do |f|%>
<% if params[:tea_id] %>
<%= f.hidden_field :tea_id %>
<% else %>
<div>
<%= f.label :tea_id, "Select a Tea Blend" %>
<%= f.collection_select :tea_id, Tea.alpha, :id, :flavor_and_brand, include_blank: true %>
</div>
<% end %>
<div>
<%= f.label :rating %>
<%= f.number_field :rating, min:0, max:10 %>
</div>
<br>
<div>
<%= f.label :title %>
<%= f.text_field :title %>
</div>
<br>
<div>
<%= f.label :content %>
<br>
<%= f.text_area :content, size: "60x25" %>
</div>
<br>
<%= f.submit %>
<% end %>
The simple answer was that I did not include create in my before action. This is what was causing my set_tea to not be automatically done as a before action.
The correct way to do this is by defining a nested route and setting up the form so that it posts to that route. So instead of creating a single form where the user has to select the tea you create a form on the show page or by each tea in a index page where the user can create reviews.
# config/routes.rb
resources :teas do
resources :reviews, shallow: true
end
shallow: true makes it so that the member actions (show, edit, update, destroy) are not nested.
Then setup a partial for the form so that you can reuse it:
# app/views/reviews/_form.html.erb
<%= form_for([local_assigns(:tea), review]) do |f| %>
<div class="field">
<%= f.label :rating %>
<%= f.number_field :rating, min:0, max:10 %>
</div>
<div class="field">
<%= f.label :title %>
<%= f.text_field :title %>
</div>
<div class="field">
<%= f.label :content %>
<%= f.text_area :content, size: "60x25" %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
By passing an array you get the nested route as the action attribute (/teas/1/reviews) and don't have to monkey around with a hidden input. local_assigns(:tea) avoids a NoMethodError if its not passed to the partial. The array is compacted so that this partial will work for both creating and updating.
# app/views/reviews/new.html.erb
<%= render partial: 'form', tea: #tea, review: #review >
# app/views/reviews/edit.html.erb
<%= render partial: 'form', review: #review >
# app/views/teas/show.html.erb
<h2>Review this tea</h2>
<%= render partial: 'reviews/form', tea: #tea, review: #tea.reviews.new >
In the controller you can just fetch the tea from params[:tea_id] since you passed it in the path.
class ReviewsController < ApplicationController
before_action :set_tea, only: [:new, :index, :create]
before_action :set_review, only: [:show, :edit, :update, :destroy]
# POST /teas/1/reviews
def create
# creating the review off the tea reveals intent better than doing
# it off the user
#review = #tea.reviews.new(review_params) do |r|
r.user = current_user
end
# Always check if the record is actually persisted
# - not just if the applications validations pass!
if #review.save
# you could also redirect to the review but this makes more
# sense from a ux perspective
redirect_to #tea, notice: 'Thank you for your review'
else
render :new
end
end
# GET /reviews/:id/edit
def edit
end
# PUT|PATCH /reviews/:id
def update
if #review.update(review_params)
redirect_to #review, notice: 'Review updated.'
else
render :edit
end
end
private
def set_tea
#tea = Tea.find(params[:tea_id])
end
def set_review
#review = Review.find(params[:id])
end
def review_params
params.require(:review).permit(:rating, :title)
end
end
im beginner of ruby on rails. i have brand - products list.
Brand,
class Brand < ApplicationRecord
has_many :products, dependent: :destroy
validates :title, presence: true,
length: { minimum: 2 }
end
Product,
class Product < ApplicationRecord
belongs_to :brand
end
products.controller
class ProductsController < ApplicationController
skip_before_action :verify_authenticity_token
def edit
#product = Product.find(params[:id])
end
def new
#product = Product.new
end
def update
#brand = Brand.find(params[:brand_id])
#product = Product.find(params[:id])
#product.update(product_params)
redirect_to brand_path(#brand)
end
def create
#brand = Brand.find(params[:brand_id])
#product = #brand.products.create(product_params)
redirect_to brand_path(#brand)
end
def destroy
#brand = Brand.find(params[:brand_id])
#product = #brand.products.find(params[:id])
#product.destroy
redirect_to brand_path(#brand)
end
def update
#brand = Brand.find(params[:brand_id])
#product = #brand.products.find(params[:id])
#product.destroy
end
helper_method :update
private
def product_params
params.require(:product).permit(:name)
end
end
the new.html.erb,
<h1> Add a new product </h1>
<%= form_with model: #brand, local: true do |form| %>
<p>
<%= form.label :title,"Product name" %><br>
<%= form.text_field :name %>
</p>
<p>
<%= form.label :title,"Select a Brand" %><br>
<%= form.collection_select(:brand, Brand.all, :id, :title) { #brand = Brand.find(params[:brand_id]) } %>
</p>
<p>
<%= form.submit "Save Product", :onclick => "create" %>
</p>
<% end %>
routes.rb
Rails.application.routes.draw do
get 'welcome/index'
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
resources :brands do
resources :products
end
root 'welcome#index'
end
when i cliked the button i get error,
No route matches [POST] "/brands/%23%3CBrand::ActiveRecord_Relation:0x00007f5e30d87490%3E/products/new"
Rails.root: /home/berkay/e-ticaret
SO, how can i save this product?
change that one
<%= form_with model: #brand, local: true do |form| %>
to
<%= form_with(model: #product, url: [#brand, #product]) %>
Also add
#brand = Brand.find(a_brand_id)
inside your new method of ProductsController class. So, rails is going to know which brand is parent of that product.
UPDATE
I've created a dummy project which is going to work as you expected.
products/_form.html.erb partial for product
<%= form_with(model: product, local: true) do |form| %>
<% if product.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(product.errors.count, "error") %> prohibited this product from being saved:</h2>
<ul>
<% product.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= form.label :title %>
<%= form.text_field :title %>
</div>
<div class="field">
<%= form.label :title, "Select a Brand" %><br>
<%= form.collection_select(:brand_id, Brand.all, :id, :title, {selected: #brand.id}) %>
</div>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
routes.rb
Rails.application.routes.draw do
resources :products
resources :brands
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end
products_controller.rb
class ProductsController < ApplicationController
# GET /products/new
def new
if params[:brand_id]
#brand = Brand.find(params[:brand_id])
end
#product = Product.new
end
def edit
#brand = #product.brand
end
...
# Never trust parameters from the scary internet, only allow the white list through.
def product_params
params.require(:product).permit(:title, :brand_id)
end
end
I've also added a hotlink to create a product for a given brand
brands/show.html.erb
<p id="notice"><%= notice %></p>
<p>
<strong>Title:</strong>
<%= #brand.title %>
</p>
<%= link_to 'Edit', edit_brand_path(#brand) %> |
<%= link_to 'Back', brands_path %> |
<%= link_to 'Create Products', new_product_path(brand_id: #brand.id) %>
When using nested routes you would have to write
form_with model: [#brand, #product]
it will use the array to compose the path and the last item will be actually edited.
Your new action should best be changed as follows:
def new
#brand = Brand.find(params[:brand_id)
#product = #brand.products.build
end
I am working on a form for a editorial calendar app. I have two things going out that are pretty similar and not working.
Working with 3 models: Platforms, Posts and Calendars. They are join tables. Platform <=> Post, Post <=> Calendars
Post/new & Post/edit form:
<div class="container">
<div class="form-field">
<%= form_for #post do |f| %>
<%= f.label :title %>
<%= f.text_field :title, required: true %> <br>
Title is required.
</div>
<div class="form-field">
<%= f.label :content%>
<%= f.text_area :content%>
</div>
<div class="form-field">
<%= f.label :link %>
<%= f.text_field :link %>
</div>
<div class="file-field">
<%= f.label :picture%>
<%= f.file_field :picture, id: :post_picture%>
</div>
<div class="file-field">
<%= f.label :finalized %>
<%= f.radio_button :finalized , true%>
<%= f.label :finalized, "Yes" %>
<%= f.radio_button :finalized, false %>
<%= f.label :finalized, "No" %>
</div>
<%= f.hidden_field :user_id %> <br>
<div class="form-field">
<%= f.fields_for :platform_attributes do |platform| %>
<%= platform.label :platform, "Social Platforms"%>
<%= platform.collection_check_boxes :platform_ids, Platform.all, :id, :name %> <br> <br>
</div>
<div>
<h4> Or Create a new platform: </h4>
<%= platform.label :platform, 'New Platform'%>
<%= platform.text_field :name%> <br> <br>
</div>
<% end %>
<%= f.submit%>
<% end %>
</div>
My post controller is handling the checkboxes issue, and the "schedule post" issue. It will only allow me to schedule for one calendar, and it does not save the updates and add additional calendars.
Posts Controller:
class PostsController < ApplicationController
before_action :set_post, only: [:show, :edit, :update, :schedule_post, :destroy]
def new
#posts = current_user.posts.select {|p| p.persisted?}
#post = current_user.posts.build
#platforms = Platform.all
end
def edit
#calendars = current_user.calendars
#platforms = Platform.all
end
def create
#post = current_user.posts.build(post_params)
if #post.save
redirect_to post_path(#post)
else
redirect_to new_post_path
end
end
def update
#post.update(post_params)
if #post.save
redirect_to post_path(#post), notice: 'Your post has been updated.'
else
redirect_to edit_post_path(#post)
end
end
def schedule_post
#calendar_post = CalendarPost.new(calendar_post_params)
if #calendar_post.save
binding.pry
redirect_to post_path(#post)
else
render 'show'
end
end
private
def set_post
#post = Post.find(params[:id])
end
def set_calendars
#calendars = current_user.calendars
end
def post_params
params.require(:post).permit(:title, :content, :link, :finalized, :picture, :user_id, :platform_attributes => [:platform_ids, :name])
end
def calendar_post_params
params.require(:calendar_post).permit(:post_id, :calendar_id, :date, :time)
end
end
I want the user to be able to add a post to multiple platforms and multiple calendars because of the versatility of what someone may need.
I also have my setter in my Post model.
class Post < ApplicationRecord
has_many :calendar_posts
has_many :calendars, through: :calendar_posts
has_many :platform_posts
has_many :platforms, through: :platform_posts
belongs_to :user
def platform_attributes=(platform_attributes)
if platform_attributes['platform_ids']
platform_attributes.platform_ids.each do |id|
platform = Platform.find(id: id)
self.platforms << platform
end
end
if platform_attributes['name'] != ""
platform = Platform.find_or_create_by(name: platform_attributes['name'])
self.platforms << platform
end
end
thoughts? why are they not saving to more than one calendar or more than one platform if they choose to have more than one?
Here is the updated code... and more of what I know about these changes and what is happening.
My submit button is not working for some odd reason on my form, so I'm trying to get the params submitted but it won't even route to give me params even if I raise them, nothing is happening.
On the form you can choose checkboxes or add in a platform. If you add in a platform it creates that one but it does not also save the other ones you selected. If you go to edit the post, and click submit with changes, no page loads at all and nothing is happening in log. It's just idle.
<%= f.fields_for :platform_attributes do |platform| %>
assumes you are creating one platform... it says "these are the fields for this platform"
but platform_ids is intended to be a selection of a set of platforms... and probably should be outside of the fields_for section (which should only surround the name field).
try something like the following:
<div class="form-field">
<%= f.label :platform_ids, "Social Platforms"%>
<%= f.collection_check_boxes :platform_ids, Platform.all, :id, :name %> <br> <br>
</div>
<div>
<%= f.fields_for :platform_attributes do |platform| %>
<h4> Or Create a new platform: </h4>
<%= platform.label :name, 'New Platform'%>
<%= platform.text_field :name%> <br> <br>
<% end %>
<%# end fields_for %>
</div>
Also you'll need to update permit/require appropriately eg
def post_params
params.require(:post).permit(:title, :content, :link, :finalized, :picture, :user_id, :platform_ids, :platform_attributes => [:name])
end
Note: not tested - bugs are left as an exercise for the reader ;)
I have a many_to_many association between Categories and Articles, using has_and_belongs_to_many in a Rails 4 app:
Here are the corresponding migration and classes:
class CategoriesArticles < ActiveRecord::Migration
def change
create_table :categories_articles, id: false do |t|
t.belongs_to :category, index: true
t.belongs_to :article, index: true
end
add_index :categories_articles, [:category_id, :article_id]
end
end
class Category < ActiveRecord::Base
has_and_belongs_to_many :articles
end
class Article < ActiveRecord::Base
has_and_belongs_to_many :categories
end
When a user creates a new article, I simply want to give him or her the option to select categories that he/she wants to associate with the new article. I want the user to be able to select these categories with checkboxes.
Here's the ArticlesController:
class ArticlesController < ApplicationController
before_action :set_article, only: [:show, :edit, :update, :destroy]
before_action :authenticate_user!, only: [:new, :create, :edit, :destroy, :update]
before_action :verify_own_article, only: [:destroy]
respond_to :html
def index
if user_signed_in?
#articles = current_user.article_feed
# TODO: if there are too few articles on a user's feed, we want to display more articles
else
#articles = Article.all
end
#articles = #articles.page(params[:page] || 1).per(12)
respond_with(#articles)
end
def show
respond_with(#article)
end
def new
#categories = Category.all
#article = Article.new
respond_with(#article)
end
def edit
if current_user.articles.find_by_id(params[:id]).nil?
flash[:notice] = "You do not have permission to edit this article."
redirect_to #article
end
end
def create
# Creates article object with current_user_id, initial_comment, and URL
#article = current_user.articles.build(article_params)
# Uses Pismo (gem) to grab title, content, photo of URL
#article.populate_url_fields
if #article.save
flash[:success] = "Article created!"
# Might need to change the location of this redirect
redirect_to root_url
else
flash[:notice] = "Invalid article."
redirect_to new_article_path
end
end
def update
#article.update(article_params)
flash[:notice] = "Article successfully updated."
respond_with(#article)
end
def destroy
if #article
#article.destroy
flash[:notice] = "Article successfully destroyed."
else
flash[:notice] = "You do not have permission to delete this article."
end
# TODO: change this to another redirect location
redirect_to root_path
end
private
def set_article
#article = Article.find(params[:id])
end
def article_params
params.require(:article).permit(:url, :title, :datetime, :content, :photo, :initial_comment)
end
# Ensure that a signed in user can only delete articles that they have posted
def verify_own_article
#article = current_user.articles.find_by_id(params[:id])
end
end
Here's the article new.html.erb view:
<h1>New article</h1>
<%= render 'form' %>
<%= link_to 'Back', articles_path %>
... and the form partial:
<%= form_for(#article) do |f| %>
<% if #article.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(#article.errors.count, "error") %> prohibited this article from being saved:</h2>
<ul>
<% #article.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :url %><br>
<%= f.text_field :url %>
</div>
<div class="field">
<%= f.label :initial_comment %><br>
<%= f.text_field :initial_comment %>
</div>
<% #categories.each do |t| %>
<div class="field">
<%= f.label t.name %>
<%= f.check_box "categories[#{t.id}]" %>
<br />
</div>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
However, this is erroring for me, specifically the lines:
<% #categories.each do |t| %>
<div class="field">
<%= f.label t.name %>
<%= f.check_box "categories[#{t.id}]" %>
<br />
</div>
<% end %>
Specifically, it's telling me:
undefined method 'categories[1]' for #<Article:0x007f401193d520>. How do I fix this? Thanks.
I note your join table is named
create_table :categories_articles, id: false do |t|
The name needs to be in alphabetical order.
create_table :articles_categories, id: false do |t|
My pick is that Rails cannot find your join table. This means that the form when it calls #article.categories cannot find what it needs. The same will happen if you call #category.articles (ie: being able to find the join table is not related to the arbitrary order of the objects).
See my answer to this question
I was following this tutorial http://www.sitepoint.com/nested-comments-rails/ to implement nested comments for an image board. It worked fine until I made "comments" belong to "boards" and then had to nest my routes.
Here are my routes:
Rails.application.routes.draw do
root "boards#index"
devise_for :users do
get '/users/sign_out' => 'devise/sessions#destroy'
end
resources :boards do
resources :comments
get '/comments/new/(:parent_id)', to: 'comments#new', as: :new_comment
get '/comments/(:parent_id)', to: 'comments#destroy', as: :delete_comment
get '/comments/edit/(:parent_id)', to: 'comments#edit', as: :edit_comment
end
end
Here is my form:
<%= form_for [#board, #comment] do |f| %>
<% if #comment.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(#comment.errors.count, "error") %> prohibited this comment from being saved:</h2>
<ul>
<% #comment.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<%= f.hidden_field :parent_id %>
<div class="form-group">
<% if #comment.parent_id == nil %>
<%= f.label :title %>
<%= f.text_field :title, class: 'form-control' %>
<% else %>
<% nil %>
<% end %>
</div>
<div class="form-group">
<%= f.radio_button(:user_id, current_user.id) %>
<%= f.label(:user_id, "I want to post as myself") %>
<%= f.radio_button(:user_id, nil) %>
<%= f.label(:user_id, "I want to post anonymously") %>
</div>
<div class="form-group">
<%= f.label :content %>
<%= f.text_area :content, class: 'form-control', required: true %>
</div>
<div class="form-group">
<%= f.label :image %>
<%= f.file_field :image %>
</div>
<%= f.submit class: 'btn btn-primary' %>
<% end %>
And here is my controller:
class CommentsController < ApplicationController
def index
#comments = Comment.hash_tree
end
def new
#comment = Comment.new(parent_id: params[:parent_id])
end
def edit
#comment = Comment.find(params[:parent_id])
end
def create
if params[:comment][:parent_id].to_i > 0
parent = Comment.find_by_id(params[:comment].delete(:parent_id))
#comment = parent.children.build(comment_params)
else
#comment = Comment.new(comment_params)
end
if #comment.save
redirect_to root_url
else
render 'new'
end
end
def update
#comment = Comment.find(params[:id])
if #comment.update(comment_params)
redirect_to #comment
else
render 'edit'
end
end
def make_parent
#comment.parent_id = nil
#comment.save
end
def destroy
#comment = Comment.find(params[:parent_id])
#comment.destroy
respond_to do |format|
format.html { redirect_to comments_url }
end
authorize! :destroy, #comment
end
private
def comment_params
params.require(:comment).permit(:title, :content, :user_id, :image)
end
end
I've tried setting a custom route in the form, this gets the form to at least appear, however when you hit the submit button it returns 'No route matches [POST] "/boards/1/comments/new"'. If I got to the controller and then change the corresponding "get" to a "post" then it causes the form to just reappear after pressing submit and nothing is added to the database. I've also experimented with shallow nesting my routes as per my instructors advice but this didn't work.
Your association between boards and comments must be:
board.rb
has_many :comments
comment.rb
belongs_to :user
routes.rb
resources :boards do
resources :comments, only: [:new, :edit, :destroy]
end
this will create a route
new_board_comment GET /boards/:board_id/comments/new(.:format) comments#new
edit_board_comment GET /boards/:board_id/comments/:id/edit(.:format) comments#edit
board_comment DELETE /boards/:board_id/comments/:id(.:format) comments#destroy