polymorphic_path or url_for how customize controller name - ruby-on-rails

I have a situation where I have #challenge and #idea instances from Challenge and Idea model responsively.
I have one partial which is going to used in many controllers, so based on controllers I want to generate path.
e.g
So when I am in IdeasController polymorphic_path([#challenge, #idea]) will generate "/challenges/12/ideas/45"
but if I am in any other controller suppose RefinementsController, I want generate path as "challenges/12/refinements/45"
How can I generate it using polymorphic_path or url_for
code:
app/controllers/ideas_controller.rb
class IdeasController < ApplicationController
before_action :authenticate_user!
load_and_authorize_resource :challenge
load_and_authorize_resource :idea, through: :challenge
def index
end
def show
end
end
app/controllers/refinements_controller.rb
class RefinementsController < ApplicationController
before_action :authenticate_user!
load_and_authorize_resource :challenge, parent: true
load_and_authorize_resource :idea, through: :challenge, parent: false
def index
#ideas = #ideas.refined
render 'ideas/index'
end
def show
render 'ideas/show'
end
end
app/views/ideas/index.html.erb
<div class="ideas">
<% #ideas.each do |idea| %>
<div class="idea">
<div class="title">
<%= link_to idea.title, polymorphic_path([#challenge, idea]) %>
</div>
</div>
<% end %>
</div>
app/views/ideas/show.html.erb
<div class="idea">
<div class="title">
<%= link_to idea.title, polymorphic_path([#challenge, #idea]) %>
</div>
<div class="descriptio">
<%= #idea.description %>
</div>
</div>

Partial
Without having your partial code here, I'm going to make an assumption that you are calling polymorphic_path inside your partial, hence wanting to change the path as per the controller it's loaded from
If you're calling a partial, you have to remember its been designed to be an independent, modular element of your application, and so including #instance variables in your partial will likely be an antipattern
What I would recommend is to populate your partial with local variables, allowing you to populate those accordingly:
#app/controllers/ideas_controller.rb
Class IdeasController < ApplicationController
def show
#challenge = Challenge.find params[:challenge_id] if params[:challenge_id].present?
#idea = Idea.find params[:id]
end
end
#app/views/ideas/show.html.erb
<%= render "shared/partial", locals: {challenge: #challenge, child: #idea} %>
#app/views/shared/_partial.html.erb
<%= link_to "Test", polymorphic_path([challenge, child]) %>
--
Update
Thanks for your code
You'll be totally better be using the following:
#app/views/ideas/index.html.erb
<%= render "ideas/item", collection: #items, as: :item, locals: {challenge: #challenge} %>
#app/views/ideas/show.html.erb
<%= render "ideas/item", object: #item, as: :item, locals: {challenge: #challenge} %>
This will allow you to call:
#app/views/ideas/_idea.html.erb
<div class="idea">
<div class="title">
<%= link_to idea.title, polymorphic_path([challenge, idea]) %>
</div>
<% if action_name =="show" %>
<div class="descriptio">
<%= idea.description %>
</div>
<% end %>
</div>

Although it's not a good practice but if it's only a minor change and rest of your partial is same in all cases then what you do is use some generic variable in your partial. In your case your polymorphic url depends on the initialized value of second variable i.e #idea(as first part challenges/12/ is same) so you can do something like this:
polymorphic_path([#challenge,#something_generic])
and now depending on your controllers action you can change #something_generic to make proper url for your form
class IdeasController < ApplicationController
def new
#challenge = Challenge.find(params[:challenge_id])
#something_generic = #challenge.ideas.build
end
end
class RefinementsController < ApplicationController
def new
#challenge = Challenge.find(params[:challenge_id])
#something_generic = #challenge.refinements.build
end
end
Update:
Change your code to this:
class IdeasController < ApplicationController
before_action :authenticate_user!
load_and_authorize_resource :challenge
load_and_authorize_resource :idea, through: :challenge
def index
end
def show
#something_generic = Idea.find(params[:id])
end
end
class RefinementsController < ApplicationController
before_action :authenticate_user!
load_and_authorize_resource :challenge, parent: true
load_and_authorize_resource :idea, through: :challenge, parent: false
def index
#something_generic = Refinement.find(params[:id])
render 'ideas/index'
end
def show
#something_generic = Refinement.find(params[:id])
render 'ideas/show'
end
end
Since you are using cancancan and have proper filters so #idea and #challenge will be defined automatically in your actions.
For index.html i think you'll be better of using separate templates but if you still want to use a single template then you can do something like this:
app/views/ideas/index.html.erb
<div class="ideas">
<% #ideas.each do |idea| %>
<div class="idea">
<div class="title">
<% if controller_name == "ideas" %>
<%= link_to idea.title, polymorphic_path([#challenge, idea]) %>
<% else %>
<%= link_to idea.title, polymorphic_path([#challenge, #something_generic]) %>
<% end %>
</div>
</div>
<% end %>
</div>
and show.html.erb
<div class="idea">
<div class="title">
<%= link_to #idea.title, polymorphic_path([#challenge, #something_generic]) %>
</div>
<div class="description">
<%= #idea.description %>
</div>
</div>

Related

Delete/Create in-place (without redirect)

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' %>');

Simple form for not working on belongs to

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

Ruby on Rails: how to make a form for associated models (nested)

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' %>

Ruby on Rails: updating model when checkbox is changed

I got projects and todos inside them, todos have checkbox & text, so when I change the checkbox the whole model must be updated without any buttons.
Here is my project on heroku: https://polar-scrubland-30279.herokuapp.com/
index.html.erb ( I know that I'm not the best code speller)
<% #projects.each do |project| %>
<div class="col-md-4">
<div class="project">
<%= form_for(local_assigns[:project] || project) do |f| %>
<div class="project-header">
<h2> <%= f.object.title %> </h2>
</div>
<div class="project-todos">
<%= f.fields_for(:todos) do |tf| %>
<div class="todo">
<div class="todo--checkbox">
<%= tf.check_box :isCompleted %>
</div>
<div class="todo--text">
<%= tf.object.text %>
</div>
</div>
<% end %>
</div>
<% end %>
</div>
</div>
<% end %>
This is my ProjectsController:
class ProjectsController < ApplicationController
def index
#projects = Project.all
end
def show
#project = Project.find(params[:id])
end
def new
#project = Project.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
project.rb
class Project < ActiveRecord::Base
has_many :todos
accepts_nested_attributes_for :todos
end
todo.rb
class Todo < ActiveRecord::Base
belongs_to :project
end
The question is: how do I connect my form with my controller, and change todo's boolean variable when checkbox is checked/uncheked. I've tried some variants, but there is need a refresh button, which I don't wanna have.
GitHub - https://github.com/NanoBreaker/taskmanager
You have to use
jQuery checkbox change and click event
to catch checkbox change event,
further you can use ajax request
http://api.jquery.com/jquery.ajax/ on the change event,
in the ajax request use path to update method in Projects controller.
Note that you have to put the path in file config/routes.rb,
something like this:
post 'url' => 'projects#update'

NameError in Discussions#index

Im trying to write an ruby app that lets you post discussions and comment on posts. The issue is that I cant get the comments- here called microposts, to show up below the discussions. Right now, im getting the error NameError in Discussions#index: undefined local variable or method `discussion' for #<#:0x0000010563a1e0>. Any ideas?
the micropost form
<% #micropost = Micropost.new %>
<%= form_for(#micropost) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<div class="field">
<%= f.text_area :content, placeholder: "Compose new micropost..." %>
</div>
<%= f.hidden_field :discussion_id, discussion.id%>
<%= f.submit "Post", class: "btn btn-large btn-primary" %>
<% end %>
micropost controller
class MicropostsController < ApplicationController
before_filter :signed_in_user, only: [:create, :destroy]
def index
end
def create
#discussion = current_user.discussions.new
#micropost = current_user.microposts.build(params[:micropost])
if #micropost.save
flash[:success] = "Posted!"
redirect_to root_url
else
render 'static_pages/home'
end
end
def destroy
end
end
discussion controller
class DiscussionsController < ApplicationController
before_filter :signed_in_user, only: [:index, :edit, :update]
def show
#user = User.find(params[:id])
#discussions = #user.discussion.paginate(page: params[:page])
#microposts = #user.micropost.paginate(page: params[:page])
end
def index
#discussions = Discussion.all
end
def create
#discussion = current_user.discussions.build(params[:discussion])
if #discussion.save
flash[:success] = "Discussion Started!"
redirect_to root_url
else
render 'static_pages/home'
end
end
def destroy
end
def edit
end
def update
end
def new
end
end
discussion view
<% content_for :script do %>
<%= javascript_include_tag 'hover_content' %>
<% end %>
<li>
<div class = "intro-bar"><span class = "intro"><%=discussion.intro %></span></div>
<div class = "content-bar">
<span class = "content"><%= discussion.content %></span>
<div class = "buttons">
<div class = "vote-neg"><%= link_to "Break Up", signup_path,class: "btn btn-large btn-breakup" %></div>
<div class = "vote-plus"><%= link_to "Stay Together", signup_path,class: "btn btn-large btn-staytogether" %></div>
</div>
</div>
</li>
<span class = "timestamp">
Posted <%= time_ago_in_words(discussion.created_at) %> ago.
</span>
<div class = "comments">
<% discussion.microposts.each do |micropost| %>
<li>
<div class = "post-comment"><%= micropost.content%></div>
</li>
<% end %>
</div>
<% if signed_in? %>
<div class = "row">
<aside class = "span4">
<section>
<%= render 'shared/micropost_form', :locals => {:discussion => discussion }%>
</section>
</aside>
</div>
<% end %>
thanks for the help, im sure its something really simple. Im quite new at this
In this code,
class DiscussionsController < ApplicationController
before_filter :signed_in_user, only: [:index, :edit, :update]
def show
#user = User.find(params[:id])
#discussions = #user.discussion.paginate(page: params[:page])
#microposts = #user.micropost.paginate(page: params[:page])
end
it looks like #user.discussion is your problem. Should probably be #user.discussions. Likewise with the next line, #user.microposts
I suppose you show discussion with show method. But, in show method and everywhere else in controller except create you declare #discussions.
I actually can't get, why in show method you do what suits more for index method, but anyway, Rails pass variable from controller to views if it is declared as instance (with #).
So, to define variale in views, you should have #discussion in relative controller method, and locals should also pass #discussion:
:locals => {:discussion => #discussion }

Resources