Hello I have a simple rails app that has two models a goal and a task
The goal has many tasks, and a task belongs to a goal.
For some reason, probably a rookie error, I cannot get the form to the task form to render with simple form.
Models
Goal
class Goal < ApplicationRecord
has_many :tasks
end
Task
class Task < ApplicationRecord
belongs_to :goal
end
Controllers
Goals
class GoalsController < ApplicationController
before_action :set_goal, only: [:show]
def show
end
private
def set_goal
#goal = Goal.find(params[:id])
end
end
View views/goals/show
<div class="row">
<%= #goal.title %>
<div class="row">
<ul>
<% #goal.tasks.each do |task| %>
<li><%= task.name %></li>
<% end %>
</ul>
<%= render partial: 'tasks/form', locals: {comment: #goal.tasks.new} %>
</div>
Form views/tasks/_form
<%= simple_form_for([#goal, #task]) do |f| %>
<div class="form-inputs">
<%= f.input :name %>
<%= f.input :description %>
</div>
<div class="form-actions">
<%= f.button :submit %>
</div>
<% end %>
I get the error NoMethodError in Goals#show
so obviously I need to add the #task to my goals show.... but how
so I added to my goals show method
#task = Task.find_or_create_by(:task_id)
then i get the error
Unsupported argument type: task_id (Symbol)
so I added the following to my goals_controller
def show
#task = Goal.task.find_or_create_by(:task_id)
end
but then I get
NoMethodError in GoalsController#show
undefined method `task' for #<Class:0x00007ff8c79b0920> Did you mean? take
Routes
Rails.application.routes.draw do
resources :tasks
resources :goals
end
As per Jagdeep's comment above adding Try adding #task = #goal.tasks.build in goals_controller#show fixed this issue.
hope this helps
Related
I have a rails question. I'm building a site where posts have likes, both posts and likes are their own model. A user can only like a post once, and once they like it the like button becomes an "unlike" button, that deletes the like.
I'm trying to create an experience in which the user can like, or unlike a post - and will not be redirected, but the like will update. With my limited rails knowledge, this isn't an easy task. Can anyone point me in the right direction?
Here is my /likes/_likes.html.erb template partial with the like/unlike button:
<% liked = #post.likes.find { |like| like.user_id == current_user.id} %>
<div class="likes">
<% if liked %>
<%= button_to 'Unlike', post_like_path(#post, liked), method: :delete %>
<% else %>
<%= button_to 'Like', post_likes_path(#post), method: :post %>
<% end %>
<%= #post.likes.count %><%= (#post.likes.count) == 1 ? 'Like' : 'Likes'%>
</div>
Here is my Like controller:
class LikesController < ApplicationController
before_action :find_post
before_action :find_like, only: [:destroy]
def create
if (!already_liked?)
#post.likes.create(user_id: current_user.id)
end
end
def destroy
if (already_liked?)
#like.destroy
end
end
private
def already_liked?
Like.where(user_id: current_user.id, post_id:
params[:post_id]).exists?
end
def find_post
#post = Post.find(params[:post_id])
end
def find_like
#like = #post.likes.find(params[:id])
end
end
Here is one of the views in which the _likes partial shows up (although the issue persists everywhere it appears):
<div class="post-display">
<% if #post.title %>
<h1><%= #post.title %></h1>
<% end %>
<% if #post.user %>
Post by <%= #post.user.email %>
<% end %>
<% if #post.price %>
<p>$<%= sprintf "%.2f", #post.price %></p>
<% end %>
<% if #post.description %>
<p><%= #post.description %></p>
<% end %>
<% if #post.image.present? %>
<%= image_tag #post.image.variant(:small) %>
<% end %>
<%= render 'likes/likes' %>
</div>
<% if current_user == #post.user %>
<%= link_to "Edit", edit_post_path(#post) %>
<%= button_to "Delete", #post, method: :delete %>
<% end %>
<% if #post.comments.count > 0 %>
<div class="post-comments">
<h2 class="post-comments-headline">Comments</h2>
<%= render #post.comments %>
</div>
<% end %>
<h2>Add a comment:</h2>
<%= render 'comments/form' %>
If you don't have an answer to my question, but have an idea on how to improve my code - let me know either way! I'm trying to learn here...
Thank you,
Jill
Since you're using rails 7, rendering turbo_stream in response to "like" and "unlike" buttons will update the page without refreshing.
# config/routes.rb
resources :posts do
# NOTE: i've used singular `resource`, since there is no need to have `id`
# for the like.
resource :like, only: [:destroy, :create]
end
https://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/Resources.html#method-i-resource
# app/models/post.rb
class Post < ApplicationRecord
has_many :likes
def liked_by? user
likes.where(user: user).exists?
end
end
# app/models/like.rb
class Like < ApplicationRecord
belongs_to :post
belongs_to :user
# NOTE: avoid double likes.
validates_uniqueness_of :user, scope: :post, message: "already liked this post"
# TODO: create a unique index migration, to really make sure no double likes.
# `add_index :likes, [:post_id, :user_id], unique: true`
end
I've simplified LikesController a bit. No need for before_action filters:
# app/controllers/likes_controller.rb
class LikesController < ApplicationController
# POST /posts/:post_id/like
def create
# NOTE: uniqueness validation in `Like` model will prevent creating dup likes.
post.likes.create(user: current_user)
# you can access `like` error if you want to show it:
# like = post.likes.create(user: current_user)
# like.errors.full_messages
# NOTE: that's it, now we render `likes` partial inside a `turbo_stream`
render turbo_stream: turbo_stream.replace(
helpers.dom_id(post, :like), # this is the target div `id` that will be replaced
partial: "posts/likes", # with `likes` partial.
locals: { post: post }
)
end
# DELETE /posts/:post_id/like
def destroy
# NOTE: this will work regardless if there are any likes or not.
post.likes.where(user: current_user).destroy_all
# NOTE: alternatively, we can render the same `turbo_stream` as above
# in a template `likes/likes.turbo_stream.erb`:
render :likes
end
private
def post
#post ||= Post.find params[:post_id]
end
end
<!-- app/views/posts/_likes.html.erb -->
<!-- `dom_id` helps us generate a uniq id: "like_post_1" -->
<div id="<%= dom_id(post, :like) %>">
<!-- yes, there is a rails helper for this -->
<%= pluralize post.likes.count, "like" %>
<% if post.liked_by? current_user %>
<%= button_to "Unlike", post_like_path(post), method: :delete %>
<% else %>
<%= button_to "Like", post_like_path(post) %>
<% end %>
</div>
This turbo_stream is the same as in create action:
<!-- app/views/likes/likes.turbo_stream.erb -->
<%= turbo_stream.replace dom_id(#post, :like) do %>
<%= render "posts/likes", post: #post %>
<% end %>
https://turbo.hotwired.dev/handbook/streams
Try this
views file where likes partial render
<div id='post_likes'>
<%= render 'likes/likes' %>
</div>
/likes/_likes.html.erb
<div class="likes">
<% if liked %>
<%= button_to 'Unlike', post_like_path(#post, liked), method: :delete, remote: true %>
<% else %>
<%= button_to 'Like', post_likes_path(#post), method: :post, remote: true %>
<% end %>
<%= #post.likes.count %><%= pluralize(#post.likes.count, 'Like') %>
</div>
views/likes/create.js.erb
$('#post_likes').html('<%= render 'likes/likes' %>');
views/likes/destroy.js.erb
$('#post_likes').html('<%= render 'likes/likes' %>');
This app has the following models:
Farm (has_many :crops)
Crop (belongs_to :farm, has_many :issues)
Issue (belongs_to :crop)
Here are the routes:
resources :farms do
resources :crops do
resources :issues
end
end
I want a user to be able to create a new "issue" from the Farm#show page that lists all the farm's crops. Here is the form that is causing the error on the Farm#show page:
undefined method `crop_issues_path' for #<#:0x007fa814a3cc30>
#from the show action on the controller:
##farm = Farm.find(params[:id])
##crops = #farm.crops
<% #crops.each do |crop| %>
<%= crop.id %>
<%= form_for([crop, crop.issues.build]) do |f| %>
<%= f.select(:issue_type, options_for_select([['mold'], ['pests'], ['dehydration'], ['other']])) %>
<%= f.text_area :notes %><br>
<%= f.submit "New Issue", :class => "button" %>
<% end %>
<% end %>
My create action on issues controller:
def create
#crop = Crop.find(params[:crop_id])
#issues = #crop.issues.create(params[:issue].permit(:issue_type, :notes, :crop_id))
redirect_to :back
end
I have used nearly identical code when the crops and issues were not nested under farms, and it works. I believe the issue is because of the nesting, but cannot figure out a solution.
I think your problem is with the object you're binging the form to. It should be #farm, as you're in the #farms show action.
I modified it to this:
<% #crops.each do |crop| %>
<%= crop.id %>
<%= form_for([#farm, crop, crop.issues.build]) do |f| %>
<%= f.text_area :notes %><br>
<%= f.submit "New Issue", :class => "button" %>
<% end %>
<% end %>
with my controller like this:
class FarmsController < ApplicationController
def index
end
def show
#farm = Farm.find_by_id(params[:id])
#crops = #farm.try(:crops)
end
end
First of all I have this:
https://polar-scrubland-30279.herokuapp.com/ - my project which is deployed on heroku (Captain Obvious)
I've got projects and todos inside them.
For this moment I show all projects using this way:
------index.html.erb------
<%= render #projects %>
------_project.html.erb-----
<div class="project">
<div class="project-header">
<h2><%= project.title %></h2>
</div>
<div class="project-todos">
<% project.todos.all.each do |todo| %>
<p><%= check_box('tag', todo.__id__, {class: 'icheckbox_square-blue', checked: todo.isCompleted}) %> <%= content_tag :todotext, todo.text %></p>
<% end %>
</div>
</div>
And as you understand it doesn't allow me to change my todo's status when checkbox is checked. So that's why I need a form that will allow me to track all the checkboxes. Also I wanna make text-decoration: line-through when checkbox is pressed, but don't get how to.
Is there a way to creat a form which will satisfy my needs? Please can you help me, Any information will be appreciated.
ADDITIONAL INFORAMTION:
GitHub - https://github.com/NanoBreaker/taskmanager
project.rb
class Project < ActiveRecord::Base
has_many :todos
end
todo.rb
class Todo < ActiveRecord::Base
belongs_to :project
end
Lets start with the models:
class Project < ApplicationRecord
has_many :todos
accepts_nested_attributes_for :todos
end
class Todo < ApplicationRecord
belongs_to :project
end
accepts_nested_attributes_for lets you create or modify several nested Todo records at once when creating or updating a Project.
# will update 2 todos at once
#project.update(
todos_attributes: [ { id: 1, isComplete: true }, { id: 2, isComplete: false }]
)
We can use fields_for to create nested inputs for todos:
<%= f.form_for(#project) do |f| %>
<%= f.fields_for(:todos) do |tf| %>
<%= tf.check_box :isCompleted %>
<% end %>
<% end %>
This generates fields for todos nested under the key todos_attributes. We can whitelist them by using a hash key containing a array of permitted attributes.
class ProjectsController < ApplicationController
before_action :set_project, only: [:show, :edit, :update, :destroy]
def new
#project = Project.new
# this seeds the project with 3 empty tasks
# otherwise we don't have any inputs.
3.times { #project.todos.new }
end
def create
#project = Project.new(project_params)
if #project.save
# ...
else
# ...
end
end
def update
if #project.update(project_params)
# ...
else
# ...
end
end
private
def set_project
#project = Project.find(params[:id])
end
def project_params
params.require(:project)
.permit(:foo, :bar,
todos_attributes: [:isCompleted, :text]
)
end
end
You can create a form for each project by creating a partial which uses a local instead of an instance variable:
# app/views/projects/_form.html.erb
<%= f.form_for(local_assigns[:project] || #project) do |f| %>
<%= f.fields_for(:todos) do |tf| %>
<%= tf.check_box :isCompleted %>
<% end %>
<% end %>
# app/views/projects/index.html.erb
<% #projects.each do |project| %>
<%= render partial: 'projects/form', project: project %>
<% end %>
You can reuse the same partial for the other views as well:
# app/views/projects/new.html.erb
<%= render partial: 'projects/form' %>
# app/views/projects/edit.html.erb
<%= render partial: 'projects/form' %>
I have a nested relationship where dashboard has many rewards, and I am trying to add a fields_for to the page in order to edit the rewards. Unfortunately, it doesn't seem to be working and I don't know why.
Here's what I have.
Dashboard model:
class Dashboard < ActiveRecord::Base
belongs_to :manager
has_many :rewards
accepts_nested_attributes_for :rewards, allow_destroy: true
end
Rewards model:
class Reward < ActiveRecord::Base
belongs_to :dashboard
end
Dashboard controller:
class DashboardsController < ApplicationController
before_action :authenticate_manager!
# Requires user to be signed in
def index
#dashboards = Dashboard.all
end
def new
#dashboard = Dashboard.new
end
def edit
#dashboard = Dashboard.find(params[:id])
end
def create
#dashboard = Dashboard.new(dashboard_params)
#dashboard.save
if #dashboard.save
redirect_to dashboard_path(#dashboard)
else
render :action => new
end
end
def update
#dashboard = Dashboard.find(params[:id])
if #dashboard.update(dashboard_params)
redirect_to :action => :show
else
render 'edit'
end
end
def show
#dashboard = Dashboard.find(params[:id])
end
def destroy
#dashboard = Dashboard.find_by_id(params[:id])
if #dashboard.destroy
redirect_to dashboards_path
end
end
private
def dashboard_params
args = params.require(:dashboard).permit(:title, :description, :rewards, {rewards_attributes: [ :id, :title, :referralAmount, :dashboardid, :selected, :_destroy] } )
args
end
end
Form in dashboards view:
<%= form_for :dashboard, url: dashboard_path(#dashboard), method: :patch do |f| %>
<% if #dashboard.errors.any? %>
<div id="error_explanation">
<h2>
<%= pluralize(#dashboard.errors.count, "error") %> prohibited
this dashboard from being saved:
</h2>
<ul>
<% #dashboard.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<p>
<%= f.label :title %><br>
<%= f.text_field :title %>
</p>
<p>
<%= f.label :description %><br>
<%= f.text_field :description %>
</p>
<%= f.fields_for :rewards do |reward| %>
<%= reward.label :title %><br>
<%= reward.text_field :title %>
<%= reward.check_box :_destroy %>
<%= reward.label :_destroy, "Remove reward" %>
<% end %>
<p>
<%= f.submit %>
</p>
<% end %>
I went ahead and manually added rewards to the database through the rails console and it worked beautifully, but they are not showing up on the page. They will show up if I iterate through them like so
<% if #dashboard.rewards.any? %>
<ul>
<% #dashboard.rewards.each do |reward| %>
<li><%= reward.title %></li>
<li><%= reward.referralAmount %></li>
<% end %>
</ul>
<% else %>
<p>no rewards</p>
<% end %>
However the fields_for does not display the rewards or their content and resultingly allow one to edit them.
Let me know if you need further information/code.
Try to modify your:
View:
<% if #dashboard.errors.any? %>
<div id="error_explanation">
<h2>
<%= pluralize(#dashboard.errors.count, "error") %> prohibited
this dashboard from being saved:
</h2>
<ul>
<% #dashboard.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<%= form_for #dashboard, url: dashboard_path(#dashboard) do |f| %>
........
<% end %>
Controller (has_many relationship):
def new
#dashboard = Dashboard.new
#dashboard.rewards.build
end
private
def dashboard_params
params.require(:dashboard).permit(:title, :description,
rewards_attributes: [
:id,
:title,
:referralAmount,
:dashboardid,
:selected,
:_destroy
])
end
You don't have to set the method: patch if form.
Once you got in edit page, Rails will use the update action in controller when form submission.
To check it, run rake routes,
you will see somsthing like this:
PATCH /dashboards/:id(.:format) dashboards#update
PUT /dashboards/:id(.:format) dashboards#update
In controller you need to give build
def new
#dashboard = Dashboard.new
#dashboard.rewards.build
end
"build" is just create a new object in memory so that the view can take this object and display something, especially for a form.
Hope it helps for you
You should build object before nested form. You can add whatever you want that object.
Try it in controller;
def new
#dashboard = Dashboard.new
3.times do
#dashboard.build_reward
end
end
Try setting an "#rewards" instance variable in your dashboards edit method (where #rewards = #dashboard.rewards). Then replace :rewards with #rewards.
Edit:
I believe my initial answer is inapproriate for your exact question (while it would be helpful on say the page to show a specific dashboard and its rewards). The answers above are on the right track re:
refining your params method per #aldrien.h;
Adding #santosh dadi's suggestion of
#dashboard.rewards.build
(assuming you only want one rewards fields on a form for "new")
Finally though, to avoid making fake information for a new rewards form, adding to the top of your Dashboards model:
accepts_nested_attributes_for :rewards, reject_if: lambda {|attributes| attributes['title'].blank?}
http://guides.rubyonrails.org/form_helpers.html#nested-forms
I have been trying to fix an error associated with using the Ancestry gem for comments on my app for Rails 4. I used railscast episode 262 as a guide. However, unlike the episode, my comments model is a nested resource inside another model.Before I go further, I will supply the necessary code for reference. If you like to read the error right away, it is mentioned right after all the code snippets.
The Relevant Models:
class Comment < ActiveRecord::Base
has_ancestry
belongs_to :user
belongs_to :scoreboard
end
class Scoreboard < ActiveRecord::Base
#scoreboard model is like an article page on which users can post comments
belongs_to :user
has_many :teams, dependent: :destroy
has_many :comments, dependent: :destroy
end
Relevant code in the route file:
resources :scoreboards do
resources :comments
resources :teams, only: [:edit, :create, :destroy, :update]
end
The Scoreboards Controller Method for the page on which one can post comments:
def show
#scoreboard = Scoreboard.find_by_id(params[:id])
#team = #scoreboard.teams.build
#comment = #scoreboard.comments.new
end
The Comments Controller:
class CommentsController < ApplicationController
def new
#scoreboard = Scoreboard.find(params[:scoreboard_id])
#comment = #scoreboard.comments.new(:parent_id => params[:parent_id])
end
def create
#scoreboard = Scoreboard.find(params[:scoreboard_id])
#comment = #scoreboard.comments.new comment_params
if #comment.save
redirect_to scoreboard_url(#comment.scoreboard_id)
else
render 'new'
end
end
private
def comment_params
params.require(:comment).permit(:body, :parent_id).merge(user_id: current_user.id)
end
end
I will include the migration for the ancestry gem if any mistakes were made on that :
class AddAncestryToComments < ActiveRecord::Migration
def change
add_column :comments, :ancestry, :string
add_index :comments, :ancestry
end
end
The following code shows the view code:
Scoreboard#show View which is giving me the error in the last line:
<div class= "comment-section">
<%= form_for [#scoreboard, #comment] do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<%= f.text_area :body, class: "comment-field" %>
<%= f.hidden_field :parent_id %> #is it needed to include this here? because this form is for new comments not replies
<%= f.submit "Join the discussion...", class: " comment-button btn btn-primary" %>
<% end %>
<%= nested_comments #scoreboard.comments.reject(&:new_record?).arrange(:order => :created_at) %>
</div>
The (comments partial)_comment.html.erb View:
<div class=" comment-div">
<p> Posted by <%= link_to "#{comment.user.name}", comment.user %>
<%= time_ago_in_words(comment.created_at) %> ago
</p>
<div class="comment-body">
<%= comment.body %>
<%= link_to "Reply", new_scoreboard_comment_path(#scoreboard, comment, :parent_id => comment) %>
</div>
</div>
The helper method to render comments:
def nested_comments(comments)
comments.map do |comment, sub_comment| #the comments.map also gives me an error if I choose to render the comments without the .arrange ancestry method
render(comment) + content_tag(:div, nested_comments(sub_comment), class: "nested_messages")
end.join.html_safe
end
The new.html.erb for Comments which one is redirected to for the replies form submission:
<%= form_for [#scoreboard, #comment] do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<%= f.text_area :body, class: "comment-field" %>
<%= f.hidden_field :parent_id %>
<%= f.submit "Join the discussion...", class: " comment-button btn btn-primary" %>
<% end %>
Upon creating a scoreboard, I am redirected to the show page, where i get the following error:
undefined method `arrange' for []:Array
Even though the array of comments is empty, I get the same error if it wasnt. I have tried .subtree.arrange but that gives me the same error. Also, the ancestry documentation said that .arrange works on scoped classes only. I don't know what that means. I would appreciate some help on making the page work so the comments show properly ordered with the replies after their parent comments. If this is the wrong approach for threaded comments(replies and all), I would appreciate some guidance on what to research next.
.reject(&:new_record?) this will return an array. The error sounds like arrange is a scope on ActiveRecord. So move the reject to the end and it should work.
#scoreboard.comments.arrange(:order => :created_at).reject(&:new_record?)
In regards your comment nesting, I have implemented this before, and found the Railscasts recommendation of a helper to be extremely weak.
Passing parent_id to a comment
Instead, you're better using a partial which becomes recursive depending on the number of children each comment has:
#app/views/scoreboards/show.html.erb
<%= render #comments %>
#app/views/scoreboards/_comment.html.erb
<%= link_to comment.title, comment_path(comment) %>
<div class="nested">
<%= render comment.children if comment.has_children? %>
</div>