Route concern and polymorphic model: how to share controller and views? - ruby-on-rails

Given the routes:
Example::Application.routes.draw do
concern :commentable do
resources :comments
end
resources :articles, concerns: :commentable
resources :forums do
resources :forum_topics, concerns: :commentable
end
end
And the model:
class Comment < ActiveRecord::Base
belongs_to :commentable, polymorphic: true
end
When I edit or add a comment, I need to go back to the "commentable" object. I have the following issues, though:
1) The redirect_to in the comments_controller.rb would be different depending on the parent object
2) The references on the views would differ as well
= simple_form_for comment do |form|
Is there a practical way to share views and controllers for this comment resource?

In Rails 4 you can pass options to concerns. So if you do this:
# routes.rb
concern :commentable do |options|
resources :comments, options
end
resources :articles do
concerns :commentable, commentable_type: 'Article'
end
Then when you rake routes, you will see you get a route like
POST /articles/:id/comments, {commentable_type: 'Article'}
That will override anything the request tries to set to keep it secure. Then in your CommentsController:
# comments_controller.rb
class CommentsController < ApplicationController
before_filter :set_commentable, only: [:index, :create]
def create
#comment = Comment.create!(commentable: #commentable)
respond_with #comment
end
private
def set_commentable
commentable_id = params["#{params[:commentable_type].underscore}_id"]
#commentable = params[:commentable_type].constantize.find(commentable_id)
end
end
One way to test such a controller with rspec is:
require 'rails_helper'
describe CommentsController do
let(:article) { create(:article) }
[:article].each do |commentable|
it "creates comments for #{commentable.to_s.pluralize} " do
obj = send(commentable)
options = {}
options["#{commentable.to_s}_id"] = obj.id
options["commentable_type".to_sym] = commentable.to_s.camelize
options[:comment] = attributes_for(:comment)
post :create, options
expect(obj.comments).to eq [Comment.all.last]
end
end
end

You can find the parent in a before filter like this:
comments_controller.rb
before_filter: find_parent
def find_parent
params.each do |name, value|
if name =~ /(.+)_id$/
#parent = $1.classify.constantize.find(value)
end
end
end
Now you can redirect or do whatever you please depending on the parent type.
For example in a view:
= simple_form_for [#parent, comment] do |form|
Or in a controller
comments_controller.rb
redirect_to #parent # redirect to the show page of the commentable.

Related

Rails: How to redirect to a specific controller (index) depending on condition

I have a Ruby on Rails application that can generate "roles" for actors in movies; the idea is that if a user looks at a movie detail page, they can click "add role", and the same if they look at an actor detail page.
Once the role is generated, I would like to redirect back to where they came from - the movie detail page or the actor detail page... so in the controller's "create" and "update" method, the redirect_to should either be movie_path(id) or actor_path(id). How do I keep the "origin" persistent, i. e. how do I remember whether the user came from movie detail or from actor detail (and the id, respectively)?
I would setup separate nested routes and just use inheritance, mixins and partials to avoid duplication:
resources :movies do
resources :roles, module: :movies, only: :create
end
resources :actors do
resources :roles, module: :actors, only: :create
end
class RolesController < ApplicationController
before_action :set_parent
def create
#role = #parent.roles.create(role_params)
if #role.save
redirect_to #parent
else
render :new
end
end
private
# guesses the name based on the module nesting
# assumes that you are using Rails 6+
# see https://stackoverflow.com/questions/133357/how-do-you-find-the-namespace-module-name-programmatically-in-ruby-on-rails
def parent_class
module_parent.name.singularize.constantize
end
def set_parent
parent_class.find(param_key)
end
def param_key
parent_class.model_name.param_key + "_id"
end
def role_params
params.require(:role)
.permit(:foo, :bar, :baz)
end
end
module Movies
class RolesController < ::RolesController
end
end
module Actors
class RolesController < ::RolesController
end
end
# roles/_form.html.erb
<%= form_with(model: [parent, role]) do |form| %>
# ...
<% end %>

How to set a like button in rails

Hello I have an exercise app where a user should be able to Like some products.
I could find a way to display the product he liked, but I really can't figure how to create and make work the like button.
I am not using any gem, I wan't to understand how to do it from Scratch.
Here are my models:
class User < ApplicationRecord
has_many :likes
has_many :liked_products, through: :likes, source: :product
end
class Product < ApplicationRecord
has_many :likes
end
class Like < ApplicationRecord
belongs_to :user
belongs_to :product
end
In my view product show where I want the like button:
<h1><%= #product.name %></h1>
<%= link_to "Like", product_likes_path(#product), method: :put, remote: true %>
my routes:
Rails.application.routes.draw do
root to: 'visitors#index'
devise_for :users
resources :users
resources :products do
resource :likes
end
end
That's my products controller, I think things must come in here but I don't know HOW!
class ProductsController < ApplicationController
before_action :find_product, only: :show
def index
#products = Product.all
end
def show
##product.like => gives an error 404
end
private
def find_product
#product = Product.find(params[:id])
end
end
I had created a likes controller but it seems it is not useful.... So... I gave up there...
class LikesController < ApplicationController
def new
#like = Like.new(like_params)
end
def create
#like = Like.new(like_params)
end
private
def like_params
params.require(:likes).permit(:user_id, :product_id)
end
end
I would really enjoy some light on this please :)
Finally found out how to set the controller
class LikesController < ApplicationController
def create
#user = current_user.id
#product = params[:product_id]
likes = {user_id: #user, product_id: #product}
#like = Like.new(likes)
#like.save!
if #like.save
redirect_to user_path(#user)
else
redirect_to product_path
end
end
end
the buttton
<%= link_to "Like", product_likes_path(#product), method: :post %>
routes
Rails.application.routes.draw do
root to: 'products#index'
devise_for :users
resources :users
resources :users do
resources :products do
resources :likes
end
end
end
You could try something along these lines:
Routes:
Rails.application.routes.draw do
root to: 'visitors#index'
devise_for :users
resources :users do
resources :products do
resources :likes
end
end
resources :products do
resource :likes
end
end
Which will give you something like:
... other routes ...
user_product_likes GET /users/:user_id/products/:product_id/likes(.:format) likes#index
POST /users/:user_id/products/:product_id/likes(.:format) likes#create
new_user_product_like GET /users/:user_id/products/:product_id/likes/new(.:format) likes#new
edit_user_product_like GET /users/:user_id/products/:product_id/likes/:id/edit(.:format) likes#edit
user_product_like GET /users/:user_id/products/:product_id/likes/:id(.:format) likes#show
PATCH /users/:user_id/products/:product_id/likes/:id(.:format) likes#update
PUT /users/:user_id/products/:product_id/likes/:id(.:format) likes#update
DELETE /users/:user_id/products/:product_id/likes/:id(.:format) likes#destroy
... other routes ...
Then:
<%= link_to "Like", user_product_likes_path(#user, #product), method: :post, remote: true %>
And in your LikesController:
class LikesController < ApplicationController
def new
#like = Like.new(like_params)
end
def create
#like = Like.new(like_params)
if #like.save
... do something happy
else
... do something sad
end
end
private
def like_params
params.require(:likes).permit(:user_id, :product_id)
end
end
Untested, so buyer beware. You might need to fiddle with your like_params and other stuff.

Uninitialized constant Model:: Error with polymorphic relation

I created a polymorphic relation in a book reviewing app that I am writing. My app has the nested models: Piece >> Section >> Subsection >> Subsubsection, and I have created a model which belongs to all of these called KeyConcept, my intention being that each of these can have a key concept. When trying to display the "Show" view of the "Piece" model i get the following error:
NameError in Pieces#show
uninitialized constant Piece::Keyconcept
<h5>Summary: </h5><p><%= simple_format(#piece.summary) %></p>
<br/>
<% #piece.keyconcepts.each do |concept| %>
<li>
<%= link_to concept.definition, r, class: 'section_name' %>
</li>
So the routes.rb file looks like this:
resources :pieces do
resources :sections do
resources :subsections do
resources :subsubsections
end
end
resources :links
end
resources :pieces, :sections, :subsections, :subsubsections do
resources :connections, only: [:index, :new, :edit, :update, :destroy, :create]
resources :keyconcepts, only: [:index, :new, :edit, :update, :destroy, :create, :show]
end
the model.rb files look like this:
in models/concerns/conceptable.rb
module Conceptable
extend ActiveSupport::Concern
included do
has_many :keyconcepts, as: :conceptable
end
end
key_concept.rb
class KeyConcept < ActiveRecord::Base
belongs_to :conceptable, polymorphic: true
end
piece.rb
class Piece < ActiveRecord::Base
include Connectable
include Conceptable
has_many :sections
has_many :subsections, through: :sections
has_many :links
end
I don't know if this is a problem in the controller?
class KeyconceptsController < ApplicationController
include KeyconceptsHelper
def whereami
if params[:piece_id]
#piece = Piece.find(params[:piece_id])
here = #piece
parameter = :piece_id
type = "Piece"
elsif params[:section_id]
#section = ::Section.find(params[:section_id])
#piece = #section.piece_id
here = #section
parameter = :section_id
type = "Section"
elsif params[:subsection_id]
#subsection = Subsection.find(params[:subsection_id])
#section = #subsection.section_id
#piece = Section.find(id=#section).piece_id
here = #subsection
parameter = :subsection_id
type = "Subsection"
elsif params[:subsubsection_id]
#subsubsection = Subsubsection.find(params[:subsubsection_id])
#subsection = #subsubsection.subsection_id
#section = Subsection.find(id=#subsection).section_id
#piece = Section.find(id=#section).piece_id
here = #subsubsection
parameter = :subsubsection_id
type = "Subsubsection"
end
end
def redirect
if type == "Piece"
redirect_to piece_path(#piece)
elsif type == "Section"
redirect_to piece_section_path(#piece, #section)
elsif type == "Subsection"
redirect_to piece_section_subsection_path(#piece, #section, #subsection)
elsif type == "Subsubsection"
redirect_to piece_section_subsection_subsubsection_path(#piece, #section, #subsection, #subsubsection)
end
end
def index
whereami.call
end
def show
whereami.call
r = redirect.call
end
def new
#keyconcept = KeyConcept.new
#keyconcept.conceptable_id = here.id
end
def create
whereami.call
#keyconcept = KeyConcept.new(keyconcept_params)
#keyconcept.conceptable_id = params[parameter]
#keyconcept.conceptable_type = type
#keyconcept.save
redirect.call
end
def destroy
here.destroy
redirect.call
flash.notice = "#{type} '#{here.name}' from '#{#piece.name}' deleted!"
end
def edit
whereami.call
end
def update
whereami.call
here.update(keyconcept_params)
flash.notice = "#{type} '#{here.name}' Updated!"
redirect.call
end
end
I have reloaded the console and I get the same error. I have also tried to do a few things in the console and this: Piece.first.keyconcepts, does not work (i get the same NameError: uninitialized constant Piece::Keyconcept) however this: KeyConcept.first DOES work, i even though i get nil because i havent created any instances yet.
I notice that, in the error message it says Keyconcept and not camelcase KeyConcept. I think this is where the problem lies but I do not have enough experience to understand it.
I would appreciate help in solving this!
The problem here is you are not following proper conventions
Change your model name to Keyconcept and file_name to keyconcept.rb
keyconcept.rb
class Keyconcept < ActiveRecord::Base
belongs_to :conceptable, polymorphic: true
end
OR
You will need to change keyconcepts to key_concepts in the following places:
routes
resources :keyconcepts
controller name
class KeyConceptsController < ApplicationController
...
end
controller file name
key_concepts_controller.rb
strong params
def key_concept_params
params.require(:key_concept).permit(:your, :params)
end
associations
has_many :key_concepts

Ruby on Rails [has_one]

So I'm pretty new to Rails; I have an assignment I'm trying to finish for my coursework and I'm stuck. I already have Topics and Posts for a reddit clone that the assignment is on. But now I'm supposed to add one summary for each post and after diving into that for awhile, I am stuck.
Here are the assignment requirements:
Create a Summary model which references posts.
Modify Post so that it has_one summary.
Create the necessary routes to create and show post summaries.
Create Summary views to create and show post summaries.
The error I'm hitting when I try to follow a 'Summary' link I've created to sit next to a post title:
ActionController::UrlGenerationError in Topics#show
No route matches {:action=>"show", :controller=>"summaries", :id=>nil, :post_id=>nil, :topic_id=>"1"} missing required keys: [:id, :post_id]
It's pointing me to the Topics#show method which looks like this:
def show
#topic = Topic.find(params[:id])
#posts = #topic.posts
authorize #topic
end
And my SummariesController looks like this:
class SummariesController < ApplicationController
def new
#topic = Topic.find(params[:topic_id])
#post = #topic.posts
#summary = Summary.new
authorize #summary
end
def create
#topic = Topic.find(params[:topic_id])
#post = #topic.posts
#summary = Summary.new(params.require(:summary).permit(:description))
authorize #summary
if #summary.save
flash[:notice] = "Your post summary was saved"
redirect_to [#topic, #post]
else
flash[:error] = "Sorry. There was an error saving your summary. Please try again"
render :new
end
end
def show
#summary = Summary.find(params[:id])
#post = Post.find(params[:post_id])
#topic = Topic.find(params[:topic_id])
authorize #summary
end
end
And here are my models:
Post:
class Post < ActiveRecord::Base
has_many :comments
has_one :summary
belongs_to :user
belongs_to :topic
default_scope { order('created_at DESC') }
end
Topic:
class Topic < ActiveRecord::Base
has_many :posts
end
Summary:
class Summary < ActiveRecord::Base
belongs_to :post
end
Lastly here is my routes file and the html.erb link:
Rails.application.routes.draw do
devise_for :users
resources :topics do
resources :posts, except: [:index] do
resources :summaries, except: [:index, :edit]
end
end
get 'about' => 'welcome#about'
root to: 'welcome#index'
end
html.erb summary Link:
<small>
<%= link_to "Summary", topic_post_summary_path(#topic, #post, #summary)%>
</small>
I'm not sure where I'm going wrong here.

Nested (and Unested at-same-time) Resources better approach

I have two models like that
class Plan < ActiveRecord::Base
belongs_to :profile
And
class Profile < ActiveRecord::Base
has_many :plans
And routes like: (I need to)
resources :profiles do
resources :plans
end
resources :plans
So, following up ruby-on-rails - Problem with Nested Resources, I've made my PLANS index controller like this, to works NESTED and UNESTED at same time (the only way I've found for now):
def index
if params.has_key? :profile_id
#profile = Profile.find(params[:profile_id])
#plans = #profile.plans
else
#plans = Plan.all
end
There is a cleaner approach to this?
I have another models in this situation, and putting all actions, in all controllers to behave like this is cumbersome.
You gave me an idea:
models/user.rb:
class User < ActiveRecord::Base
has_many :posts
attr_accessible :name
end
models/post.rb:
class Post < ActiveRecord::Base
belongs_to :user
attr_accessible :title, :user_id
end
controllers/posts_controller.rb:
class PostsController < ApplicationController
belongs_to :user # creates belongs_to_user filter
# #posts = Post.all # managed by belongs_to_user filter
# GET /posts
# GET /posts.json
def index
respond_to do |format|
format.html # index.html.erb
format.json { render json: #posts }
end
end
end
And now the substance:
controllers/application_controller.rb:
class ApplicationController < ActionController::Base
protect_from_forgery
def self.belongs_to(model)
# Example: model == :user
filter_method_name = :"belongs_to_#{model}_index" # :belongs_to_user_index
foreign_key = "#{model}_id" # 'user_id'
model_class = model.to_s.classify # User
class_eval <<-EOV, __FILE__, __LINE__ + 1
def #{filter_method_name} # def belongs_to_user_index
if params.has_key? :'#{foreign_key}' # if params.has_key? :user_id
instance_variable_set :"##{model}", # instance_variable_set :"#user",
#{model_class}.find(params[:'#{foreign_key}']) # User.find(params[:user_id])
instance_variable_set :"#\#{controller_name}", # instance_variable_set :"##{controller_name}",
##{model}.send(controller_name.pluralize) # #user.send(controller_name.pluralize)
else # else
instance_variable_set :"#\#{controller_name}", # instance_variable_set :"##{controller_name}",
controller_name.classify.constantize.all # controller_name.classify.constantize.all
end # end
end # end
EOV
before_filter filter_method_name, only: :index # before_filter :belongs_to_user_index, only: :index
end
end
The code is not complex to understand if you have notions of Ruby metaprogramming: it declares a before_filter which declares the instance variables inferring the names from the controller name and from the association. It is implemented just for the index actions, which is the only action using the plural instance variable version, but it should be easy to write a filter version for the other actions.

Resources