Home > Mobile >  Reusing model in ruby on rails application
Reusing model in ruby on rails application

Time:11-05

I'm currently in the process of adding comments to one of my models MaintenanceRequest and wondering how I should go about this.

I have a model Offer which already has comments built and it is set up with ActionCable as follows

# models/comment.rb

class Comment < ApplicationRecord
  belongs_to :user
  belongs_to :offer
end
# channels/comment_channel.rb

class CommentChannel < ApplicationCable::Channel
  def subscribed
    offer = Offer.find params[:offer]
    stream_for offer
  end
end

and the comment controller looks as

# controllers/comments_controller.rb

class CommentsController < ApplicationController
  before_action :authenticate_user!
  before_action :is_valid_offer

  def create
    offer = Offer.find(comment_params[:offer_id])

    if comment_params[:content].blank?
      # return redirect_to request.referrer, alert: 'Comment cannot be empty.'
      return render json: {success: false}
    end

    if offer.user_id != current_user.id && offer.landlord_id != current_user.id
      return render json: {success: false}
    end

    @comment = Comment.new(
      user_id: current_user.id,
      offer_id: offer.id,
      content: comment_params[:content],
    )

    if @comment.save
      # redirect_to request.referrer, notice: 'Comment sent.'
      CommentChannel.broadcast_to offer, message: render_comment(@comment)
      render json: {success: true}
    else
      redirect_to request.referrer, alert: "Couldn't send comment."
      render json: {success: true}
    end
  end
end

My question is that is there a way I could use the same model for comments belonging to MaintenanceRequest or should I just create new model and controller for there comments? It would seem that this would get quite messy if I tried to reuse the current comment model. What would be the "best practice" here?

CodePudding user response:

If you want to make a resuable assocation you want to use a polymorphic assocation:

class Comment < ApplicationRecord
  belongs_to :user
  belongs_to :commentable, 
    polymorphic: true

  validates :content, 
    presence: true
end

Instead of using a single column holding an integer pointing to a fixed table this "cheats" the relation database model by using a one column for the other tables primary key and storing the class name of the entitity in a string column (in this example called commentable_type).

You can create the needed columns by passing the polymorphic option when generating the migration (or model):

rails g migration add_commentable_to_comments commentable:belongs_to{polymorphic}

However that controller action is a bit of train wreck. It should look more like this:

class CommentsController < ApplicationController
  before_action :authenticate_user!
  before_action :set_offer
  before_action :authorize_user

  def create
    @comment = @offer.comments.new(comment_params) do |c|
      c.user = current_user
    end

    if @comment.save
      # redirect_to request.referrer, notice: 'Comment sent.'
      CommentChannel.broadcast_to offer, message: render_comment(@comment)
      # rendering json here is actually just stupid. Just use a HTTP status code 
      # as a response if you're not actually returning the rendered entity.
      render json: { success: true }, 
        status: :created,
        location: @comment
    else
      # either return JSON which is actually useful or just a status code
      render json: { success: false }, 
        status: :unprocessable_entity
    end
  end

  private

  def set_offer
    @offer = Offer.find(params[:offer_id])
  end

  # this should be extracted to an authorization layer such as Pundit
  def authorize_offer
    if @offer.user_id != current_user.id && @offer.landlord_id != current_user.id 
      render json: { success: false },
        status: :unauthorized
    end
  end
end

There are two way to resuse this controller for different types of "commentables". You can either check for the presence of the offer_id or maintenance_request_id in a single controller and use that to deduce the class or you can use inheritance to destribute the responsibilities better:

# routes.rb

resources :offers do
  resources :comments, 
    only: :create,
    module: :offers
end

resources :maintainence_requests do
  resources :comments, 
    only: :create,
    module: :maintainence_requests
end
class CommentsController
  before_action :set_commentable, only: [:new, :create, :index]
  before_action :authenticate_user!
  attr_reader :commentable

  def create
    @comment = commentable.comments.new(comment_params) do |c|
      c.user = current_user
    end
    if @comment.save
      # simple extendability feature that lets you "tap into" the flow 
      # by passing a block when calling super in subclasses
      yield @comment if block_given?
      render json: @comment,
             status: :created
    else
      render json: { errors: @comment.errors.full_messages },
             status: :unprocessable_entity
    end
  end

  
  private

  # Uses simple heuristics based on module nesting to guess the name of the model
  # that is being commented upon. Overide if needed.
  # @example Foos::BarsController -> Foo
  def commentable_class
    @commentable_class ||= self.class.module_parent.name.singularize.constantize
  end 

  def commentable_param_key
    commentable_class.model_name.param_key
  end

  def set_commentable
    @commentable = commentable_class.find("#{commentable_param_key}_id")
  end

  def comment_params
    params.require(:comment)
          .permit(:content)
  end
end
module Offers
  class CommentsController < ::CommentsController
    # POST /offers/:offer_id/comments
    def create
      # block is called after a comment is succesfully saved but before 
      # the response is set
      super do |comment|
        CommentChannel.broadcast_to comment.offer, 
          message: render_comment(comment) 
      end
    end
  end
end
module MaintainenceRequests
  class CommentsController < ::CommentsController
  end
end
  • Related